pi-remote-ios/Tests/CoreTests/DeviceTokenRegistrarTests.s...

120 lines
4.8 KiB
Swift

// 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)
// test02test07 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
}
}