import XCTest @testable import LChatCore // 🤖 The DEK-sync slice's pure halves (issue #1089): the release matching rule // (a fresh SiwA assertion's account must carry the very anchor the item was // written under) and the synced item's wire shape. The keychain and network // edges are deliberately thin and exercised by the device matrix, not here. final class DEKSyncTests: XCTestCase { private let item = DEKSync.Item(v: 1, dekB64: "AAAA", siwaAnchor: "hash-1", credentialID: "cred-1") private func anchor(_ kind: String, _ id: String) -> HomeClient.AnchorEntry { HomeClient.AnchorEntry(kind: kind, id: id) } // ---- the release matching rule ------------------------------------------ func testReleasableWhenAccountCarriesTheAnchor() { XCTAssertTrue(DEKSync.releasable(item, anchors: [ anchor("passkey", "pk-1"), anchor("siwa", "hash-1"), ])) } func testReleasableAcceptsAnyOfSeveralSiwaAnchors() { // A combined account may hold two Apple anchors; the item matches the // one it was written under regardless of which Apple ID asserted. XCTAssertTrue(DEKSync.releasable(item, anchors: [ anchor("siwa", "hash-other"), anchor("siwa", "hash-1"), ])) } func testNotReleasableForADifferentAccount() { // The family-iPad shape: the assertion resolved someone else's (or a // brand-new) account — its anchors don't include the item's. XCTAssertFalse(DEKSync.releasable(item, anchors: [ anchor("siwa", "hash-other"), ])) } func testNotReleasableWhenTheAnchorWasRemoved() { XCTAssertFalse(DEKSync.releasable(item, anchors: [ anchor("passkey", "pk-1"), anchor("email", "hash-1"), ])) } func testNotReleasableAgainstAnEmptyAccount() { XCTAssertFalse(DEKSync.releasable(item, anchors: [])) } func testMatchRequiresTheSiwaKind() { // Same id under a different kind must not satisfy the gate — the rule // is "the SiwA anchor", not "any anchor with that hash". XCTAssertFalse(DEKSync.releasable(item, anchors: [ anchor("email", "hash-1"), anchor("google", "hash-1"), ])) } // ---- the item's wire shape ---------------------------------------------- func testItemRoundTrips() throws { let data = try JSONEncoder().encode(item) XCTAssertEqual(try JSONDecoder().decode(DEKSync.Item.self, from: data), item) } func testItemToleratesUnknownFieldsFromNewerBuilds() throws { // A newer build may extend the synced item; older builds must still // read the fields they know (the vault blob's same forward rule). let json = #"{"v":2,"dekB64":"BBBB","siwaAnchor":"h","credentialID":"c","future":true}"# let decoded = try JSONDecoder().decode(DEKSync.Item.self, from: Data(json.utf8)) XCTAssertEqual(decoded.dekB64, "BBBB") XCTAssertEqual(decoded.siwaAnchor, "h") } // ---- the anchors listing decode ----------------------------------------- func testAnchorEntryDecodesTheServerShape() throws { // home/anchors.go rows: {kind, id, created_at} — created_at ignored. let json = #"{"anchors":[{"kind":"siwa","id":"hash-1","created_at":1752882000}]}"# struct R: Decodable { let anchors: [HomeClient.AnchorEntry] } let r = try JSONDecoder().decode(R.self, from: Data(json.utf8)) XCTAssertEqual(r.anchors.first?.kind, "siwa") XCTAssertEqual(r.anchors.first?.id, "hash-1") } }