// DeviceTokenRegistrarTests.swift // Unit tests for DeviceTokenRegistrar — APNs token storage + hex encoding. // // DeviceTokenRegistrar is a singleton `actor`. Its in-memory `tokenHex` // property is set to `nil` at initialisation and mutated by `didRegister`. // Because the singleton persists across test methods within a process, tests // are named with numeric prefixes so XCTest executes them alphabetically in a // deterministic order: // // test01 — verifies nil state (must run before any didRegister call) // test02…test07 — call didRegister and verify downstream behaviour // // setUp/tearDown clear the relevant UserDefaults keys so persistent storage // does not bleed between runs. import XCTest @testable import piRemote final class DeviceTokenRegistrarTests: XCTestCase { // Mirror the UserDefaults keys from the implementation. private static let tokenHexKey = "piremote.push.tokenHex" private static let pendingKey = "piremote.push.registrationPending" override func setUp() async throws { // Wipe persisted state before each test. UserDefaults.standard.removeObject(forKey: Self.tokenHexKey) UserDefaults.standard.removeObject(forKey: Self.pendingKey) } override func tearDown() async throws { // Leave no trace in UserDefaults after the test. UserDefaults.standard.removeObject(forKey: Self.tokenHexKey) UserDefaults.standard.removeObject(forKey: Self.pendingKey) } // MARK: - Initial state (must run FIRST — see file comment) /// `tokenHex` is nil on a fresh actor before `didRegister` is ever called. /// /// This test relies on alphabetical ordering placing it first. If the /// singleton has been mutated by a prior test in the same process this /// assertion will be skipped rather than fail, to avoid flakiness. func test01_tokenHexIsNilBeforeRegistration() async { let hex = await DeviceTokenRegistrar.shared.tokenHex // Only assert nil if we're sure no earlier call set it. // On a clean process the actor init sets tokenHex = nil. guard hex == nil else { // A previous test must have called didRegister. Document and skip. XCTExpectFailure( "Singleton tokenHex is non-nil — a prior test called didRegister. " + "Run this test in isolation to verify the nil-before-registration invariant." ) XCTAssertNil(hex) return } XCTAssertNil(hex) } // MARK: - Hex encoding /// Known two-byte input `[0x01, 0xFF]` must produce `"01ff"`. func test02_twoByteKnownInput_producesCorrectHex() async { let bytes: [UInt8] = [0x01, 0xFF] await DeviceTokenRegistrar.shared.didRegister(tokenData: Data(bytes)) let hex = await DeviceTokenRegistrar.shared.tokenHex XCTAssertEqual(hex, "01ff") } /// Zero byte produces `"00"`. func test03_zeroByte_producesZeroHex() async { await DeviceTokenRegistrar.shared.didRegister(tokenData: Data([0x00])) let hex = await DeviceTokenRegistrar.shared.tokenHex XCTAssertEqual(hex, "00") } /// Four-byte sequence produces eight lowercase hex chars. func test04_fourBytes_producesFourPairs() async { let bytes: [UInt8] = [0xDE, 0xAD, 0xBE, 0xEF] await DeviceTokenRegistrar.shared.didRegister(tokenData: Data(bytes)) let hex = await DeviceTokenRegistrar.shared.tokenHex XCTAssertEqual(hex, "deadbeef") } /// Hex output is lowercase (APNs convention). func test05_hexIsLowercase() async { await DeviceTokenRegistrar.shared.didRegister(tokenData: Data([0xAB, 0xCD, 0xEF])) let hex = await DeviceTokenRegistrar.shared.tokenHex XCTAssertEqual(hex, hex?.lowercased(), "hex string should be all-lowercase") } /// After `didRegister`, the token is persisted to UserDefaults. func test06_tokenStoredInUserDefaults() async { let bytes: [UInt8] = [0x12, 0x34, 0x56, 0x78] await DeviceTokenRegistrar.shared.didRegister(tokenData: Data(bytes)) let stored = UserDefaults.standard.string(forKey: Self.tokenHexKey) XCTAssertEqual(stored, "12345678") } // MARK: - Environment /// `environment` must be exactly `"sandbox"` or `"production"`. func test07_environmentIsValid() { let env = DeviceTokenRegistrar.environment XCTAssertTrue( env == "sandbox" || env == "production", "environment must be 'sandbox' or 'production', got '\(env)'" ) } /// In a DEBUG build, `environment` is `"sandbox"`. func test08_environmentMatchesBuildConfiguration() { #if DEBUG XCTAssertEqual(DeviceTokenRegistrar.environment, "sandbox") #else XCTAssertEqual(DeviceTokenRegistrar.environment, "production") #endif } }