NicknameDirectoryTests.swift (6146B)
1 import CoreData 2 import Foundation 3 import Testing 4 5 @testable import Crossmate 6 7 @Suite("Nickname directory") 8 struct NicknameDirectoryTests { 9 10 /// Runs `body` with the directory backed by a per-test temporary file so 11 /// parallel tests never touch the shared App Group container. 12 private func withTemporaryDirectoryFile( 13 _ body: @Sendable () async throws -> Void 14 ) async throws { 15 let url = FileManager.default.temporaryDirectory 16 .appendingPathComponent("nickname-directory-\(UUID().uuidString).json") 17 defer { try? FileManager.default.removeItem(at: url) } 18 try await NicknameDirectory.$testingFileURL.withValue(url) { 19 try await body() 20 } 21 } 22 23 // MARK: - File round trip 24 25 @Test("save/load/entry round-trips through the backing file") 26 func fileRoundTrip() async throws { 27 try await withTemporaryDirectoryFile { 28 #expect(NicknameDirectory.load().isEmpty) 29 let directory = [ 30 "_alice": NicknameDirectory.Entry(nickname: "Mum"), 31 "_bob": NicknameDirectory.Entry(nickname: "Bobby") 32 ] 33 NicknameDirectory.save(directory) 34 #expect(NicknameDirectory.load() == directory) 35 #expect(NicknameDirectory.entry(for: "_alice")?.nickname == "Mum") 36 #expect(NicknameDirectory.entry(for: "_carol") == nil) 37 #expect(NicknameDirectory.entry(for: "") == nil) 38 } 39 } 40 41 @Test("saving an empty directory removes the file") 42 func emptySaveRemovesFile() async throws { 43 try await withTemporaryDirectoryFile { 44 NicknameDirectory.save( 45 ["_alice": NicknameDirectory.Entry(nickname: "Mum")] 46 ) 47 #expect(!NicknameDirectory.load().isEmpty) 48 NicknameDirectory.save([:]) 49 #expect(NicknameDirectory.load().isEmpty) 50 } 51 } 52 53 // MARK: - Rebuild from Core Data 54 55 @Test("rebuildNicknameDirectory mirrors nicknamed friends and skips the rest") 56 func rebuildFromCoreData() async throws { 57 try await withTemporaryDirectoryFile { 58 try await MainActor.run { 59 let persistence = makeTestPersistence() 60 let ctx = persistence.viewContext 61 62 func addFriend( 63 authorID: String, 64 displayName: String?, 65 nickname: String?, 66 isBlocked: Bool = false 67 ) { 68 let friend = FriendEntity(context: ctx) 69 friend.authorID = authorID 70 friend.pairKey = "pair-\(authorID)" 71 friend.createdAt = Date() 72 friend.displayName = displayName 73 friend.nickname = nickname 74 friend.isBlocked = isBlocked 75 } 76 addFriend(authorID: "_alice", displayName: "Alice", nickname: "Mum") 77 addFriend(authorID: "_bob", displayName: nil, nickname: "Bobby") 78 addFriend(authorID: "_carol", displayName: "Carol", nickname: nil) 79 addFriend(authorID: "_dave", displayName: "Dave", nickname: "X", isBlocked: true) 80 try ctx.save() 81 82 FriendEntity.rebuildNicknameDirectory(in: ctx) 83 84 let directory = NicknameDirectory.load() 85 #expect(directory == [ 86 "_alice": NicknameDirectory.Entry(nickname: "Mum"), 87 "_bob": NicknameDirectory.Entry(nickname: "Bobby") 88 ]) 89 90 // Clearing the last nickname empties the directory again. 91 let req = NSFetchRequest<FriendEntity>(entityName: "FriendEntity") 92 for friend in (try? ctx.fetch(req)) ?? [] { 93 friend.nickname = nil 94 } 95 try ctx.save() 96 FriendEntity.rebuildNicknameDirectory(in: ctx) 97 #expect(NicknameDirectory.load().isEmpty) 98 } 99 } 100 } 101 102 // MARK: - Invite rows 103 104 @Test("resolvedInviterName prefers the nickname, then the shipped name") 105 @MainActor func resolvedInviterName() throws { 106 let persistence = makeTestPersistence() 107 let ctx = persistence.viewContext 108 109 func addInvite(inviterAuthorID: String, inviterName: String?) -> InviteEntity { 110 let invite = InviteEntity(context: ctx) 111 invite.gameID = UUID() 112 invite.gameTitle = "Saturday Puzzle" 113 invite.inviterAuthorID = inviterAuthorID 114 invite.inviterName = inviterName 115 invite.pingRecordName = "ping-\(UUID().uuidString)" 116 invite.shareURL = "https://example.com/share" 117 invite.createdAt = Date() 118 return invite 119 } 120 121 let friend = FriendEntity(context: ctx) 122 friend.authorID = "_alice" 123 friend.pairKey = "pair-alice" 124 friend.createdAt = Date() 125 friend.nickname = "Mum" 126 127 let nicknamed = addInvite(inviterAuthorID: "_alice", inviterName: "Alice") 128 let plain = addInvite(inviterAuthorID: "_bob", inviterName: "Bob") 129 let unnamed = addInvite(inviterAuthorID: "_carol", inviterName: "") 130 try ctx.save() 131 132 #expect(nicknamed.resolvedInviterName == "Mum") 133 #expect(plain.resolvedInviterName == "Bob") 134 #expect(unnamed.resolvedInviterName == nil) 135 } 136 137 // MARK: - Invite banner substitution 138 139 @Test("Invite body prefers the nickname over the inviter's own name") 140 func inviteBodyNickname() { 141 let ping = Ping( 142 recordName: "ping-test-nickname", 143 gameID: UUID(), 144 authorID: "_alice", 145 deviceID: "device-a", 146 playerName: "Alice", 147 puzzleTitle: "Saturday Puzzle", 148 kind: .invite, 149 payload: nil, 150 addressee: "_bob" 151 ) 152 #expect(InviteCoordinator.bodyText(for: ping, nickname: "Mum") 153 == "Mum invited you to the puzzle 'Saturday Puzzle'") 154 #expect(InviteCoordinator.bodyText(for: ping) 155 == "Alice invited you to the puzzle 'Saturday Puzzle'") 156 } 157 }