crossmate

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

NicknameDirectoryTests.swift (6450B)


      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.friendZoneName = "friend-pair-\(authorID)"
     72                     friend.friendZoneOwnerName = "_owner"
     73                     friend.databaseScope = 0
     74                     friend.createdAt = Date()
     75                     friend.displayName = displayName
     76                     friend.nickname = nickname
     77                     friend.isBlocked = isBlocked
     78                 }
     79                 addFriend(authorID: "_alice", displayName: "Alice", nickname: "Mum")
     80                 addFriend(authorID: "_bob", displayName: nil, nickname: "Bobby")
     81                 addFriend(authorID: "_carol", displayName: "Carol", nickname: nil)
     82                 addFriend(authorID: "_dave", displayName: "Dave", nickname: "X", isBlocked: true)
     83                 try ctx.save()
     84 
     85                 FriendEntity.rebuildNicknameDirectory(in: ctx)
     86 
     87                 let directory = NicknameDirectory.load()
     88                 #expect(directory == [
     89                     "_alice": NicknameDirectory.Entry(nickname: "Mum"),
     90                     "_bob": NicknameDirectory.Entry(nickname: "Bobby")
     91                 ])
     92 
     93                 // Clearing the last nickname empties the directory again.
     94                 let req = NSFetchRequest<FriendEntity>(entityName: "FriendEntity")
     95                 for friend in (try? ctx.fetch(req)) ?? [] {
     96                     friend.nickname = nil
     97                 }
     98                 try ctx.save()
     99                 FriendEntity.rebuildNicknameDirectory(in: ctx)
    100                 #expect(NicknameDirectory.load().isEmpty)
    101             }
    102         }
    103     }
    104 
    105     // MARK: - Invite rows
    106 
    107     @Test("resolvedInviterName prefers the nickname, then the shipped name")
    108     @MainActor func resolvedInviterName() throws {
    109         let persistence = makeTestPersistence()
    110         let ctx = persistence.viewContext
    111 
    112         func addInvite(inviterAuthorID: String, inviterName: String?) -> InviteEntity {
    113             let invite = InviteEntity(context: ctx)
    114             invite.gameID = UUID()
    115             invite.gameTitle = "Saturday Puzzle"
    116             invite.inviterAuthorID = inviterAuthorID
    117             invite.inviterName = inviterName
    118             invite.pingRecordName = "ping-\(UUID().uuidString)"
    119             invite.shareURL = "https://example.com/share"
    120             invite.createdAt = Date()
    121             return invite
    122         }
    123 
    124         let friend = FriendEntity(context: ctx)
    125         friend.authorID = "_alice"
    126         friend.pairKey = "pair-alice"
    127         friend.friendZoneName = "friend-pair-alice"
    128         friend.friendZoneOwnerName = "_owner"
    129         friend.databaseScope = 0
    130         friend.createdAt = Date()
    131         friend.nickname = "Mum"
    132 
    133         let nicknamed = addInvite(inviterAuthorID: "_alice", inviterName: "Alice")
    134         let plain = addInvite(inviterAuthorID: "_bob", inviterName: "Bob")
    135         let unnamed = addInvite(inviterAuthorID: "_carol", inviterName: "")
    136         try ctx.save()
    137 
    138         #expect(nicknamed.resolvedInviterName == "Mum")
    139         #expect(plain.resolvedInviterName == "Bob")
    140         #expect(unnamed.resolvedInviterName == nil)
    141     }
    142 
    143     // MARK: - Invite banner substitution
    144 
    145     @Test("Invite body prefers the nickname over the inviter's own name")
    146     func inviteBodyNickname() {
    147         let ping = Ping(
    148             recordName: "ping-test-nickname",
    149             gameID: UUID(),
    150             authorID: "_alice",
    151             deviceID: "device-a",
    152             playerName: "Alice",
    153             puzzleTitle: "Saturday Puzzle",
    154             kind: .invite,
    155             payload: nil,
    156             addressee: "_bob"
    157         )
    158         #expect(InviteCoordinator.bodyText(for: ping, nickname: "Mum")
    159             == "Mum invited you to the puzzle 'Saturday Puzzle'")
    160         #expect(InviteCoordinator.bodyText(for: ping)
    161             == "Alice invited you to the puzzle 'Saturday Puzzle'")
    162     }
    163 }