CallKit/PushKit интеграция + фикс PacketPushNotification (tokenType, deviceId)

This commit is contained in:
2026-04-01 00:39:34 +05:00
parent 0470b306a9
commit 8f69781a66
16 changed files with 1058 additions and 63 deletions

View File

@@ -0,0 +1,252 @@
import Testing
@testable import Rosetta
// MARK: - Push Notification Extended Tests
struct PushNotificationExtendedTests {
@Test("Realistic FCM token with device ID round-trip")
func fcmTokenWithDeviceIdRoundTrip() throws {
// Real FCM tokens are ~163 chars
let fcmToken = "dQw4w9WgXcQ:APA91bHnzPc5Y0z4R8kP3mN6vX2tL7wJ1qA5sD8fG0hK3lZ9xC2vB4nM7oP1iU8yT6rE5wQ3jF4kL2mN0bV7cX9sD1aF3gH5jK7lP9oI2uY4tR6eW8qZ0xC"
var packet = PacketPushNotification()
packet.notificationsToken = fcmToken
packet.action = .subscribe
packet.tokenType = .fcm
packet.deviceId = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop"
let decoded = try decode(packet)
#expect(decoded.notificationsToken == fcmToken)
#expect(decoded.action == .subscribe)
#expect(decoded.tokenType == .fcm)
#expect(decoded.deviceId == "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop")
}
@Test("Realistic VoIP hex token round-trip")
func voipTokenWithDeviceIdRoundTrip() throws {
// PushKit tokens are 32 bytes = 64 hex chars
let voipToken = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
var packet = PacketPushNotification()
packet.notificationsToken = voipToken
packet.action = .subscribe
packet.tokenType = .voipApns
packet.deviceId = "device-xyz-123"
let decoded = try decode(packet)
#expect(decoded.notificationsToken == voipToken)
#expect(decoded.tokenType == .voipApns)
}
@Test("Long token (256 chars) round-trip — stress test UInt32 string length")
func longTokenRoundTrip() throws {
let longToken = String(repeating: "x", count: 256)
var packet = PacketPushNotification()
packet.notificationsToken = longToken
packet.action = .subscribe
packet.tokenType = .fcm
packet.deviceId = "dev"
let decoded = try decode(packet)
#expect(decoded.notificationsToken == longToken)
#expect(decoded.notificationsToken.count == 256)
}
@Test("Unicode device ID with emoji and Cyrillic round-trip")
func unicodeDeviceIdRoundTrip() throws {
let unicodeId = "Телефон Гайдара 📱"
var packet = PacketPushNotification()
packet.notificationsToken = "token"
packet.action = .subscribe
packet.tokenType = .fcm
packet.deviceId = unicodeId
let decoded = try decode(packet)
#expect(decoded.deviceId == unicodeId)
}
@Test("Unsubscribe action round-trip for both token types",
arguments: [PushTokenType.fcm, PushTokenType.voipApns])
func unsubscribeRoundTrip(tokenType: PushTokenType) throws {
var packet = PacketPushNotification()
packet.notificationsToken = "test-token"
packet.action = .unsubscribe
packet.tokenType = tokenType
packet.deviceId = "dev"
let decoded = try decode(packet)
#expect(decoded.action == .unsubscribe)
#expect(decoded.tokenType == tokenType)
}
private func decode(_ packet: PacketPushNotification) throws -> PacketPushNotification {
let data = PacketRegistry.encode(packet)
guard let result = PacketRegistry.decode(from: data),
let decoded = result.packet as? PacketPushNotification
else { throw TestError("Failed to decode PacketPushNotification") }
#expect(result.packetId == 0x10)
return decoded
}
}
// MARK: - Signal Peer Call Flow Tests
struct SignalPeerCallFlowTests {
@Test("Incoming call signal with realistic secp256k1 keys")
func incomingCallSignalRoundTrip() throws {
let caller = "02a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
let callee = "03f0e1d2c3b4a5968778695a4b3c2d1e0f9e8d7c6b5a49382716051a2b3c4d5e6f"
let packet = PacketSignalPeer(src: caller, dst: callee, sharedPublic: "",
signalType: .call, roomId: "")
let decoded = try decode(packet)
#expect(decoded.signalType == .call)
#expect(decoded.src == caller)
#expect(decoded.dst == callee)
#expect(decoded.sharedPublic == "")
#expect(decoded.roomId == "")
}
@Test("Key exchange with X25519 public key")
func keyExchangeRoundTrip() throws {
let x25519Key = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
let packet = PacketSignalPeer(src: "02src", dst: "02dst", sharedPublic: x25519Key,
signalType: .keyExchange, roomId: "")
let decoded = try decode(packet)
#expect(decoded.signalType == .keyExchange)
#expect(decoded.sharedPublic == x25519Key)
#expect(decoded.roomId == "")
}
@Test("Create room with UUID room ID")
func createRoomRoundTrip() throws {
let roomId = "550e8400-e29b-41d4-a716-446655440000"
let packet = PacketSignalPeer(src: "02src", dst: "02dst", sharedPublic: "",
signalType: .createRoom, roomId: roomId)
let decoded = try decode(packet)
#expect(decoded.signalType == .createRoom)
#expect(decoded.roomId == roomId)
#expect(decoded.sharedPublic == "")
}
@Test("endCallBecauseBusy short format — 3 bytes wire size, no src/dst")
func endCallBusyShortFormat() throws {
let packet = PacketSignalPeer(src: "ignored", dst: "ignored", sharedPublic: "ignored",
signalType: .endCallBecauseBusy, roomId: "ignored")
let data = PacketRegistry.encode(packet)
// Short form: 2 bytes packetId + 1 byte signalType = 3 bytes
#expect(data.count == 3)
let decoded = try decode(packet)
#expect(decoded.signalType == .endCallBecauseBusy)
#expect(decoded.src == "")
#expect(decoded.dst == "")
}
@Test("endCallBecausePeerDisconnected short format — 3 bytes wire size")
func endCallDisconnectedShortFormat() throws {
let packet = PacketSignalPeer(src: "ignored", dst: "ignored", sharedPublic: "ignored",
signalType: .endCallBecausePeerDisconnected, roomId: "ignored")
let data = PacketRegistry.encode(packet)
#expect(data.count == 3)
let decoded = try decode(packet)
#expect(decoded.signalType == .endCallBecausePeerDisconnected)
}
private func decode(_ packet: PacketSignalPeer) throws -> PacketSignalPeer {
let data = PacketRegistry.encode(packet)
guard let result = PacketRegistry.decode(from: data),
let decoded = result.packet as? PacketSignalPeer
else { throw TestError("Failed to decode PacketSignalPeer") }
#expect(result.packetId == 0x1A)
return decoded
}
}
// MARK: - Enum Parity Tests
struct CallPushEnumParityTests {
@Test("SignalType enum values match server",
arguments: [
(SignalType.call, 0), (SignalType.keyExchange, 1), (SignalType.activeCall, 2),
(SignalType.endCall, 3), (SignalType.createRoom, 4),
(SignalType.endCallBecausePeerDisconnected, 5), (SignalType.endCallBecauseBusy, 6)
])
func signalTypeEnumValues(pair: (SignalType, Int)) {
#expect(pair.0.rawValue == pair.1)
}
@Test("WebRTCSignalType enum values match server",
arguments: [(WebRTCSignalType.offer, 0), (WebRTCSignalType.answer, 1),
(WebRTCSignalType.iceCandidate, 2)])
func webRTCSignalTypeValues(pair: (WebRTCSignalType, Int)) {
#expect(pair.0.rawValue == pair.1)
}
@Test("PushTokenType enum values match server")
func pushTokenTypeValues() {
#expect(PushTokenType.fcm.rawValue == 0)
#expect(PushTokenType.voipApns.rawValue == 1)
}
}
// MARK: - Wire Format Byte-Level Tests
struct CallPushWireFormatTests {
@Test("PushNotification byte layout: token→action→tokenType→deviceId")
func pushNotificationByteLayout() {
var packet = PacketPushNotification()
packet.notificationsToken = "A"
packet.action = .unsubscribe
packet.tokenType = .fcm
packet.deviceId = "B"
let data = PacketRegistry.encode(packet)
#expect(data.count == 16)
// packetId = 0x0010
#expect(data[0] == 0x00); #expect(data[1] == 0x10)
// token "A": length=1, 'A'=0x0041
#expect(data[2] == 0x00); #expect(data[3] == 0x00)
#expect(data[4] == 0x00); #expect(data[5] == 0x01)
#expect(data[6] == 0x00); #expect(data[7] == 0x41)
// action = 1 (unsubscribe)
#expect(data[8] == 0x01)
// tokenType = 0 (fcm)
#expect(data[9] == 0x00)
// deviceId "B": length=1, 'B'=0x0042
#expect(data[10] == 0x00); #expect(data[11] == 0x00)
#expect(data[12] == 0x00); #expect(data[13] == 0x01)
#expect(data[14] == 0x00); #expect(data[15] == 0x42)
}
@Test("SignalPeer call byte layout: signalType→src→dst")
func signalPeerCallByteLayout() {
let packet = PacketSignalPeer(src: "S", dst: "D", sharedPublic: "",
signalType: .call, roomId: "")
let data = PacketRegistry.encode(packet)
#expect(data.count == 15)
// packetId = 0x001A
#expect(data[0] == 0x00); #expect(data[1] == 0x1A)
// signalType = 0 (call)
#expect(data[2] == 0x00)
// src "S": length=1, 'S'=0x0053
#expect(data[3] == 0x00); #expect(data[4] == 0x00)
#expect(data[5] == 0x00); #expect(data[6] == 0x01)
#expect(data[7] == 0x00); #expect(data[8] == 0x53)
// dst "D": length=1, 'D'=0x0044
#expect(data[9] == 0x00); #expect(data[10] == 0x00)
#expect(data[11] == 0x00); #expect(data[12] == 0x01)
#expect(data[13] == 0x00); #expect(data[14] == 0x44)
}
}
// MARK: - Helpers
private struct TestError: Error, CustomStringConvertible {
let description: String
init(_ message: String) { self.description = message }
}

View File

@@ -0,0 +1,167 @@
import Testing
@testable import Rosetta
/// Verifies PacketPushNotification wire format matches server
/// (im.rosetta.packet.Packet16PushNotification).
///
/// Server wire format:
/// writeInt16(packetId=0x10)
/// writeString(notificationToken)
/// writeInt8(action) 0=subscribe, 1=unsubscribe
/// writeInt8(tokenType) 0=FCM, 1=VoIPApns
/// writeString(deviceId)
struct PushNotificationPacketTests {
// MARK: - Enum Value Parity
@Test("PushNotificationAction.subscribe == 0 (server: SUBSCRIBE)")
func subscribeActionValue() {
#expect(PushNotificationAction.subscribe.rawValue == 0)
}
@Test("PushNotificationAction.unsubscribe == 1 (server: UNSUBSCRIBE)")
func unsubscribeActionValue() {
#expect(PushNotificationAction.unsubscribe.rawValue == 1)
}
@Test("PushTokenType.fcm == 0 (server: FCM)")
func fcmTokenTypeValue() {
#expect(PushTokenType.fcm.rawValue == 0)
}
@Test("PushTokenType.voipApns == 1 (server: VoIPApns)")
func voipTokenTypeValue() {
#expect(PushTokenType.voipApns.rawValue == 1)
}
// MARK: - Round Trip (encode decode)
@Test("FCM subscribe round-trip preserves all fields")
func fcmSubscribeRoundTrip() throws {
var packet = PacketPushNotification()
packet.notificationsToken = "test-fcm-token-abc123"
packet.action = .subscribe
packet.tokenType = .fcm
packet.deviceId = "device-id-xyz"
let decoded = try decodePushNotification(packet)
#expect(decoded.notificationsToken == "test-fcm-token-abc123")
#expect(decoded.action == .subscribe)
#expect(decoded.tokenType == .fcm)
#expect(decoded.deviceId == "device-id-xyz")
}
@Test("VoIP unsubscribe round-trip preserves all fields")
func voipUnsubscribeRoundTrip() throws {
var packet = PacketPushNotification()
packet.notificationsToken = "voip-hex-token-deadbeef"
packet.action = .unsubscribe
packet.tokenType = .voipApns
packet.deviceId = "another-device-id"
let decoded = try decodePushNotification(packet)
#expect(decoded.notificationsToken == "voip-hex-token-deadbeef")
#expect(decoded.action == .unsubscribe)
#expect(decoded.tokenType == .voipApns)
#expect(decoded.deviceId == "another-device-id")
}
@Test("Empty token and deviceId round-trip")
func emptyFieldsRoundTrip() throws {
var packet = PacketPushNotification()
packet.notificationsToken = ""
packet.action = .subscribe
packet.tokenType = .fcm
packet.deviceId = ""
let decoded = try decodePushNotification(packet)
#expect(decoded.notificationsToken == "")
#expect(decoded.deviceId == "")
}
// MARK: - Wire Format Byte Verification
@Test("Packet ID is 0x10 in encoded data")
func packetIdInEncodedData() {
#expect(PacketPushNotification.packetId == 0x10)
let packet = PacketPushNotification()
let data = PacketRegistry.encode(packet)
// First 2 bytes = packetId in big-endian: 0x00 0x10
#expect(data.count >= 2)
#expect(data[0] == 0x00)
#expect(data[1] == 0x10)
}
@Test("Wire format field order matches server: token → action → tokenType → deviceId")
func wireFormatFieldOrder() throws {
// Use known short values so we can verify byte positions.
var packet = PacketPushNotification()
packet.notificationsToken = "T" // 1 UTF-16 code unit
packet.action = .subscribe // 0
packet.tokenType = .voipApns // 1
packet.deviceId = "D" // 1 UTF-16 code unit
let data = PacketRegistry.encode(packet)
// Expected layout:
// [0-1] packetId = 0x0010 (2 bytes)
// [2-5] string length = 1 (UInt32 big-endian) for "T"
// [6-7] 'T' = 0x0054 (UInt16 big-endian)
// [8] action = 0 (subscribe)
// [9] tokenType = 1 (voipApns)
// [10-13] string length = 1 for "D"
// [14-15] 'D' = 0x0044 (UInt16 big-endian)
#expect(data.count == 16)
// packetId
#expect(data[0] == 0x00)
#expect(data[1] == 0x10)
// token string length = 1
#expect(data[2] == 0x00)
#expect(data[3] == 0x00)
#expect(data[4] == 0x00)
#expect(data[5] == 0x01)
// 'T' in UTF-16 BE
#expect(data[6] == 0x00)
#expect(data[7] == 0x54)
// action = 0 (subscribe)
#expect(data[8] == 0x00)
// tokenType = 1 (voipApns)
#expect(data[9] == 0x01)
// deviceId string length = 1
#expect(data[10] == 0x00)
#expect(data[11] == 0x00)
#expect(data[12] == 0x00)
#expect(data[13] == 0x01)
// 'D' in UTF-16 BE
#expect(data[14] == 0x00)
#expect(data[15] == 0x44)
}
// MARK: - Helper
private func decodePushNotification(
_ packet: PacketPushNotification
) throws -> PacketPushNotification {
let encoded = PacketRegistry.encode(packet)
guard let decoded = PacketRegistry.decode(from: encoded),
let decodedPacket = decoded.packet as? PacketPushNotification
else {
throw NSError(
domain: "PushNotificationPacketTests", code: 1,
userInfo: [NSLocalizedDescriptionKey: "Failed to decode PacketPushNotification"]
)
}
#expect(decoded.packetId == 0x10)
return decodedPacket
}
}