2026-09-21 13:52:52 +02:00
|
|
|
import Foundation
|
|
|
|
|
import Testing
|
|
|
|
|
|
|
|
|
|
@testable import Persistence
|
|
|
|
|
|
|
|
|
|
@Suite(
|
|
|
|
|
"CollectingRepository repository, collection and events",
|
|
|
|
|
.tags(.repository)
|
|
|
|
|
)
|
|
|
|
|
struct CommunityRepositoryTests {
|
|
|
|
|
|
|
|
|
|
// MARK: Collection tests
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
func `keeps a collection per account, within a series' run`() async throws {
|
|
|
|
|
try await withDatabase { fluent in
|
|
|
|
|
let collecting = CollectingRepository(fluent: fluent)
|
|
|
|
|
let accounts = AccountRepository(fluent: fluent)
|
|
|
|
|
let mara = try await accounts.account(email: "mara.v@example.com")
|
|
|
|
|
let sam = try await accounts.account(email: "sam@example.com")
|
|
|
|
|
|
|
|
|
|
// Echo Country runs six issues: #9 is outside it, and #2 is given twice.
|
|
|
|
|
try await collecting.own(issues: [1, 2, 2, 9, 0], ofSeries: "echo-country", by: mara)
|
|
|
|
|
try await collecting.own(issues: [2, 3], ofSeries: "echo-country", by: mara)
|
|
|
|
|
try await collecting.own(issues: [1], ofSeries: "no-such-series", by: mara)
|
|
|
|
|
try await collecting.disown(issue: 1, ofSeries: "echo-country", by: sam)
|
|
|
|
|
|
|
|
|
|
let owned = try await collecting.ownedIssues(of: mara)
|
|
|
|
|
let others = try await collecting.ownedIssues(of: sam)
|
|
|
|
|
|
|
|
|
|
try await collecting.disown(issue: 1, ofSeries: "echo-country", by: mara)
|
|
|
|
|
|
|
|
|
|
let after = try await collecting.ownedIssues(of: mara)
|
|
|
|
|
|
|
|
|
|
#expect(owned == ["echo-country": [1, 2, 3]])
|
|
|
|
|
#expect(others.isEmpty)
|
|
|
|
|
#expect(after == ["echo-country": [2, 3]])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Events tests
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
func `lists the events still to come, soonest first, with the shops trading there`() async throws {
|
|
|
|
|
try await withDatabase { fluent in
|
|
|
|
|
let clock = Clock()
|
|
|
|
|
let collecting = CollectingRepository(fluent: fluent, now: clock.now)
|
|
|
|
|
|
2026-10-25 12:47:23 +01:00
|
|
|
// The present, which every demo con is seeded after.
|
|
|
|
|
clock.advance(by: Date().timeIntervalSince(clock.now()))
|
2026-09-21 13:52:52 +02:00
|
|
|
|
|
|
|
|
let all = try await collecting.upcomingEvents()
|
|
|
|
|
|
2026-10-25 12:47:23 +01:00
|
|
|
// The first con's last day: still on. Two weeks later it is over, and so is Stripdagen.
|
|
|
|
|
clock.advance(by: try #require(all.first).endsOn.timeIntervalSince(clock.now()))
|
2026-09-21 13:52:52 +02:00
|
|
|
|
|
|
|
|
let lastDay = try await collecting.upcomingEvents()
|
|
|
|
|
|
|
|
|
|
clock.advance(by: 14 * 86_400)
|
|
|
|
|
|
|
|
|
|
let later = try await collecting.upcomingEvents()
|
|
|
|
|
|
|
|
|
|
#expect(all.map(\.slug) == ["dutch-comic-con", "stripdagen-haarlem", "comic-con-amsterdam", "comic-con-brussels"])
|
|
|
|
|
#expect(Set(all[0].shops.map(\.slug)) == ["kestrel-row-comics", "silverfish-comics", "third-panel-supply"])
|
|
|
|
|
#expect(lastDay.first?.slug == "dutch-comic-con")
|
|
|
|
|
#expect(later.first?.slug == "comic-con-amsterdam")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 12:47:23 +01:00
|
|
|
@Test
|
|
|
|
|
func `dates the demo cons on weekends, from the first one after seeding`() {
|
|
|
|
|
var calendar = Calendar(identifier: .gregorian)
|
|
|
|
|
|
|
|
|
|
calendar.timeZone = TimeZone(identifier: "Europe/Amsterdam") ?? .gmt
|
|
|
|
|
|
|
|
|
|
// Monday 21 September 2026: the dates the prototype first shipped with.
|
|
|
|
|
let days = Seed.events(after: Date(timeIntervalSince1970: 1_790_000_000)).map { entry in
|
|
|
|
|
[entry.event.startsOn, entry.event.endsOn]
|
|
|
|
|
.map { calendar.dateComponents([.month, .day, .hour], from: $0) }
|
|
|
|
|
.map { "\($0.month ?? 0)/\($0.day ?? 0) \($0.hour ?? 0):00" }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#expect(days == [
|
|
|
|
|
["9/26 12:00", "9/27 12:00"],
|
|
|
|
|
["10/3 12:00", "10/4 12:00"],
|
|
|
|
|
["11/21 12:00", "11/22 12:00"],
|
|
|
|
|
["12/5 12:00", "12/6 12:00"],
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 13:52:52 +02:00
|
|
|
@Test
|
|
|
|
|
func `lets a seller pick the events their shop trades at`() async throws {
|
|
|
|
|
try await withDatabase { fluent in
|
2026-10-25 12:47:23 +01:00
|
|
|
let collecting = CollectingRepository(fluent: fluent)
|
2026-09-21 13:52:52 +02:00
|
|
|
let accounts = AccountRepository(fluent: fluent)
|
|
|
|
|
let joost = try await accounts.account(email: "joost@kestrelrow.example")
|
|
|
|
|
let mara = try await accounts.account(email: "mara.v@example.com")
|
|
|
|
|
|
|
|
|
|
try await collecting.setEvents(["stripdagen-haarlem", "comic-con-amsterdam", "no-such-event"], forShopOwnedBy: joost)
|
|
|
|
|
// An account without a shop changes nothing.
|
|
|
|
|
try await collecting.setEvents(["dutch-comic-con"], forShopOwnedBy: mara)
|
|
|
|
|
|
|
|
|
|
let trading = try await collecting.upcomingEvents()
|
|
|
|
|
.filter { $0.shops.contains { $0.slug == "kestrel-row-comics" } }
|
|
|
|
|
.map(\.slug)
|
|
|
|
|
|
|
|
|
|
#expect(trading == ["stripdagen-haarlem", "comic-con-amsterdam"])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
func `narrows a search to the shops trading at an event`() {
|
|
|
|
|
let filter = ListingFilter(shops: ["silverfish-comics"])
|
|
|
|
|
|
|
|
|
|
#expect(Seed.listings.filter(filter.matches).map(\.shop.slug).allSatisfy { $0 == "silverfish-comics" })
|
|
|
|
|
#expect(Seed.listings.filter(filter.matches).count == 4)
|
|
|
|
|
#expect(Seed.listings.filter(ListingFilter(shops: []).matches).isEmpty)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Recovery tests
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
func `replaces every passkey of an account with the one it recovers with`() async throws {
|
|
|
|
|
try await withDatabase { fluent in
|
|
|
|
|
let accounts = AccountRepository(fluent: fluent)
|
|
|
|
|
let sam = try #require(try await accounts.join(email: "sam@example.com", credentialID: "old-1", publicKey: [1], signCount: 3))
|
|
|
|
|
|
|
|
|
|
try await accounts.addPasskey(to: sam, credentialID: "old-2", publicKey: [2], signCount: 0)
|
|
|
|
|
try await accounts.replacePasskeys(of: sam, credentialID: "new", publicKey: [9], signCount: 0)
|
|
|
|
|
|
|
|
|
|
let passkeys = try await accounts.passkeys(of: sam)
|
|
|
|
|
let lost = try await accounts.passkey(credentialID: "old-1")
|
|
|
|
|
|
|
|
|
|
#expect(passkeys.map(\.passkey.credentialID) == ["new"])
|
|
|
|
|
#expect(lost == nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|