EnsureGameEntityTests.swift (4098B)
1 import CloudKit 2 import CoreData 3 import Foundation 4 import Testing 5 6 @testable import Crossmate 7 8 /// Pins down the lazy-parent-creation invariant that prevents Move / Snapshot / 9 /// Player records from being silently dropped when they arrive in a different 10 /// CKSyncEngine fetch batch than the Game record that created the zone. 11 @Suite("EnsureGameEntity") 12 @MainActor 13 struct EnsureGameEntityTests { 14 15 @Test("creates a hidden stub when no GameEntity exists") 16 func createsHiddenStub() throws { 17 let persistence = makeTestPersistence() 18 let ctx = persistence.viewContext 19 let gameID = UUID() 20 let zoneID = RecordSerializer.zoneID(for: gameID) 21 22 let entity = RecordSerializer.ensureGameEntity( 23 forGameID: gameID, 24 zoneID: zoneID, 25 in: ctx 26 ) 27 28 #expect(entity.id == gameID) 29 #expect(entity.ckRecordName == "game-\(gameID.uuidString)") 30 #expect(entity.ckZoneName == zoneID.zoneName) 31 #expect(entity.databaseScope == 0) 32 #expect(entity.ckZoneOwnerName == nil) 33 #expect(entity.title == "") 34 // Empty puzzleSource is what GameSummary.init? uses to filter the row 35 // out of the library until the real Game record arrives. 36 #expect(entity.puzzleSource == "") 37 } 38 39 @Test("returns existing entity, no duplicate") 40 func returnsExisting() throws { 41 let persistence = makeTestPersistence() 42 let ctx = persistence.viewContext 43 let gameID = UUID() 44 let zoneID = RecordSerializer.zoneID(for: gameID) 45 46 let first = RecordSerializer.ensureGameEntity( 47 forGameID: gameID, 48 zoneID: zoneID, 49 in: ctx 50 ) 51 first.title = "preserved" 52 let second = RecordSerializer.ensureGameEntity( 53 forGameID: gameID, 54 zoneID: zoneID, 55 in: ctx 56 ) 57 58 #expect(first.objectID == second.objectID) 59 #expect(second.title == "preserved") 60 61 let req = NSFetchRequest<GameEntity>(entityName: "GameEntity") 62 req.predicate = NSPredicate(format: "id == %@", gameID as CVarArg) 63 let all = try ctx.fetch(req) 64 #expect(all.count == 1) 65 } 66 67 @Test("derives shared scope from non-default zone owner") 68 func sharedScope() throws { 69 let persistence = makeTestPersistence() 70 let ctx = persistence.viewContext 71 let gameID = UUID() 72 let zoneID = RecordSerializer.zoneID(for: gameID, ownerName: "_someOwnerID") 73 74 let entity = RecordSerializer.ensureGameEntity( 75 forGameID: gameID, 76 zoneID: zoneID, 77 in: ctx 78 ) 79 80 #expect(entity.databaseScope == 1) 81 #expect(entity.ckZoneOwnerName == "_someOwnerID") 82 } 83 84 @Test("later Game record populates the same stub row") 85 func gameRecordPopulatesStub() throws { 86 let persistence = makeTestPersistence() 87 let ctx = persistence.viewContext 88 let gameID = UUID() 89 let zoneID = RecordSerializer.zoneID(for: gameID) 90 91 let stub = RecordSerializer.ensureGameEntity( 92 forGameID: gameID, 93 zoneID: zoneID, 94 in: ctx 95 ) 96 let stubObjectID = stub.objectID 97 98 // Simulate the Game record arriving in a later batch. applyGameRecord 99 // matches by ckRecordName via fetchOrCreate, so the stub row should be 100 // updated rather than a new row created. 101 let recordID = CKRecord.ID( 102 recordName: RecordSerializer.recordName(forGameID: gameID), 103 zoneID: zoneID 104 ) 105 let record = CKRecord(recordType: "Game", recordID: recordID) 106 record["title"] = "Real Title" as CKRecordValue 107 108 let updated = RecordSerializer.applyGameRecord(record, to: ctx) 109 110 #expect(updated.objectID == stubObjectID) 111 #expect(updated.title == "Real Title") 112 113 let req = NSFetchRequest<GameEntity>(entityName: "GameEntity") 114 req.predicate = NSPredicate(format: "id == %@", gameID as CVarArg) 115 let all = try ctx.fetch(req) 116 #expect(all.count == 1) 117 } 118 }