import SwiftData import XCTest @testable import ZitareNative @MainActor final class SnapshotSyncTests: XCTestCase { /// Erster Pull: 2 Quotes, beide werden persistiert. func test_persistsInitialPayload() async throws { let container = try SnapshotContainer.make(inMemory: true) let sync = SnapshotSync.forTesting(container: container) { Self.firstRun } try await sync.refresh() let context = ModelContext(container) let quotes = try context.fetch(FetchDescriptor()) XCTAssertEqual(quotes.count, 2) let bySlug = Dictionary(uniqueKeysWithValues: quotes.map { ($0.slug, $0) }) XCTAssertEqual(bySlug["spitteler-x"]?.authorName, "Carl Spitteler") XCTAssertEqual(bySlug["keller-x"]?.regions, ["zuerich", "schweiz"]) let metas = try context.fetch(FetchDescriptor()) XCTAssertEqual(metas.count, 1) XCTAssertEqual(metas.first?.totalCount, 2) } /// Zweiter Pull (gleicher Container): keller-x zurückgezogen, /// spitteler-x text geändert, neuere-x dazu → Update + Delete + /// Insert wie erwartet. func test_reconcilesOnSecondPull() async throws { let container = try SnapshotContainer.make(inMemory: true) let sync1 = SnapshotSync.forTesting(container: container) { Self.firstRun } try await sync1.refresh() let sync2 = SnapshotSync.forTesting(container: container) { Self.secondRun } try await sync2.refresh() let context = ModelContext(container) let after = try context.fetch(FetchDescriptor()) let afterBySlug = Dictionary(uniqueKeysWithValues: after.map { ($0.slug, $0) }) XCTAssertEqual(after.count, 2) XCTAssertEqual(afterBySlug["spitteler-x"]?.text, "A-updated") XCTAssertNotNil(afterBySlug["neuere-x"]) XCTAssertNil(afterBySlug["keller-x"]) } /// `refreshIfStale` macht kein Refresh, wenn lastSyncedAt frisch. func test_freshSnapshotSkipsRefresh() async throws { let container = try SnapshotContainer.make(inMemory: true) let sync = SnapshotSync.forTesting( container: container, staleAfter: 3600 ) { Self.smallPayload } try await sync.refresh() // Loader, der explosiv knallt — refreshIfStale darf ihn nicht // aufrufen, weil noch frisch. let brokenSync = SnapshotSync( container: container, loader: { throw SnapshotSyncError.badResponse(-999) }, staleAfter: 3600 ) try await brokenSync.refreshIfStale() // soll *nicht* werfen } // MARK: - Fixtures nonisolated(unsafe) static let firstRun: Data = .init(#""" { "generatedAt": "2026-05-08T20:48:48.795Z", "count": 2, "quotes": [ { "slug": "spitteler-x", "text": "A", "authorSlug": "spitteler", "authorName": "Carl Spitteler", "language": "de", "themeSlugs": ["lebenskunst"], "regionSlugs": ["schweiz"] }, { "slug": "keller-x", "text": "B", "authorSlug": "keller", "authorName": "Gottfried Keller", "language": "de", "themeSlugs": [], "regionSlugs": ["zuerich","schweiz"] } ] } """#.utf8) nonisolated(unsafe) static let secondRun: Data = .init(#""" { "generatedAt": "2026-05-09T00:00:00.000Z", "count": 2, "quotes": [ { "slug": "spitteler-x", "text": "A-updated", "authorSlug": "spitteler", "authorName": "Carl Spitteler", "language": "de", "themeSlugs": ["lebenskunst"], "regionSlugs": ["schweiz"] }, { "slug": "neuere-x", "text": "C", "authorSlug": "neuere", "authorName": "Anon", "language": "de", "themeSlugs": ["philosophie"], "regionSlugs": [] } ] } """#.utf8) nonisolated(unsafe) static let smallPayload: Data = .init(#""" {"generatedAt":"2026-05-01T00:00:00.000Z","count":1, "quotes":[{"slug":"a","text":"A","authorSlug":null, "authorName":null,"language":"de","themeSlugs":[],"regionSlugs":[]}]} """#.utf8) }