diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 3f196e2fc6..b35d3cf8e0 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -8249,7 +8249,7 @@ function openExternalUrl(url: string) {
{{ t("connection.driverInstall.fullError") }}
-
{{ agentInstallError }}
+
{{ agentInstallError }}
@@ -8266,14 +8266,14 @@ function openExternalUrl(url: string) { - + {{ t("connection.connectFailedTitle") }} -
+
{{ t("connection.fullErrorMessage") }}
-
{{ connectionErrorDetail }}
+
{{ connectionErrorDetail }}
diff --git a/apps/desktop/src/lib/__tests__/connection/connectionDialogErrorLayout.spec.ts b/apps/desktop/src/lib/__tests__/connection/connectionDialogErrorLayout.spec.ts new file mode 100644 index 0000000000..dd274bfac7 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/connection/connectionDialogErrorLayout.spec.ts @@ -0,0 +1,21 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const dialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8"); + +describe("connection dialog error layout", () => { + it("installs a required agent before running a connection test", () => { + const installIndex = dialogSource.indexOf("await ensureRequiredAgentDriverInstalled(config);"); + const testIndex = dialogSource.indexOf("const result = await testConnectionWithTimeout(config, runId);"); + + expect(installIndex).toBeGreaterThan(-1); + expect(testIndex).toBeGreaterThan(installIndex); + }); + + it("wraps long connection and driver installation errors inside the dialog", () => { + const wrappingClasses = "min-w-0 max-w-full overflow-x-hidden overflow-y-auto whitespace-pre-wrap break-all [overflow-wrap:anywhere]"; + + expect(dialogSource.split(wrappingClasses)).toHaveLength(3); + expect(dialogSource).toContain(''); + }); +}); diff --git a/apps/desktop/src/lib/connection/agentDriverInstallHint.ts b/apps/desktop/src/lib/connection/agentDriverInstallHint.ts index 7506b8537b..e1725f023a 100644 --- a/apps/desktop/src/lib/connection/agentDriverInstallHint.ts +++ b/apps/desktop/src/lib/connection/agentDriverInstallHint.ts @@ -37,15 +37,22 @@ export function agentDriverInstallKey(dbType: DatabaseType | undefined, driverPr return driverProfile && driverProfile !== dbType ? driverProfile : dbType; } +function usesManagedAgentDriver(dbType: DatabaseType | undefined, driverProfile?: string): boolean { + if (supportsDriverManagement(dbType)) return true; + if (dbType !== "mongodb") return false; + const profile = driverProfile?.trim().toLowerCase(); + return profile === "mongodb-legacy" || profile === "mongodb_legacy" || profile === "legacy"; +} + export function showAgentDriverInstallHint(dbType: DatabaseType | undefined, drivers: readonly AgentDriverInstallState[], driverProfile?: string): boolean { - if (!supportsDriverManagement(dbType)) return false; + if (!usesManagedAgentDriver(dbType, driverProfile)) return false; const driverKey = agentDriverInstallKey(dbType, driverProfile); if (!driverKey) return false; return drivers.find((driver) => driver.db_type === driverKey)?.installed !== true; } export function hasAgentDriverUpdate(dbType: DatabaseType | undefined, drivers: readonly AgentDriverInstallState[], driverProfile?: string): boolean { - if (!supportsDriverManagement(dbType)) return false; + if (!usesManagedAgentDriver(dbType, driverProfile)) return false; const driverKey = agentDriverInstallKey(dbType, driverProfile); return drivers.find((driver) => driver.db_type === driverKey)?.update_available === true; } diff --git a/apps/desktop/src/lib/mongo/mongoConnectionOptions.ts b/apps/desktop/src/lib/mongo/mongoConnectionOptions.ts index 7fb36fc585..de00641f7e 100644 --- a/apps/desktop/src/lib/mongo/mongoConnectionOptions.ts +++ b/apps/desktop/src/lib/mongo/mongoConnectionOptions.ts @@ -75,7 +75,10 @@ export function mongodbAuthFailureHint(message: string): string { const source = message.match(/source='([^']+)'/)?.[1]; if (!source || !message.includes("Exception authenticating MongoCredential")) return message; - return `${message}\n\nCurrent authentication database: ${source}. If this user was created in admin, set Authentication database to admin or add authSource=admin to URL params.`; + const verificationHint = `Current authentication database: ${source}. The server rejected these credentials. Verify the username and password, and confirm that the user was created in ${source}.`; + if (source.toLowerCase() === "admin") return `${message}\n\n${verificationHint}`; + + return `${message}\n\n${verificationHint} If the user was created in admin, set Authentication database to admin or add authSource=admin to URL params.`; } function parseMongoUrlParams(urlParams: string | undefined): URLSearchParams { diff --git a/crates/dbx-core/src/agent_connection.rs b/crates/dbx-core/src/agent_connection.rs index e342ca1ce4..5e0d679f12 100644 --- a/crates/dbx-core/src/agent_connection.rs +++ b/crates/dbx-core/src/agent_connection.rs @@ -260,9 +260,13 @@ pub fn mongo_legacy_error_with_auth_hint(err: &str) -> String { return err.to_string(); }; let source = &source[..source_end]; - format!( - "{err}\n\nCurrent authentication database: {source}. If this user was created in admin, set Authentication database to admin or add authSource=admin to URL params." - ) + let verification_hint = format!( + "Current authentication database: {source}. The server rejected these credentials. Verify the username and password, and confirm that the user was created in {source}." + ); + if source.eq_ignore_ascii_case("admin") { + return format!("{err}\n\n{verification_hint}"); + } + format!("{err}\n\n{verification_hint} If the user was created in admin, set Authentication database to admin or add authSource=admin to URL params.") } pub fn mongo_uses_legacy_driver(config: &ConnectionConfig) -> bool { @@ -861,6 +865,8 @@ mod tests { assert!(hinted.starts_with(err)); assert!(hinted.contains("Current authentication database: admin")); + assert!(hinted.contains("The server rejected these credentials")); + assert!(!hinted.contains("add authSource=admin")); } #[test] diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 196ebe241d..941247d6ea 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -5591,7 +5591,7 @@ mod tests { assert_eq!( mongo_legacy_error_with_auth_hint(err), - "Agent RPC error: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='rwuser', source='gray_lite_twin_fat'}\n\nCurrent authentication database: gray_lite_twin_fat. If this user was created in admin, set Authentication database to admin or add authSource=admin to URL params." + "Agent RPC error: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='rwuser', source='gray_lite_twin_fat'}\n\nCurrent authentication database: gray_lite_twin_fat. The server rejected these credentials. Verify the username and password, and confirm that the user was created in gray_lite_twin_fat. If the user was created in admin, set Authentication database to admin or add authSource=admin to URL params." ); } diff --git a/packages/app-tests/agentDriverInstallHint.test.ts b/packages/app-tests/agentDriverInstallHint.test.ts index d8db77f953..5e8ab8d8d5 100644 --- a/packages/app-tests/agentDriverInstallHint.test.ts +++ b/packages/app-tests/agentDriverInstallHint.test.ts @@ -18,6 +18,13 @@ test("shows the agent driver install hint for Access when missing", () => { assert.equal(showAgentDriverInstallHint("access", [{ db_type: "access", installed: false }]), true); }); +test("installs the MongoDB Legacy agent before testing a legacy MongoDB connection", () => { + assert.equal(agentDriverInstallKey("mongodb", "mongodb-legacy"), "mongodb"); + assert.equal(showAgentDriverInstallHint("mongodb", [{ db_type: "mongodb", installed: false }], "mongodb-legacy"), true); + assert.equal(showAgentDriverInstallHint("mongodb", [{ db_type: "mongodb", installed: true }], "mongodb-legacy"), false); + assert.equal(showAgentDriverInstallHint("mongodb", [{ db_type: "mongodb", installed: false }], "mongodb"), false); +}); + test("does not show agent driver install hints for built-in database types", () => { assert.equal(showAgentDriverInstallHint("mysql", [{ db_type: "informix", installed: false }]), false); }); diff --git a/packages/app-tests/mongoConnectionOptions.test.ts b/packages/app-tests/mongoConnectionOptions.test.ts index 0d22927f4e..d35c40191f 100644 --- a/packages/app-tests/mongoConnectionOptions.test.ts +++ b/packages/app-tests/mongoConnectionOptions.test.ts @@ -56,10 +56,18 @@ test("adds a MongoDB authSource hint for legacy authentication failures", () => assert.equal( mongodbAuthFailureHint(message), - "Agent RPC error: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='rwuser', source='gray_lite_twin_fat'}\n\nCurrent authentication database: gray_lite_twin_fat. If this user was created in admin, set Authentication database to admin or add authSource=admin to URL params.", + "Agent RPC error: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='rwuser', source='gray_lite_twin_fat'}\n\nCurrent authentication database: gray_lite_twin_fat. The server rejected these credentials. Verify the username and password, and confirm that the user was created in gray_lite_twin_fat. If the user was created in admin, set Authentication database to admin or add authSource=admin to URL params.", ); }); +test("does not suggest authSource=admin when MongoDB already authenticated against admin", () => { + const message = "Agent RPC error: Exception authenticating MongoCredential{mechanism=SCRAM-SHA-1, userName='rwuser', source='admin'}"; + const hinted = mongodbAuthFailureHint(message); + + assert.match(hinted, /The server rejected these credentials/); + assert.doesNotMatch(hinted, /add authSource=admin/); +}); + test("adds a MongoDB URL encoding hint for reserved password characters", () => { const message = "MongoDB connection failed: Kind: An invalid argument was provided: password must be URL encoded";