GameStoreContributingDevicesTests.swift (3457B)
1 import CoreData 2 import Foundation 3 import Testing 4 5 @testable import Crossmate 6 7 /// `GameStore.contributingDevices` enumerates the `(author, device)` pairs that 8 /// wrote a `MovesEntity` for a game — the local, device-level signal replay uses 9 /// to decide whether the local journal alone covers the grid (no other device → 10 /// no CloudKit). It must distinguish this account's own second device, which the 11 /// author-keyed roster cannot. 12 @Suite("GameStore.contributingDevices", .isolatedNotificationState) 13 @MainActor 14 struct GameStoreContributingDevicesTests { 15 16 private static let puzzleSource = """ 17 Title: Test Puzzle 18 Author: Test 19 20 21 AB 22 CD 23 """ 24 25 private func makeGame(in ctx: NSManagedObjectContext) throws -> (GameEntity, UUID) { 26 let gameID = UUID() 27 let entity = GameEntity(context: ctx) 28 entity.id = gameID 29 entity.title = "Test" 30 entity.puzzleSource = Self.puzzleSource 31 entity.createdAt = Date() 32 entity.updatedAt = Date() 33 entity.ckRecordName = "game-\(gameID.uuidString)" 34 entity.ckZoneName = "game-\(gameID.uuidString)" 35 entity.databaseScope = 1 36 try ctx.save() 37 return (entity, gameID) 38 } 39 40 private func addMoves( 41 for entity: GameEntity, 42 gameID: UUID, 43 authorID: String, 44 deviceID: String, 45 in ctx: NSManagedObjectContext 46 ) throws { 47 let row = MovesEntity(context: ctx) 48 row.game = entity 49 row.authorID = authorID 50 row.deviceID = deviceID 51 row.cells = try MovesCodec.encode([:]) 52 row.updatedAt = Date() 53 row.ckRecordName = RecordSerializer.recordName( 54 forMovesInGame: gameID, authorID: authorID, deviceID: deviceID 55 ) 56 try ctx.save() 57 } 58 59 @Test("empty when nobody has written") 60 func emptyWhenNoMoves() throws { 61 let persistence = makeTestPersistence() 62 let store = makeTestStore(persistence: persistence) 63 let (_, gameID) = try makeGame(in: persistence.viewContext) 64 65 #expect(store.contributingDevices(for: gameID).isEmpty) 66 } 67 68 @Test("one author across two devices yields two device keys") 69 func sameAuthorTwoDevices() throws { 70 let persistence = makeTestPersistence() 71 let store = makeTestStore(persistence: persistence) 72 let (entity, gameID) = try makeGame(in: persistence.viewContext) 73 74 try addMoves(for: entity, gameID: gameID, authorID: "alice", deviceID: "iphone", in: persistence.viewContext) 75 try addMoves(for: entity, gameID: gameID, authorID: "alice", deviceID: "ipad", in: persistence.viewContext) 76 77 let devices = store.contributingDevices(for: gameID) 78 #expect(devices == [ 79 JournalDeviceKey(authorID: "alice", deviceID: "iphone"), 80 JournalDeviceKey(authorID: "alice", deviceID: "ipad"), 81 ]) 82 } 83 84 @Test("distinct authors are kept separate") 85 func distinctAuthors() throws { 86 let persistence = makeTestPersistence() 87 let store = makeTestStore(persistence: persistence) 88 let (entity, gameID) = try makeGame(in: persistence.viewContext) 89 90 try addMoves(for: entity, gameID: gameID, authorID: "alice", deviceID: "iphone", in: persistence.viewContext) 91 try addMoves(for: entity, gameID: gameID, authorID: "bob", deviceID: "iphone", in: persistence.viewContext) 92 93 #expect(store.contributingDevices(for: gameID).count == 2) 94 } 95 }