fix: crash merged overviews when two accounts share a sourceKey (#39)

Adding the same server and username twice gives two connections one `sourceKey`, and `rehydrate`'s `Dictionary(uniqueKeysWithValues:)` traps on it, crashing every merged overview on construction; this keeps the first source and adds a regression test (185 ForjiTests green).

I grant Stefan Hausotte an irrevocable, worldwide, royalty-free license to use, sublicense, and distribute my contribution, including through Apple's App Store under the project's App Store exception.

Reviewed-on: https://codeberg.org/secana/Forji/pulls/39
This commit is contained in:
systemBlue 2026-06-02 18:05:53 +02:00 committed by secana
parent c6dda2cd70
commit 89a0cd0bb2
2 changed files with 33 additions and 1 deletions

View file

@ -73,8 +73,11 @@ extension PaginationState where Item: Identifiable {
/// Replace placeholder auth services using the given connection sources.
/// Items whose sourceKey has no matching source are removed.
func rehydrate<T>(from sources: [ConnectionSource]) where Item == TaggedItem<T> {
// Two connected accounts with the same server+username share a sourceKey,
// so keep the first rather than trapping on a duplicate key.
let lookup = Dictionary(
uniqueKeysWithValues: sources.map { ($0.sourceKey, $0.authService) },
sources.map { ($0.sourceKey, $0.authService) },
uniquingKeysWith: { first, _ in first },
)
items = items.compactMap { item in
guard let auth = lookup[item.sourceKey] else { return nil }

View file

@ -190,4 +190,33 @@ struct TaggedItemTests {
#expect(pagination.items.isEmpty)
}
@MainActor
@Test func rehydrateHandlesDuplicateSourceKeys() {
// Two connected accounts with the same server+username produce duplicate
// sourceKeys; rehydrate must not trap on the duplicate key.
let liveAuth = AuthenticationService()
let key = "https://a.com:u"
let sources = [
ConnectionSource(
sourceKey: key, name: "A",
client: ForgejoClient(serverURL: "https://a.com", username: "u", token: "t"),
authService: liveAuth,
),
ConnectionSource(
sourceKey: key, name: "A (duplicate)",
client: ForgejoClient(serverURL: "https://a.com", username: "u", token: "t"),
authService: AuthenticationService(),
),
]
let pagination = PaginationState<TaggedItem<FIssue>>()
pagination.items = [
TaggedItem(item: makeIssue(id: 1), sourceKey: key, instanceName: "A", authService: makeAuth()),
]
pagination.rehydrate(from: sources)
#expect(pagination.items.count == 1)
#expect(pagination.items[0].authService === liveAuth) // first source wins
}
}