import Foundation
import Testing
@testable import IrdP2P
@testable import IrdP2PSignaling
import IrdP2PCrypto
@Suite struct RoomHandshakeTests {
@Test func twoPeersVerifyEachOtherAndGoDirect() async throws {
let h = Harness()
let (a, ca) = try await h.join(peer: lowPeer)
#expect(a.roomId == RoomCrypto.roomId(secret: h.secret, appSalt: h.appSalt))
#expect(await ca.waitFor(.status(.joined)))
#expect(await ca.waitFor(.roster([])))
let (b, cb) = try await h.join(peer: highPeer)
#expect(await cb.waitFor(.roster([]))) // b's roster settles before a is verified
#expect(await ca.waitFor(.peerJoined(highPeer)))
#expect(await ca.waitFor(.peerReady(highPeer, .relayed)))
#expect(await cb.waitFor(.peerJoined(lowPeer)))
#expect(await cb.waitFor(.peerReady(lowPeer, .relayed)))
#expect(await ca.waitFor(.peerReady(highPeer, .direct)))
#expect(await cb.waitFor(.peerReady(lowPeer, .direct)))
#expect(await a.members == [highPeer])
#expect(await b.transport(of: lowPeer) == .direct)
// no identity announced: no identity events
#expect(!ca.all.contains { if case .peerIdentity = $0 { return true } else { return false } })
await b.leave()
#expect(await ca.waitFor(.peerLeft(highPeer)))
#expect(await cb.waitFor(.status(.closed(reason: "left"))))
await a.leave()
}
@Test func identityIsVerifiedAndAnnounced() async throws {
let h = Harness()
var ida = Identity.generate()
ida.name = "Ana"; ida.hue = 200; ida.glyph = "🐢"
let (a, ca) = try await h.join(peer: lowPeer, identity: ida, preferDataChannel: false)
let (b, cb) = try await h.join(peer: highPeer, identity: nil, preferDataChannel: false)
let expected = PeerIdentity(publicKeyBase64URL: ida.publicKeyBase64URL, fingerprint: ida.fingerprint, name: "Ana", hue: 200, glyph: "🐢")
#expect(await cb.waitFor(.peerIdentity(lowPeer, expected)))
#expect(await b.identity(of: lowPeer) == expected)
#expect(await ca.waitFor(.peerJoined(highPeer)))
#expect(await a.identity(of: highPeer) == nil)
await a.leave(); await b.leave()
}
@Test func strangersInTheRoomAreIgnored() async throws {
let h = Harness()
let (a, ca) = try await h.join(peer: lowPeer, preferDataChannel: false)
#expect(await ca.waitFor(.roster([])))
// someone who knows the room id but not the secret joins the relay room and talks garbage
var cfg = RelaySession.Configuration(url: URL(string: "wss://x/ws")!, origin: nil, room: a.roomId, peer: thirdPeer, limits: .defaults)
cfg.pacing = false
let intruder = RelaySession(configuration: cfg, transport: h.relay, clock: h.clock)
try await intruder.start()
try await intruder.send(signal: .object(["v": .int(1), "iv": .string("AAAAAAAAAAAAAAAA"), "ct": .string("AAAAAAAAAAAAAAAAAAAAAAAAAAAA")]), to: lowPeer)
try await intruder.send(signal: .string("junk"), to: lowPeer)
// a genuine peer still works
let (b, cb) = try await h.join(peer: highPeer, preferDataChannel: false)
#expect(await cb.waitFor(.peerJoined(lowPeer)))
#expect(await ca.waitFor(.peerJoined(highPeer)))
#expect(await a.members == [highPeer])
#expect(!ca.contains(.peerJoined(thirdPeer)))
await intruder.leave()
#expect(!ca.contains(.peerLeft(thirdPeer)))
await a.leave(); await b.leave()
}
@Test func wrongSecretNeverMeets() async throws {
let h = Harness()
let (a, ca) = try await h.join(peer: lowPeer, preferDataChannel: false)
let (b, cb) = try await h.join(peer: highPeer, preferDataChannel: false, secret: .random())
#expect(await ca.waitFor(.roster([])))
#expect(await cb.waitFor(.roster([])))
#expect(a.roomId != b.roomId)
await a.leave(); await b.leave()
}
@Test func silentPeerBecomesAStrangerAfterHelloTimeout() async throws {
let h = Harness()
let (a, ca) = try await h.join(peer: lowPeer, preferDataChannel: false)
#expect(await ca.waitFor(.roster([])))
var cfg = RelaySession.Configuration(url: URL(string: "wss://x/ws")!, origin: nil, room: a.roomId, peer: highPeer, limits: .defaults)
cfg.pacing = false
let silent = RelaySession(configuration: cfg, transport: h.relay, clock: h.clock)
try await silent.start()
#expect(await eventuallyTrue { h.relay.signalFrameCount >= 1 }) // a sent its hello
h.clock.advance(by: .seconds(11))
try? await Task.sleep(for: .milliseconds(50))
#expect(await a.members.isEmpty)
#expect(!ca.contains(.peerJoined(highPeer)))
await silent.leave(); await a.leave()
}
}
func eventuallyTrue(timeout: Duration = .seconds(3), _ condition: @Sendable () async -> Bool) async -> Bool {
let deadline = ContinuousClock.now + timeout
while ContinuousClock.now < deadline {
if await condition() { return true }
try? await Task.sleep(for: .milliseconds(5))
}
return await condition()
}