fix(connections): persist SSL settings and honor a URL's sslmode (#36)
Connecting to a managed provider (Neon in the report) failed with "connection is insecure (try using `sslmode=require`)" because the TLS mode never reached the backend: - ConnectionFormData carried no ssl_* fields and both submit handlers (NewConnectionScreen.buildPayload, EditConnectionModal save + test) omitted them, so the SSH/SSL tab's mode was silently dropped before IPC. - SslFields wrote ssl_ca_cert / ssl_client_cert / ssl_client_key — names the Rust ConnectionInput ignores (it expects ssl_ca_path / ssl_cert_path / ssl_key_path). - parseConnectionString ignored query params, so a pasted Neon/Supabase URL's ?sslmode=require was discarded. Pasting a URI now fills every setting it can (sslmode / ssl-mode, including MySQL's REQUIRED and VERIFY_IDENTITY spellings) and offers a derived label (database name, else host/file stem) as a placeholder that Test/Save uses when the field is left blank. Managed presets pre-select their TLS mode (Supabase / Neon → Require, PlanetScale → Verify Full). Backend safety net: db::tls::resolve_ssl_mode defaults *.neon.tech / *.supabase.co / *.psdb.cloud to `require` when a connection has no ssl_mode, so connections saved before this fix keep working; an explicit mode (including disable) always wins. Tests: +15 frontend (parser, form payload, SSL field names, edit modal), +4 Rust (resolve_ssl_mode / tls_decision_for).
This commit is contained in:
@@ -10,18 +10,23 @@ const { updateConnection, loadAll } = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../stores/connectionStore", () => ({
|
||||
useConnectionStore: (sel: (s: any) => any) =>
|
||||
sel({ updateConnection, loadAll, folders: [], tags: [] }),
|
||||
useConnectionStore: Object.assign(
|
||||
(sel: (s: any) => any) =>
|
||||
sel({ updateConnection, loadAll, folders: [], tags: [] }),
|
||||
{ getState: () => ({ getConnectionPassword: async () => "pw" }) },
|
||||
),
|
||||
}));
|
||||
vi.mock("../../lib/commands", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../lib/commands")>();
|
||||
return {
|
||||
...actual,
|
||||
updateConnection: vi.fn(),
|
||||
testConnection: vi.fn(),
|
||||
testConnection: vi.fn().mockResolvedValue({ ok: true }),
|
||||
saveConnectionPassword: vi.fn(),
|
||||
saveConnectionSshPassword: vi.fn(),
|
||||
saveConnectionSshPassphrase: vi.fn(),
|
||||
getConnectionSshPassword: vi.fn().mockResolvedValue(null),
|
||||
getConnectionSshPassphrase: vi.fn().mockResolvedValue(null),
|
||||
deleteConnectionPassword: vi.fn(),
|
||||
};
|
||||
});
|
||||
@@ -121,4 +126,62 @@ describe("EditConnectionModal", () => {
|
||||
);
|
||||
expect(vi.mocked(commands.saveConnectionPassword)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("persists the existing SSL settings on save", async () => {
|
||||
render(
|
||||
<EditConnectionModal
|
||||
connection={{
|
||||
...baseConn,
|
||||
ssl_mode: "verify-full",
|
||||
ssl_ca_path: "/tmp/ca.pem",
|
||||
ssl_cert_path: "/tmp/client.pem",
|
||||
ssl_key_path: "/tmp/client.key",
|
||||
}}
|
||||
open={true}
|
||||
onClose={() => {}}
|
||||
onSaved={() => {}}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: /^save$/i }));
|
||||
await waitFor(() => expect(vi.mocked(commands.updateConnection)).toHaveBeenCalled());
|
||||
expect(vi.mocked(commands.updateConnection)).toHaveBeenCalledWith(
|
||||
"c1",
|
||||
expect.objectContaining({
|
||||
ssl_mode: "verify-full",
|
||||
ssl_ca_path: "/tmp/ca.pem",
|
||||
ssl_cert_path: "/tmp/client.pem",
|
||||
ssl_key_path: "/tmp/client.key",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("tests with the same SSL + SSH config it would save", async () => {
|
||||
render(
|
||||
<EditConnectionModal
|
||||
connection={{
|
||||
...baseConn,
|
||||
ssl_mode: "require",
|
||||
ssh_host: "jump.example.com",
|
||||
ssh_port: 22,
|
||||
ssh_user: "tunnel",
|
||||
ssh_auth_method: "key",
|
||||
ssh_private_key_path: "/tmp/id_ed25519",
|
||||
}}
|
||||
open={true}
|
||||
onClose={() => {}}
|
||||
onSaved={() => {}}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: /^test$/i }));
|
||||
await waitFor(() => expect(vi.mocked(commands.testConnection)).toHaveBeenCalled());
|
||||
expect(vi.mocked(commands.testConnection)).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
ssl_mode: "require",
|
||||
ssh_host: "jump.example.com",
|
||||
ssh_user: "tunnel",
|
||||
ssh_auth_method: "key",
|
||||
ssh_private_key_path: "/tmp/id_ed25519",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user