crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

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 }