crossmate

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

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 }