import Foundation import XCTest @testable import CallsKit /// A handoff the browser never sees: it says what it was begun with, and /// hands back `result` when the test lets it. final class FakeHandoff: Handoff, @unchecked Sendable { private let gate = DispatchSemaphore(value: 0) private let result: String private(set) var cancelled = false init(result: String) { self.result = result } func browserURL() -> String { "https://calls.oficina.works/native/start?id=x" } func confirmationCode() -> String { "ABCD-1234" } func wait() throws -> String { gate.wait() if cancelled { throw NSError(domain: "test", code: 1) } return result } func approve() { gate.signal() } func cancel() { cancelled = true gate.signal() } } final class BeginLog: @unchecked Sendable { private let lock = NSLock() private var calls: [String] = [] var log: [String] { lock.withLock { calls } } func add(_ s: String) { lock.withLock { calls.append(s) } } } @MainActor private func settle(_ done: @MainActor () -> Bool) async { let deadline = Date().addingTimeInterval(5) while !done(), Date() < deadline { try? await Task.sleep(for: .milliseconds(10)) } } private let offer = String(repeating: "A", count: 42) + "w" /// A server that cannot be reached (.invalid never resolves), so that a /// test that ends in a join sends nothing anywhere. private let origin = "https://calls.invalid" private let kept = StoredSession( origin: origin, cookies: #"[{"name":"__Host-rastrillo_session","value":"kept"}]"#) @MainActor private func makeModel(_ store: SessionStore, _ remote: SessionRemote) -> CallsModel { let defaults = UserDefaults(suiteName: "AppLinkTests")! defaults.removePersistentDomain(forName: "AppLinkTests") defaults.set(origin, forKey: CallsServer.key) return CallsModel(store: store, remote: remote, defaults: defaults) } private func link(_ path: String, offer: String? = nil) -> URL { var url = "oficina-calls://join?url=https%3A%2F%2Fcalls.invalid%2F" + path.replacingOccurrences(of: "/", with: "%2F") if let offer { url += "&offer=" + offer } return URL(string: url)! } /// The web's "Join in the app" (2026-10-02): reported by a Calls user as /// "App link doesn't work if you're signed in". The link named the /// meeting and nothing else, and the app has a session of its own or none, /// so a guest signed in on the web — or anyone whose default browser was /// not the one they were signed in to — could not get there. final class AppLinkTests: XCTestCase { private func remote(_ server: ScriptedRemote, _ begun: BeginLog, _ handoff: FakeHandoff) -> SessionRemote { var remote = server.remote remote.begin = { address, offer in begun.add("\(address) \(offer ?? "-")") return handoff } return remote } /// Signed out, from the web: the offer is taken up at once, no browser /// is opened (the one the person came from is waiting), and once it /// confirms, the app joins that meeting as that person. @MainActor func testAnOfferFromTheWebIsTakenUpAndJoins() async { // Asked once, when the sign-in is adopted: a guest's is not kept. let server = ScriptedRemote(checks: [.success("participant")]) let begun = BeginLog() let handoff = FakeHandoff( result: #"{"origin":"https://calls.invalid","room":"abc123","cookies":[{"name":"__Host-rastrillo_session","value":"guest"}]}"# ) let model = makeModel(MemorySessionStore(), remote(server, begun, handoff)) model.openMeetingURL(link("m/abc123", offer: offer)) XCTAssertTrue(model.authenticating) XCTAssertTrue(model.handingOff, "confirmed in the browser it came from") XCTAssertEqual(model.pendingLink, "", "started, not offered") await settle { !model.confirmationCode.isEmpty } XCTAssertEqual(begun.log, ["https://calls.invalid/m/abc123 \(offer)"]) XCTAssertEqual(model.confirmationCode, "ABCD-1234") handoff.approve() // The join is real and calls.invalid does not resolve, so it is over // quickly: joined, or tried and said why it could not. await settle { model.joining || !model.error.isEmpty } XCTAssertTrue(model.signedIn) XCTAssertEqual(model.room, "abc123") XCTAssertTrue(model.joining || !model.error.isEmpty, "the meeting they asked for, joined") XCTAssertFalse(model.authenticating) model.leave() } /// Signed in, as somebody that meeting is not for, and no offer: the /// app says so and offers a sign-in for that meeting only, rather than /// joining and failing. @MainActor func testASignInThatCannotJoinSaysSoAndOffersAVisit() async { let server = ScriptedRemote(checks: [.success("member")], opens: [#"{"room":"","noAccess":true}"#]) let begun = BeginLog() let model = makeModel(MemorySessionStore(kept), remote(server, begun, FakeHandoff(result: ""))) model.restore() await settle { model.signedIn && !model.restoring } model.openMeetingURL(link("m/abc123")) await settle { !model.opening && !model.pendingLink.isEmpty } XCTAssertEqual(model.pendingLink, "https://calls.invalid/m/abc123") XCTAssertTrue(model.pendingNoAccess) XCTAssertFalse(model.joining) XCTAssertTrue(model.signedIn, "the app's own sign-in stays") XCTAssertTrue(begun.log.isEmpty, "offered, not started") } /// Signed in, as somebody that meeting is not for, from the web: the /// person in that browser is handed over for it, as a visit. @MainActor func testASignInThatCannotJoinTakesUpTheWebsOffer() async { let server = ScriptedRemote(checks: [.success("member")], opens: [#"{"room":"","noAccess":true}"#]) let begun = BeginLog() let handoff = FakeHandoff(result: "") let model = makeModel(MemorySessionStore(kept), remote(server, begun, handoff)) model.restore() await settle { model.signedIn && !model.restoring } model.openMeetingURL(link("m/abc123", offer: offer)) await settle { !begun.log.isEmpty } XCTAssertEqual(begun.log, ["https://calls.invalid/m/abc123 \(offer)"]) XCTAssertTrue(model.handingOff) model.cancelSignIn() } /// Signed in, as someone who may join: the app's own session opens it, /// with no browser, offer or not — the case that always worked. @MainActor func testASignInThatCanJoinNeedsNoBrowser() async { let server = ScriptedRemote(checks: [.success("member")], opens: [#"{"room":"abc123"}"#]) let begun = BeginLog() let model = makeModel(MemorySessionStore(kept), remote(server, begun, FakeHandoff(result: ""))) model.restore() await settle { model.signedIn && !model.restoring } model.openMeetingURL(link("m/abc123", offer: offer)) await settle { model.joining } XCTAssertEqual(model.room, "abc123") XCTAssertTrue(begun.log.isEmpty) model.leave() } /// The link launched the app with nothing kept: the offer waits for the /// check of the kept sign-in, then is taken up. @MainActor func testAnOfferThatLaunchedTheAppIsTakenUpAfterTheCheck() async { let server = ScriptedRemote() let begun = BeginLog() let handoff = FakeHandoff(result: "") let model = makeModel(MemorySessionStore(), remote(server, begun, handoff)) model.restore() model.openMeetingURL(link("m/abc123", offer: offer)) await settle { !begun.log.isEmpty } XCTAssertEqual(begun.log, ["https://calls.invalid/m/abc123 \(offer)"]) model.cancelSignIn() } /// A link on another server, with nobody able to take it up, says which /// server it is on before anything opens. @MainActor func testALinkOnAnotherServerSaysWhichServer() { let model = makeModel(MemorySessionStore(), ScriptedRemote().remote) model.openMeetingURL( URL(string: "oficina-calls://join?url=https%3A%2F%2Fcalls.example.org%2Fm%2Fabc123")!) XCTAssertEqual(model.pendingLink, "https://calls.example.org/m/abc123") XCTAssertFalse(model.pendingOnServer) XCTAssertEqual(model.pendingHost, "calls.example.org") XCTAssertFalse(model.authenticating) } func testOnlyTheOffersTheServerMakesAreTaken() { XCTAssertTrue(CallsModel.validOffer(offer)) XCTAssertTrue(CallsModel.validOffer(String(repeating: "_-", count: 21) + "a")) for bad in ["", "short", String(repeating: "A", count: 44), String(repeating: "A", count: 42) + "/", String(repeating: "A", count: 42) + "é"] { XCTAssertFalse(CallsModel.validOffer(bad), bad) } } }