iOS звонки в foreground с full E2EE и паритетом call-attachment
This commit is contained in:
88
RosettaTests/CallAttachmentParityTests.swift
Normal file
88
RosettaTests/CallAttachmentParityTests.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
import XCTest
|
||||
@testable import Rosetta
|
||||
|
||||
@MainActor
|
||||
final class CallAttachmentParityTests: XCTestCase {
|
||||
private var ctx: DBTestContext!
|
||||
private var packetSenderMock: CallAttachmentPacketSenderMock!
|
||||
|
||||
private var ownPrivateKeyHex: String = ""
|
||||
private var ownPublicKey: String = ""
|
||||
private var peerPublicKey: String = ""
|
||||
|
||||
override func setUpWithError() throws {
|
||||
let ownPair = try Self.makeKeyPair()
|
||||
let peerPair = try Self.makeKeyPair()
|
||||
|
||||
ownPrivateKeyHex = ownPair.privateKeyHex
|
||||
ownPublicKey = ownPair.publicKeyHex
|
||||
peerPublicKey = peerPair.publicKeyHex
|
||||
|
||||
ctx = DBTestContext(account: ownPublicKey)
|
||||
packetSenderMock = CallAttachmentPacketSenderMock()
|
||||
|
||||
SessionManager.shared.testConfigureSessionForParityFlows(
|
||||
currentPublicKey: ownPublicKey,
|
||||
privateKeyHex: ownPrivateKeyHex
|
||||
)
|
||||
SessionManager.shared.packetFlowSender = packetSenderMock
|
||||
AttachmentCache.shared.privateKey = ownPrivateKeyHex
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
ctx?.teardown()
|
||||
ctx = nil
|
||||
packetSenderMock = nil
|
||||
AttachmentCache.shared.privateKey = nil
|
||||
SessionManager.shared.testResetParityFlowDependencies()
|
||||
}
|
||||
|
||||
func testCallDurationPreviewParserMatrix() {
|
||||
let tag = "aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb"
|
||||
|
||||
XCTAssertEqual(AttachmentPreviewCodec.parseCallDurationSeconds("65"), 65)
|
||||
XCTAssertEqual(AttachmentPreviewCodec.parseCallDurationSeconds("\(tag)::125"), 125)
|
||||
XCTAssertEqual(AttachmentPreviewCodec.parseCallDurationSeconds("durationSec=42"), 42)
|
||||
XCTAssertEqual(AttachmentPreviewCodec.parseCallDurationSeconds("duration 33"), 33)
|
||||
XCTAssertEqual(AttachmentPreviewCodec.parseCallDurationSeconds("invalid"), 0)
|
||||
}
|
||||
|
||||
func testSendCallAttachmentProducesType4WithDurationPreview() async throws {
|
||||
try await ctx.bootstrap()
|
||||
|
||||
try await SessionManager.shared.sendCallAttachment(
|
||||
toPublicKey: peerPublicKey,
|
||||
durationSec: 87,
|
||||
opponentTitle: "Peer",
|
||||
opponentUsername: "peer"
|
||||
)
|
||||
|
||||
XCTAssertEqual(packetSenderMock.sentMessages.count, 1)
|
||||
guard let packet = packetSenderMock.sentMessages.first else {
|
||||
XCTFail("Expected one outgoing call attachment packet")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(packet.attachments.count, 1)
|
||||
let attachment = packet.attachments[0]
|
||||
XCTAssertEqual(attachment.type, .call)
|
||||
XCTAssertEqual(attachment.preview, "87")
|
||||
XCTAssertEqual(attachment.blob, "")
|
||||
}
|
||||
|
||||
private static func makeKeyPair() throws -> (privateKeyHex: String, publicKeyHex: String) {
|
||||
let mnemonic = try CryptoManager.shared.generateMnemonic()
|
||||
let pair = try CryptoManager.shared.deriveKeyPair(from: mnemonic)
|
||||
return (pair.privateKey.hexString, pair.publicKey.hexString)
|
||||
}
|
||||
}
|
||||
|
||||
private final class CallAttachmentPacketSenderMock: PacketFlowSending {
|
||||
private(set) var sentMessages: [PacketMessage] = []
|
||||
|
||||
func sendPacket(_ packet: any Packet) {
|
||||
if let message = packet as? PacketMessage {
|
||||
sentMessages.append(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
153
RosettaTests/CallPacketParityTests.swift
Normal file
153
RosettaTests/CallPacketParityTests.swift
Normal file
@@ -0,0 +1,153 @@
|
||||
import XCTest
|
||||
@testable import Rosetta
|
||||
|
||||
final class CallPacketParityTests: XCTestCase {
|
||||
func testSignalPeerRoundTripForCallKeyExchangeAndCreateRoom() throws {
|
||||
let call = PacketSignalPeer(
|
||||
src: "02caller",
|
||||
dst: "02callee",
|
||||
sharedPublic: "",
|
||||
signalType: .call,
|
||||
roomId: ""
|
||||
)
|
||||
let keyExchange = PacketSignalPeer(
|
||||
src: "02callee",
|
||||
dst: "02caller",
|
||||
sharedPublic: "abcdef012345",
|
||||
signalType: .keyExchange,
|
||||
roomId: ""
|
||||
)
|
||||
let createRoom = PacketSignalPeer(
|
||||
src: "02caller",
|
||||
dst: "02callee",
|
||||
sharedPublic: "",
|
||||
signalType: .createRoom,
|
||||
roomId: "room-42"
|
||||
)
|
||||
|
||||
let decodedCall = try decodeSignal(call)
|
||||
XCTAssertEqual(decodedCall.signalType, .call)
|
||||
XCTAssertEqual(decodedCall.src, "02caller")
|
||||
XCTAssertEqual(decodedCall.dst, "02callee")
|
||||
XCTAssertEqual(decodedCall.sharedPublic, "")
|
||||
XCTAssertEqual(decodedCall.roomId, "")
|
||||
|
||||
let decodedKeyExchange = try decodeSignal(keyExchange)
|
||||
XCTAssertEqual(decodedKeyExchange.signalType, .keyExchange)
|
||||
XCTAssertEqual(decodedKeyExchange.src, "02callee")
|
||||
XCTAssertEqual(decodedKeyExchange.dst, "02caller")
|
||||
XCTAssertEqual(decodedKeyExchange.sharedPublic, "abcdef012345")
|
||||
XCTAssertEqual(decodedKeyExchange.roomId, "")
|
||||
|
||||
let decodedCreateRoom = try decodeSignal(createRoom)
|
||||
XCTAssertEqual(decodedCreateRoom.signalType, .createRoom)
|
||||
XCTAssertEqual(decodedCreateRoom.src, "02caller")
|
||||
XCTAssertEqual(decodedCreateRoom.dst, "02callee")
|
||||
XCTAssertEqual(decodedCreateRoom.sharedPublic, "")
|
||||
XCTAssertEqual(decodedCreateRoom.roomId, "room-42")
|
||||
}
|
||||
|
||||
func testSignalPeerRoundTripForBusyAndPeerDisconnectedShortFormat() throws {
|
||||
let busy = PacketSignalPeer(
|
||||
src: "02should-not-be-sent",
|
||||
dst: "02should-not-be-sent",
|
||||
sharedPublic: "ignored",
|
||||
signalType: .endCallBecauseBusy,
|
||||
roomId: "ignored-room"
|
||||
)
|
||||
let disconnected = PacketSignalPeer(
|
||||
src: "02should-not-be-sent",
|
||||
dst: "02should-not-be-sent",
|
||||
sharedPublic: "ignored",
|
||||
signalType: .endCallBecausePeerDisconnected,
|
||||
roomId: "ignored-room"
|
||||
)
|
||||
|
||||
let decodedBusy = try decodeSignal(busy)
|
||||
XCTAssertEqual(decodedBusy.signalType, .endCallBecauseBusy)
|
||||
XCTAssertEqual(decodedBusy.src, "")
|
||||
XCTAssertEqual(decodedBusy.dst, "")
|
||||
XCTAssertEqual(decodedBusy.sharedPublic, "")
|
||||
XCTAssertEqual(decodedBusy.roomId, "")
|
||||
|
||||
let decodedDisconnected = try decodeSignal(disconnected)
|
||||
XCTAssertEqual(decodedDisconnected.signalType, .endCallBecausePeerDisconnected)
|
||||
XCTAssertEqual(decodedDisconnected.src, "")
|
||||
XCTAssertEqual(decodedDisconnected.dst, "")
|
||||
XCTAssertEqual(decodedDisconnected.sharedPublic, "")
|
||||
XCTAssertEqual(decodedDisconnected.roomId, "")
|
||||
}
|
||||
|
||||
func testWebRtcRoundTripForOfferAnswerAndIceCandidate() throws {
|
||||
let offer = PacketWebRTC(signalType: .offer, sdpOrCandidate: #"{"type":"offer","sdp":"v=0"}"#)
|
||||
let answer = PacketWebRTC(signalType: .answer, sdpOrCandidate: #"{"type":"answer","sdp":"v=0"}"#)
|
||||
let candidate = PacketWebRTC(
|
||||
signalType: .iceCandidate,
|
||||
sdpOrCandidate: #"{"candidate":"candidate:1 1 udp 2130706431 10.0.0.1 7777 typ host"}"#
|
||||
)
|
||||
|
||||
let decodedOffer = try decodeWebRtc(offer)
|
||||
XCTAssertEqual(decodedOffer.signalType, .offer)
|
||||
XCTAssertEqual(decodedOffer.sdpOrCandidate, offer.sdpOrCandidate)
|
||||
|
||||
let decodedAnswer = try decodeWebRtc(answer)
|
||||
XCTAssertEqual(decodedAnswer.signalType, .answer)
|
||||
XCTAssertEqual(decodedAnswer.sdpOrCandidate, answer.sdpOrCandidate)
|
||||
|
||||
let decodedCandidate = try decodeWebRtc(candidate)
|
||||
XCTAssertEqual(decodedCandidate.signalType, .iceCandidate)
|
||||
XCTAssertEqual(decodedCandidate.sdpOrCandidate, candidate.sdpOrCandidate)
|
||||
}
|
||||
|
||||
func testIceServersRoundTrip() throws {
|
||||
let packet = PacketIceServers(
|
||||
iceServers: [
|
||||
CallIceServer(
|
||||
url: "turn:turn.rosetta.im?transport=udp",
|
||||
username: "u1",
|
||||
credential: "p1",
|
||||
transport: "udp"
|
||||
),
|
||||
CallIceServer(
|
||||
url: "stun:stun.l.google.com:19302",
|
||||
username: "",
|
||||
credential: "",
|
||||
transport: ""
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
let encoded = PacketRegistry.encode(packet)
|
||||
guard let decoded = PacketRegistry.decode(from: encoded),
|
||||
let decodedPacket = decoded.packet as? PacketIceServers
|
||||
else {
|
||||
XCTFail("Failed to decode PacketIceServers")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(decoded.packetId, 0x1C)
|
||||
XCTAssertEqual(decodedPacket.iceServers, packet.iceServers)
|
||||
}
|
||||
|
||||
private func decodeSignal(_ packet: PacketSignalPeer) throws -> PacketSignalPeer {
|
||||
let encoded = PacketRegistry.encode(packet)
|
||||
guard let decoded = PacketRegistry.decode(from: encoded),
|
||||
let decodedPacket = decoded.packet as? PacketSignalPeer
|
||||
else {
|
||||
throw NSError(domain: "CallPacketParityTests", code: 1)
|
||||
}
|
||||
XCTAssertEqual(decoded.packetId, 0x1A)
|
||||
return decodedPacket
|
||||
}
|
||||
|
||||
private func decodeWebRtc(_ packet: PacketWebRTC) throws -> PacketWebRTC {
|
||||
let encoded = PacketRegistry.encode(packet)
|
||||
guard let decoded = PacketRegistry.decode(from: encoded),
|
||||
let decodedPacket = decoded.packet as? PacketWebRTC
|
||||
else {
|
||||
throw NSError(domain: "CallPacketParityTests", code: 2)
|
||||
}
|
||||
XCTAssertEqual(decoded.packetId, 0x1B)
|
||||
return decodedPacket
|
||||
}
|
||||
}
|
||||
104
RosettaTests/CallRoutingTests.swift
Normal file
104
RosettaTests/CallRoutingTests.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import XCTest
|
||||
@testable import Rosetta
|
||||
|
||||
@MainActor
|
||||
final class CallRoutingTests: XCTestCase {
|
||||
private let ownKey = "02-own"
|
||||
private let peerA = "02-peer-a"
|
||||
private let peerB = "02-peer-b"
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
CallManager.shared.resetForSessionEnd()
|
||||
CallManager.shared.bindAccount(publicKey: ownKey)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
CallManager.shared.resetForSessionEnd()
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testIncomingCallMovesToIncomingPhase() {
|
||||
let packet = PacketSignalPeer(
|
||||
src: peerA,
|
||||
dst: ownKey,
|
||||
sharedPublic: "",
|
||||
signalType: .call,
|
||||
roomId: ""
|
||||
)
|
||||
|
||||
CallManager.shared.testHandleSignalPacket(packet)
|
||||
|
||||
XCTAssertEqual(CallManager.shared.uiState.phase, .incoming)
|
||||
XCTAssertEqual(CallManager.shared.uiState.peerPublicKey, peerA)
|
||||
}
|
||||
|
||||
func testBusySignalEndsCurrentCallStateWithoutCrosstalk() {
|
||||
CallManager.shared.testSetUiState(
|
||||
CallUiState(
|
||||
phase: .outgoing,
|
||||
peerPublicKey: peerA,
|
||||
statusText: "Calling..."
|
||||
)
|
||||
)
|
||||
|
||||
let packet = PacketSignalPeer(
|
||||
src: "",
|
||||
dst: "",
|
||||
sharedPublic: "",
|
||||
signalType: .endCallBecauseBusy,
|
||||
roomId: ""
|
||||
)
|
||||
CallManager.shared.testHandleSignalPacket(packet)
|
||||
|
||||
XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
|
||||
XCTAssertEqual(CallManager.shared.uiState.statusText, "User is busy")
|
||||
XCTAssertEqual(CallManager.shared.uiState.peerPublicKey, "")
|
||||
}
|
||||
|
||||
func testPeerDisconnectedSignalEndsCurrentCallState() {
|
||||
CallManager.shared.testSetUiState(
|
||||
CallUiState(
|
||||
phase: .active,
|
||||
peerPublicKey: peerA,
|
||||
statusText: "Call active"
|
||||
)
|
||||
)
|
||||
|
||||
let packet = PacketSignalPeer(
|
||||
src: "",
|
||||
dst: "",
|
||||
sharedPublic: "",
|
||||
signalType: .endCallBecausePeerDisconnected,
|
||||
roomId: ""
|
||||
)
|
||||
CallManager.shared.testHandleSignalPacket(packet)
|
||||
|
||||
XCTAssertEqual(CallManager.shared.uiState.phase, .idle)
|
||||
XCTAssertEqual(CallManager.shared.uiState.statusText, "Peer disconnected")
|
||||
XCTAssertEqual(CallManager.shared.uiState.peerPublicKey, "")
|
||||
}
|
||||
|
||||
func testInterleavingForeignSignalDoesNotOverrideActivePeer() {
|
||||
CallManager.shared.testSetUiState(
|
||||
CallUiState(
|
||||
phase: .outgoing,
|
||||
peerPublicKey: peerA,
|
||||
statusText: "Calling..."
|
||||
)
|
||||
)
|
||||
|
||||
let foreignPacket = PacketSignalPeer(
|
||||
src: peerB,
|
||||
dst: ownKey,
|
||||
sharedPublic: "",
|
||||
signalType: .call,
|
||||
roomId: ""
|
||||
)
|
||||
CallManager.shared.testHandleSignalPacket(foreignPacket)
|
||||
|
||||
XCTAssertEqual(CallManager.shared.uiState.phase, .outgoing)
|
||||
XCTAssertEqual(CallManager.shared.uiState.peerPublicKey, peerA)
|
||||
XCTAssertEqual(CallManager.shared.uiState.statusText, "Calling...")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user