Skip to content

Commit a2ae441

Browse files
authored
Merge pull request #252 from datlechin/fix/ssl-mode-mismatch-and-redis-navigation
fix: correct SSL mode string comparison and Redis double-navigation race
2 parents 4524a2f + 0aa1bf6 commit a2ae441

10 files changed

Lines changed: 209 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- SSL/TLS always being enabled for MongoDB, Redis, and ClickHouse connections due to case mismatch in SSL mode string comparison (#249)
13+
- Redis sidebar click showing data briefly then going empty due to double-navigation race condition (#251)
14+
- MongoDB showing "Invalid database name: ''" when connecting without a database name
15+
1016
### Added
1117

1218
- Safe mode levels: per-connection setting with 6 levels (Silent, Alert, Alert Full, Safe Mode, Safe Mode Full, Read-Only) replacing the boolean read-only toggle, with confirmation dialogs and Touch ID/password authentication for stricter levels

Plugins/ClickHouseDriverPlugin/ClickHousePlugin.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
7878

7979
func connect() async throws {
8080
let useTLS = config.additionalFields["sslMode"] != nil
81-
&& config.additionalFields["sslMode"] != "disable"
82-
let skipVerification = config.additionalFields["sslMode"] == "required"
81+
&& config.additionalFields["sslMode"] != "Disabled"
82+
let skipVerification = config.additionalFields["sslMode"] == "Required"
8383

8484
let urlConfig = URLSessionConfiguration.default
8585
urlConfig.timeoutIntervalForRequest = 30
@@ -533,7 +533,7 @@ final class ClickHousePluginDriver: PluginDatabaseDriver, @unchecked Sendable {
533533

534534
private func buildRequest(query: String, database: String, queryId: String? = nil) throws -> URLRequest {
535535
let useTLS = config.additionalFields["sslMode"] != nil
536-
&& config.additionalFields["sslMode"] != "disable"
536+
&& config.additionalFields["sslMode"] != "Disabled"
537537

538538
var components = URLComponents()
539539
components.scheme = useTLS ? "https" : "http"

Plugins/MongoDBDriverPlugin/MongoDBConnection.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ final class MongoDBConnection: @unchecked Sendable {
104104
user: String,
105105
password: String?,
106106
database: String,
107-
sslMode: String = "disabled",
107+
sslMode: String = "Disabled",
108108
sslCACertPath: String = "",
109109
sslClientCertPath: String = "",
110110
readPreference: String? = nil,
@@ -169,10 +169,10 @@ final class MongoDBConnection: @unchecked Sendable {
169169
"authSource=admin"
170170
]
171171

172-
let sslEnabled = sslMode != "disabled" && !sslMode.isEmpty
172+
let sslEnabled = ["Preferred", "Required", "Verify CA", "Verify Identity"].contains(sslMode)
173173
if sslEnabled {
174174
params.append("tls=true")
175-
let verifiesCert = sslMode == "verify_ca" || sslMode == "verify_identity"
175+
let verifiesCert = sslMode == "Verify CA" || sslMode == "Verify Identity"
176176
if !verifiesCert {
177177
params.append("tlsAllowInvalidCertificates=true")
178178
}

Plugins/MongoDBDriverPlugin/MongoDBPluginDriver.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ final class MongoDBPluginDriver: PluginDatabaseDriver {
2828
self.currentDb = config.database
2929
}
3030

31+
private static let systemDatabases: Set<String> = ["admin", "local", "config"]
32+
3133
// MARK: - Connection Management
3234

3335
func connect() async throws {
@@ -37,14 +39,25 @@ final class MongoDBPluginDriver: PluginDatabaseDriver {
3739
user: config.username,
3840
password: config.password,
3941
database: currentDb,
40-
sslMode: config.additionalFields["sslMode"] ?? "disabled",
42+
sslMode: config.additionalFields["sslMode"] ?? "Disabled",
4143
sslCACertPath: config.additionalFields["sslCACertPath"] ?? "",
4244
sslClientCertPath: config.additionalFields["sslClientCertPath"] ?? "",
4345
readPreference: config.additionalFields["mongoReadPreference"],
4446
writeConcern: config.additionalFields["mongoWriteConcern"]
4547
)
4648

4749
try await conn.connect()
50+
51+
if currentDb.isEmpty {
52+
do {
53+
let dbs = try await conn.listDatabases()
54+
currentDb = dbs.first { !Self.systemDatabases.contains($0) } ?? dbs.first ?? ""
55+
} catch {
56+
conn.disconnect()
57+
throw error
58+
}
59+
}
60+
4861
mongoConnection = conn
4962
}
5063

Plugins/MySQLDriverPlugin/MariaDBPluginConnection.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ struct MariaDBPluginQueryResult {
5555

5656
struct MySQLSSLConfig {
5757
enum Mode: String {
58-
case disabled
59-
case preferred
60-
case required
61-
case verifyCa = "verify_ca"
62-
case verifyIdentity = "verify_identity"
58+
case disabled = "Disabled"
59+
case preferred = "Preferred"
60+
case required = "Required"
61+
case verifyCa = "Verify CA"
62+
case verifyIdentity = "Verify Identity"
6363
}
6464

6565
let mode: Mode
@@ -68,7 +68,7 @@ struct MySQLSSLConfig {
6868
let clientKeyPath: String
6969

7070
init(from fields: [String: String]) {
71-
self.mode = Mode(rawValue: fields["sslMode"] ?? "disabled") ?? .disabled
71+
self.mode = Mode(rawValue: fields["sslMode"] ?? "Disabled") ?? .disabled
7272
self.caCertificatePath = fields["sslCaCertPath"] ?? ""
7373
self.clientCertificatePath = fields["sslClientCertPath"] ?? ""
7474
self.clientKeyPath = fields["sslClientKeyPath"] ?? ""

Plugins/PostgreSQLDriverPlugin/LibPQPluginConnection.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ private let logger = Logger(subsystem: "com.TablePro.PostgreSQLDriver", category
1616
// MARK: - SSL Configuration
1717

1818
struct PQSSLConfig {
19-
var mode: String = "disable"
19+
var mode: String = "Disabled"
2020
var caCertificatePath: String = ""
2121
var clientCertificatePath: String = ""
2222
var clientKeyPath: String = ""
2323

2424
init() {}
2525

2626
init(additionalFields: [String: String]) {
27-
self.mode = additionalFields["sslMode"] ?? "disable"
27+
self.mode = additionalFields["sslMode"] ?? "Disabled"
2828
self.caCertificatePath = additionalFields["sslCaCertPath"] ?? ""
2929
self.clientCertificatePath = additionalFields["sslClientCertPath"] ?? ""
3030
self.clientKeyPath = additionalFields["sslClientKeyPath"] ?? ""

Plugins/RedisDriverPlugin/RedisPluginConnection.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ struct RedisSSLConfig {
2626
init() {}
2727

2828
init(additionalFields: [String: String]) {
29-
let sslMode = additionalFields["sslMode"] ?? "disable"
30-
self.isEnabled = sslMode != "disable"
29+
let sslMode = additionalFields["sslMode"] ?? "Disabled"
30+
self.isEnabled = sslMode != "Disabled"
3131
self.caCertificatePath = additionalFields["sslCaCertPath"] ?? ""
3232
self.clientCertificatePath = additionalFields["sslClientCertPath"] ?? ""
3333
self.clientKeyPath = additionalFields["sslClientKeyPath"] ?? ""

TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,13 @@ extension MainContentCoordinator {
8989
AppState.shared.isCurrentTabEditable = !isView && tableName.isEmpty == false
9090
toolbarState.isTableTab = true
9191
}
92-
runQuery()
92+
// Redis needs selectRedisDatabaseAndQuery to ensure the correct
93+
// database is SELECTed and session state is updated before querying.
94+
if connection.type == .redis, let dbIndex = Int(currentDatabase) {
95+
selectRedisDatabaseAndQuery(dbIndex)
96+
} else {
97+
runQuery()
98+
}
9399
return
94100
}
95101

TablePro/Views/Main/MainContentView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,10 @@ struct MainContentView: View {
733733
target = []
734734
}
735735
if sidebarState.selectedTables != target {
736+
// Don't clear sidebar selection while the table list is still loading.
737+
// Clearing it prematurely triggers SidebarSyncAction to re-select on
738+
// tables load, causing a double-navigation race condition.
739+
if target.isEmpty && tables.isEmpty { return }
736740
sidebarState.selectedTables = target
737741
}
738742
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
//
2+
// SSLModeStringTests.swift
3+
// TableProTests
4+
//
5+
// Tests that plugin SSL config structs correctly parse SSLMode raw values.
6+
// Plugin types are bundle targets and cannot be imported directly, so we
7+
// duplicate the config parsing logic here as private test helpers.
8+
//
9+
10+
import Foundation
11+
import Testing
12+
@testable import TablePro
13+
14+
// MARK: - Test Helpers (mirror plugin SSL config structs)
15+
16+
/// Mirror of MySQLSSLConfig.Mode from MariaDBPluginConnection.swift
17+
private enum TestMySQLSSLMode: String {
18+
case disabled = "Disabled"
19+
case preferred = "Preferred"
20+
case required = "Required"
21+
case verifyCa = "Verify CA"
22+
case verifyIdentity = "Verify Identity"
23+
}
24+
25+
/// Mirror of RedisSSLConfig init from RedisPluginConnection.swift
26+
private struct TestRedisSSLConfig {
27+
var isEnabled: Bool
28+
29+
init(additionalFields: [String: String]) {
30+
let sslMode = additionalFields["sslMode"] ?? "Disabled"
31+
self.isEnabled = sslMode != "Disabled"
32+
}
33+
}
34+
35+
/// Mirror of PQSSLConfig from LibPQPluginConnection.swift
36+
private struct TestPQSSLConfig {
37+
var mode: String = "Disabled"
38+
39+
init() {}
40+
41+
init(additionalFields: [String: String]) {
42+
self.mode = additionalFields["sslMode"] ?? "Disabled"
43+
}
44+
45+
var libpqSslMode: String {
46+
switch mode {
47+
case "Disabled": return "disable"
48+
case "Preferred": return "prefer"
49+
case "Required": return "require"
50+
case "Verify CA": return "verify-ca"
51+
case "Verify Identity": return "verify-full"
52+
default: return "disable"
53+
}
54+
}
55+
}
56+
57+
// MARK: - SSLMode Raw Values Match Plugin Expectations
58+
59+
@Suite("SSL Mode String Consistency")
60+
struct SSLModeStringTests {
61+
@Test("SSLMode.disabled.rawValue matches plugin disabled check")
62+
func disabledRawValue() {
63+
#expect(SSLMode.disabled.rawValue == "Disabled")
64+
}
65+
66+
@Test("SSLMode.required.rawValue matches plugin required check")
67+
func requiredRawValue() {
68+
#expect(SSLMode.required.rawValue == "Required")
69+
}
70+
71+
@Test("SSLMode.verifyCa.rawValue matches plugin verify CA check")
72+
func verifyCaRawValue() {
73+
#expect(SSLMode.verifyCa.rawValue == "Verify CA")
74+
}
75+
76+
@Test("SSLMode.verifyIdentity.rawValue matches plugin verify identity check")
77+
func verifyIdentityRawValue() {
78+
#expect(SSLMode.verifyIdentity.rawValue == "Verify Identity")
79+
}
80+
81+
@Test("All SSLMode cases round-trip through MySQL Mode enum")
82+
func mysqlModeRoundTrip() {
83+
for sslMode in SSLMode.allCases {
84+
let parsed = TestMySQLSSLMode(rawValue: sslMode.rawValue)
85+
#expect(parsed != nil, "MySQLSSLMode failed to parse '\(sslMode.rawValue)'")
86+
}
87+
}
88+
89+
@Test("MySQL Mode parses each SSLMode raw value to the correct case")
90+
func mysqlModeParsesCorrectCase() {
91+
#expect(TestMySQLSSLMode(rawValue: "Disabled") == .disabled)
92+
#expect(TestMySQLSSLMode(rawValue: "Preferred") == .preferred)
93+
#expect(TestMySQLSSLMode(rawValue: "Required") == .required)
94+
#expect(TestMySQLSSLMode(rawValue: "Verify CA") == .verifyCa)
95+
#expect(TestMySQLSSLMode(rawValue: "Verify Identity") == .verifyIdentity)
96+
}
97+
98+
@Test("Redis SSL disabled when sslMode is Disabled")
99+
func redisSSLDisabled() {
100+
let config = TestRedisSSLConfig(additionalFields: ["sslMode": "Disabled"])
101+
#expect(!config.isEnabled)
102+
}
103+
104+
@Test("Redis SSL enabled when sslMode is Required")
105+
func redisSSLEnabled() {
106+
let config = TestRedisSSLConfig(additionalFields: ["sslMode": "Required"])
107+
#expect(config.isEnabled)
108+
}
109+
110+
@Test("Redis SSL defaults to disabled when sslMode key is absent")
111+
func redisSSLDefaultDisabled() {
112+
let config = TestRedisSSLConfig(additionalFields: [:])
113+
#expect(!config.isEnabled)
114+
}
115+
116+
@Test("PostgreSQL maps all SSLMode raw values to correct libpq modes")
117+
func pqSSLModeMapping() {
118+
#expect(TestPQSSLConfig(additionalFields: ["sslMode": "Disabled"]).libpqSslMode == "disable")
119+
#expect(TestPQSSLConfig(additionalFields: ["sslMode": "Preferred"]).libpqSslMode == "prefer")
120+
#expect(TestPQSSLConfig(additionalFields: ["sslMode": "Required"]).libpqSslMode == "require")
121+
#expect(TestPQSSLConfig(additionalFields: ["sslMode": "Verify CA"]).libpqSslMode == "verify-ca")
122+
#expect(TestPQSSLConfig(additionalFields: ["sslMode": "Verify Identity"]).libpqSslMode == "verify-full")
123+
}
124+
125+
@Test("PostgreSQL default init uses Disabled")
126+
func pqDefaultInit() {
127+
let config = TestPQSSLConfig()
128+
#expect(config.mode == "Disabled")
129+
#expect(config.libpqSslMode == "disable")
130+
}
131+
132+
@Test("MongoDB SSL mode string comparisons use correct case")
133+
func mongoDBSSLModeStrings() {
134+
// These mirror the comparisons in MongoDBConnection.buildUri()
135+
let disabled = SSLMode.disabled.rawValue
136+
let verifyCa = SSLMode.verifyCa.rawValue
137+
let verifyIdentity = SSLMode.verifyIdentity.rawValue
138+
139+
#expect(disabled == "Disabled")
140+
let sslEnabled = disabled != "Disabled" && !disabled.isEmpty
141+
#expect(!sslEnabled)
142+
143+
let required = SSLMode.required.rawValue
144+
let sslEnabledRequired = required != "Disabled" && !required.isEmpty
145+
#expect(sslEnabledRequired)
146+
147+
let verifiesCert = verifyCa == "Verify CA" || verifyIdentity == "Verify Identity"
148+
#expect(verifiesCert)
149+
}
150+
151+
@Test("ClickHouse SSL mode string comparisons use correct case")
152+
func clickHouseSSLModeStrings() {
153+
// These mirror the comparisons in ClickHousePlugin.connect() / buildRequest()
154+
let disabled = SSLMode.disabled.rawValue
155+
let useTLS = disabled != "Disabled"
156+
#expect(!useTLS)
157+
158+
let required = SSLMode.required.rawValue
159+
let skipVerification = required == "Required"
160+
#expect(skipVerification)
161+
}
162+
}

0 commit comments

Comments
 (0)