diff --git a/Forji/Forji/Services/MultiInstanceManager.swift b/Forji/Forji/Services/MultiInstanceManager.swift index 77e97aa..dc36468 100644 --- a/Forji/Forji/Services/MultiInstanceManager.swift +++ b/Forji/Forji/Services/MultiInstanceManager.swift @@ -52,7 +52,7 @@ final class MultiInstanceManager { return await group.reduce(into: [(Int, Bool, String?)]()) { $0.append($1) } } - let bootstrapped = Dictionary(uniqueKeysWithValues: connections.map { ($0.instance.serverURL, $0) }) + let bootstrapped = bootstrappedConnectionsByAccountKey() var newConnections: [(instance: ForgejoInstance, authService: AuthenticationService)] = [] var newFailed: [(instance: ForgejoInstance, error: String)] = [] @@ -60,7 +60,7 @@ final class MultiInstanceManager { let instance = instances[index] if success { newConnections.append((instance: instance, authService: authServices[index])) - } else if let existing = bootstrapped[instance.serverURL] { + } else if let existing = bootstrapped[accountKey(for: instance)] { newConnections.append(existing) } else { newFailed.append((instance: instance, error: error ?? "Unknown error")) @@ -115,6 +115,20 @@ final class MultiInstanceManager { func displayName(for instance: ForgejoInstance) -> String { instance.name.isEmpty ? instance.serverURL : instance.name } + + func accountKey(for instance: ForgejoInstance) -> String { + "\(ForgejoClient.normalizeServerURL(instance.serverURL)):\(instance.username)" + } + + func bootstrappedConnectionsByAccountKey() + -> [String: (instance: ForgejoInstance, authService: AuthenticationService)] + { + var bootstrapped: [String: (instance: ForgejoInstance, authService: AuthenticationService)] = [:] + for connection in connections { + bootstrapped[accountKey(for: connection.instance)] = connection + } + return bootstrapped + } } /// Sendable snapshot of ForgejoInstance data needed for session restore. diff --git a/Forji/ForjiTests/MultiInstanceManagerTests.swift b/Forji/ForjiTests/MultiInstanceManagerTests.swift index 8bd3959..37cf234 100644 --- a/Forji/ForjiTests/MultiInstanceManagerTests.swift +++ b/Forji/ForjiTests/MultiInstanceManagerTests.swift @@ -1,4 +1,5 @@ import ForgejoKit +import Foundation import Testing @testable import Forji @@ -68,4 +69,55 @@ struct MultiInstanceManagerTests { let sources = manager.connectionSources() #expect(sources.isEmpty) } + + @Test func bootstrappedConnectionMapKeepsAccountsWithSameServerURL() async throws { + let serverURL = "https://forgejo.example.com/" + let normalizedServerURL = ForgejoClient.normalizeServerURL(serverURL) + let suffix = UUID().uuidString + let firstUsername = "alice-\(suffix)" + let secondUsername = "bob-\(suffix)" + let first = ForgejoInstance( + serverURL: serverURL, + username: firstUsername, + useTokenAuth: true, + ) + let second = ForgejoInstance( + serverURL: serverURL, + username: secondUsername, + useTokenAuth: true, + ) + + try await KeychainManager.shared.saveToken( + "token-\(firstUsername)", + for: normalizedServerURL, + username: firstUsername, + ) + try await KeychainManager.shared.saveToken( + "token-\(secondUsername)", + for: normalizedServerURL, + username: secondUsername, + ) + + let manager = MultiInstanceManager() + manager.bootstrap(instances: [first, second]) + let bootstrapped = manager.bootstrappedConnectionsByAccountKey() + + #expect(manager.connections.count == 2) + #expect(bootstrapped.count == 2) + #expect( + bootstrapped[manager.accountKey(for: first)]?.instance.username == firstUsername + ) + #expect( + bootstrapped[manager.accountKey(for: second)]?.instance.username == secondUsername + ) + + try await KeychainManager.shared.deleteToken( + for: normalizedServerURL, + username: firstUsername, + ) + try await KeychainManager.shared.deleteToken( + for: normalizedServerURL, + username: secondUsername, + ) + } }