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:
2026-09-24 03:25:13 +08:00
committed by GitHub
parent f807c22a36
commit 3454ce67fe
16 changed files with 549 additions and 65 deletions
@@ -52,6 +52,60 @@ describe("NewConnectionScreen", () => {
expect(screen.getByLabelText("User")).toHaveValue("u");
});
it("paste a Neon URL pre-fills every required setting, including SSL mode", async () => {
const user = userEvent.setup();
render(<NewConnectionScreen />);
await user.type(
screen.getByLabelText(/connection uri/i),
"postgresql://neondb_owner:npg_secret@ep-hidden-star-b3hqtp4x-pooler.c-4.ap-southeast-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require",
);
// Every field the URL can supply is filled, so no manual cleanup is needed
expect(screen.getByLabelText("Host")).toHaveValue(
"ep-hidden-star-b3hqtp4x-pooler.c-4.ap-southeast-1.aws.neon.tech",
);
expect(screen.getByLabelText("Port")).toHaveValue(5432);
expect(screen.getByLabelText("User")).toHaveValue("neondb_owner");
expect(screen.getByLabelText("Password")).toHaveValue("npg_secret");
expect(screen.getByLabelText("Database")).toHaveValue("neondb");
// The label stays untouched but is suggested from the database name.
expect(screen.getByLabelText(/connection label/i)).toHaveValue("");
expect(screen.getByLabelText(/connection label/i)).toHaveAttribute(
"placeholder",
"neondb",
);
// SSL mode is visible on the SSH / SSL tab...
await user.click(screen.getByRole("button", { name: /ssh \/ ssl/i }));
await user.click(screen.getByRole("button", { name: "SSL" }));
expect(screen.getByRole("button", { name: "SSL Mode" })).toHaveTextContent("Require");
// ...and reaches the save payload with no extra clicks.
await user.click(screen.getByText("Save Connection"));
await waitFor(() => expect(createConnection).toHaveBeenCalledTimes(1));
expect(createConnection).toHaveBeenCalledWith(
expect.objectContaining({
name: "neondb",
db_type: "postgresql",
ssl_mode: "require",
username: "neondb_owner",
password: "npg_secret",
database: "neondb",
}),
);
});
it("a typed label wins over the URL-derived suggestion", async () => {
const user = userEvent.setup();
render(<NewConnectionScreen />);
await user.type(screen.getByLabelText(/connection uri/i), "postgresql://u:p@localhost:5432/db");
await user.type(screen.getByLabelText(/connection label/i), "My DB");
await user.click(screen.getByText("Save Connection"));
await waitFor(() => expect(createConnection).toHaveBeenCalledTimes(1));
expect(createConnection).toHaveBeenCalledWith(
expect.objectContaining({ name: "My DB" }),
);
});
it("clicking a provider tab reveals the form with that db_type", async () => {
const user = userEvent.setup();
render(<NewConnectionScreen />);
@@ -60,6 +114,19 @@ describe("NewConnectionScreen", () => {
expect(screen.getByLabelText("Port")).toHaveValue(3306);
});
it("NeonDB card preselects Require SSL in the save payload", async () => {
const user = userEvent.setup();
render(<NewConnectionScreen />);
await user.click(screen.getByRole("button", { name: /NeonDB/i }));
await user.type(screen.getByLabelText(/connection label/i), "Neon prod");
await user.type(screen.getByLabelText("Host"), "ep-x-pooler.eu.aws.neon.tech");
await user.click(screen.getByText("Save Connection"));
await waitFor(() => expect(createConnection).toHaveBeenCalledTimes(1));
expect(createConnection).toHaveBeenCalledWith(
expect.objectContaining({ name: "Neon prod", ssl_mode: "require" }),
);
});
it("SQLite tab swaps the URI input for a File Path input", async () => {
const user = userEvent.setup();
render(<NewConnectionScreen />);
@@ -103,6 +170,20 @@ describe("NewConnectionScreen", () => {
expect(notify).toHaveBeenCalledWith("Connection successful", "success");
});
it("tests a pasted URL with its SSL mode", async () => {
const user = userEvent.setup();
render(<NewConnectionScreen />);
await user.type(
screen.getByLabelText(/connection uri/i),
"postgresql://u:p@ep-x-pooler.eu.aws.neon.tech/db?sslmode=verify-full",
);
await user.click(screen.getByText("Test Connection"));
await waitFor(() => expect(testConnection).toHaveBeenCalledTimes(1));
expect(testConnection).toHaveBeenCalledWith(
expect.objectContaining({ ssl_mode: "verify-full" }),
);
});
it("invokes onCancel when Back is clicked", async () => {
const user = userEvent.setup();
const onCancel = vi.fn();