import ActivityKit import XCTest @testable import Rosetta @MainActor final class CallLiveActivityTests: XCTestCase { private let ownKey = "02-own-la-test" private let peerKey = "02-peer-la-test" override func setUp() { super.setUp() CallManager.shared.resetForSessionEnd() CallManager.shared.bindAccount(publicKey: ownKey) } override func tearDown() { CallManager.shared.resetForSessionEnd() super.tearDown() } // MARK: - CallActivityAttributes func testCallActivityAttributesCodableRoundTrip() throws { let attrs = CallActivityAttributes( peerName: "Test User", peerPublicKey: "02abcdef1234567890", colorIndex: 5 ) let encoder = JSONEncoder() let data = try encoder.encode(attrs) let decoder = JSONDecoder() let decoded = try decoder.decode(CallActivityAttributes.self, from: data) XCTAssertEqual(decoded.peerName, "Test User") XCTAssertEqual(decoded.peerPublicKey, "02abcdef1234567890") XCTAssertEqual(decoded.colorIndex, 5) } func testCallActivityContentStateCodableRoundTrip() throws { let state = CallActivityAttributes.ContentState( durationSec: 125, isActive: true, isMuted: false ) let data = try JSONEncoder().encode(state) let decoded = try JSONDecoder().decode(CallActivityAttributes.ContentState.self, from: data) XCTAssertEqual(decoded.durationSec, 125) XCTAssertTrue(decoded.isActive) XCTAssertFalse(decoded.isMuted) } func testAttributesSizeUnderActivityKitLimit() throws { // ActivityKit limit is ~4KB for attributes let attrs = CallActivityAttributes( peerName: "Very Long Name That Could Be A Display Name In Any Language", peerPublicKey: "02abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", colorIndex: 10 ) let data = try JSONEncoder().encode(attrs) XCTAssertLessThan(data.count, 4096, "Attributes must be under 4KB ActivityKit limit") } // MARK: - Color Index Parity func testColorIndexMatchesRosettaColors() { let names = ["Alice", "Bob", "Enfants Riches", "Baragoz", "0333331", ""] let keys = ["02abc", "02def", "02123", "02456", "02789", "02000"] for (name, key) in zip(names, keys) { let expected = RosettaColors.avatarColorIndex(for: name, publicKey: key) XCTAssertGreaterThanOrEqual(expected, 0) XCTAssertLessThan(expected, RosettaColors.avatarColors.count) } } func testColorIndexDeterministic() { let idx1 = RosettaColors.avatarColorIndex(for: "Test", publicKey: "02abc") let idx2 = RosettaColors.avatarColorIndex(for: "Test", publicKey: "02abc") XCTAssertEqual(idx1, idx2, "Color index must be deterministic") } func testColorIndexDiffersForDifferentNames() { let idx1 = RosettaColors.avatarColorIndex(for: "Alice", publicKey: "02abc") let idx2 = RosettaColors.avatarColorIndex(for: "Bob", publicKey: "02abc") // Not guaranteed to differ for all inputs, but these specific ones should // Just verify they're valid XCTAssertGreaterThanOrEqual(idx1, 0) XCTAssertGreaterThanOrEqual(idx2, 0) } func testColorIndexFallsBackToPublicKey() { let idxEmpty = RosettaColors.avatarColorIndex(for: "", publicKey: "02abcdef") let idxSpaces = RosettaColors.avatarColorIndex(for: " ", publicKey: "02abcdef") XCTAssertEqual(idxEmpty, idxSpaces, "Empty name and spaces-only should both fall back to publicKey") } // MARK: - Initials Parity func testInitialsTwoWords() { let result = RosettaColors.initials(name: "Enfants Riches", publicKey: "02abc") XCTAssertEqual(result, "ER") } func testInitialsSingleWord() { let result = RosettaColors.initials(name: "Baragoz", publicKey: "02abc") XCTAssertEqual(result, "BA") } func testInitialsEmptyName() { let result = RosettaColors.initials(name: "", publicKey: "02abcdef") XCTAssertEqual(result, "02") } func testInitialsThreeWords() { let result = RosettaColors.initials(name: "John Paul Smith", publicKey: "02abc") XCTAssertEqual(result, "JP") } // MARK: - Live Activity Lifecycle func testLiveActivityCreatedOnOutgoingCall() { let result = CallManager.shared.startOutgoingCall( toPublicKey: peerKey, title: "Peer", username: "peer" ) XCTAssertEqual(result, .started) // Live Activity may or may not start depending on simulator capabilities // but the code path should not crash } func testLiveActivityCreatedOnIncomingCall() { let packet = PacketSignalPeer( src: peerKey, dst: ownKey, sharedPublic: "", signalType: .call, roomId: "" ) CallManager.shared.testHandleSignalPacket(packet) XCTAssertEqual(CallManager.shared.uiState.phase, .incoming) // Live Activity start called without crash } func testLiveActivityEndedOnFinishCall() { _ = CallManager.shared.startOutgoingCall( toPublicKey: peerKey, title: "Peer", username: "peer" ) CallManager.shared.endCall() XCTAssertNil(CallManager.shared.liveActivity, "Live Activity should be nil after endCall") } // MARK: - App Group Avatar func testAppGroupAvatarFileCreatedAndCleaned() { guard let containerURL = FileManager.default.containerURL( forSecurityApplicationGroupIdentifier: "group.com.rosetta.dev" ) else { // App Group not available in test environment — skip return } let avatarURL = containerURL.appendingPathComponent("call_avatar.jpg") // Clean up try? FileManager.default.removeItem(at: avatarURL) XCTAssertFalse(FileManager.default.fileExists(atPath: avatarURL.path)) // Write test data let testData = Data([0xFF, 0xD8, 0xFF, 0xE0]) // JPEG header try? testData.write(to: avatarURL) XCTAssertTrue(FileManager.default.fileExists(atPath: avatarURL.path)) // Clean up try? FileManager.default.removeItem(at: avatarURL) XCTAssertFalse(FileManager.default.fileExists(atPath: avatarURL.path)) } // MARK: - Sound Manager func testCallSoundManagerSingleton() { let a = CallSoundManager.shared let b = CallSoundManager.shared XCTAssertTrue(a === b, "CallSoundManager must be singleton") } }