oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import XCTest
3
4@testable import CallsKit
5
6/// The Keychain's stand-in: the same three calls, held in memory, and
7/// safe to reach from the detached tasks the model uses for the real one.
8final class MemorySessionStore: SessionStore, @unchecked Sendable {
9 private let lock = NSLock()
10 private var kept: StoredSession?
11
12 init(_ kept: StoredSession? = nil) { self.kept = kept }
13
14 var session: StoredSession? { lock.withLock { kept } }
15 func load() -> StoredSession? { lock.withLock { kept } }
16 func save(_ session: StoredSession) { lock.withLock { kept = session } }
17 func clear() { lock.withLock { kept = nil } }
18}
19
20/// What the server said, scripted, and what the model asked it.
21final class ScriptedRemote: @unchecked Sendable {
22 private let lock = NSLock()
23 private var checks: [Result<String, Error>]
24 private var opens: [String]
25 private(set) var asked: [String] = []
26
27 init(checks: [Result<String, Error>] = [], opens: [String] = []) {
28 self.checks = checks
29 self.opens = opens
30 }
31
32 var requests: [String] { lock.withLock { asked } }
33
34 var remote: SessionRemote {
35 SessionRemote(
36 check: { origin, cookies in
37 try self.lock.withLock {
38 self.asked.append("check \(origin) \(cookies)")
39 return try self.checks.removeFirst().get()
40 }
41 },
42 open: { origin, _, address in
43 self.lock.withLock {
44 self.asked.append("open \(origin) \(address)")
45 return self.opens.removeFirst()
46 }
47 },
48 signOut: { origin, cookies in
49 self.lock.withLock { self.asked.append("signout \(origin) \(cookies)") }
50 })
51 }
52}
53
54private let kept = StoredSession(
55 origin: "https://calls.oficina.works",
56 cookies: #"[{"name":"__Host-rastrillo_session","value":"kept"}]"#)
57
58private struct Unreachable: LocalizedError {
59 var errorDescription: String? { "cannot reach Calls" }
60}
61
62/// The model's work here happens in tasks it starts itself; this waits
63/// for the state they leave rather than for the tasks, which are not
64/// the model's to hand out.
65@MainActor private func settle(_ done: @MainActor () -> Bool) async {
66 let deadline = Date().addingTimeInterval(5)
67 while !done(), Date() < deadline { try? await Task.sleep(for: .milliseconds(10)) }
68}
69
70final class KeptSignInTests: XCTestCase {
71 @MainActor func testNothingKeptIsTheSignInScreenAndAsksNobody() async {
72 let server = ScriptedRemote()
73 let model = CallsModel(store: MemorySessionStore(), remote: server.remote)
74 model.restore()
75 await settle { !model.restoring }
76 XCTAssertFalse(model.signedIn)
77 XCTAssertTrue(server.requests.isEmpty)
78 }
79
80 @MainActor func testAKeptMemberSessionLandsHomeWithStartOffered() async {
81 let server = ScriptedRemote(checks: [.success("member")])
82 let model = CallsModel(store: MemorySessionStore(kept), remote: server.remote)
83 model.restore()
84 XCTAssertTrue(model.restoring, "nothing is offered until the instance answers")
85 await settle { !model.restoring }
86 XCTAssertTrue(model.signedIn)
87 XCTAssertTrue(model.canStart)
88 XCTAssertFalse(model.unchecked)
89 XCTAssertEqual(model.room, "")
90 XCTAssertEqual(model.origin, kept.origin)
91 XCTAssertEqual(server.requests, ["check \(kept.origin) \(kept.cookies)"])
92 }
93
94 @MainActor func testAnEndedSessionIsForgottenAndPointsAtSignIn() async {
95 let store = MemorySessionStore(kept)
96 let server = ScriptedRemote(checks: [.success("signed-out")])
97 let model = CallsModel(store: store, remote: server.remote)
98 model.restore()
99 await settle { !model.restoring && store.session == nil }
100 XCTAssertFalse(model.signedIn)
101 XCTAssertNil(store.session)
102 XCTAssertEqual(model.pendingLink, "", "signing in again is the home's one button")
103 XCTAssertTrue(model.error.contains("calls.oficina.works"))
104 XCTAssertTrue(model.error.contains("Choose Sign in"))
105 }
106
107 @MainActor func testABadConnectionKeepsTheSessionAndOffersToCheckAgain() async {
108 let store = MemorySessionStore(kept)
109 let server = ScriptedRemote(checks: [.failure(Unreachable()), .success("member")])
110 let model = CallsModel(store: store, remote: server.remote)
111 model.restore()
112 await settle { !model.restoring }
113 XCTAssertEqual(store.session, kept, "no network says nothing about the session")
114 XCTAssertTrue(model.signedIn)
115 XCTAssertTrue(model.unchecked)
116 XCTAssertTrue(model.error.contains("cannot reach Calls"))
117 model.recheck()
118 await settle { !model.restoring }
119 XCTAssertFalse(model.unchecked)
120 XCTAssertTrue(model.error.isEmpty)
121 XCTAssertTrue(model.signedIn)
122 }
123
124 @MainActor func testASessionThatIsNoLongerAMembersIsNotKept() async {
125 let store = MemorySessionStore(kept)
126 let server = ScriptedRemote(checks: [.success("participant")])
127 let model = CallsModel(store: store, remote: server.remote)
128 model.restore()
129 await settle { !model.restoring && store.session == nil }
130 XCTAssertFalse(model.signedIn)
131 XCTAssertNil(store.session)
132 }
133
134 @MainActor func testOnlyAFreshMembersSessionIsKept() async {
135 for (answer, keptAfter, canStart) in [
136 (Result<String, Error>.success("member"), true, true),
137 (.success("participant"), false, false),
138 (.failure(Unreachable()), false, true),
139 ] {
140 let store = MemorySessionStore()
141 let server = ScriptedRemote(checks: [answer])
142 let model = CallsModel(store: store, remote: server.remote)
143 model.origin = kept.origin
144 model.cookies = kept.cookies
145 model.signedIn = true
146 model.canStart = true
147 model.keep(kept)
148 await settle { !server.requests.isEmpty && (store.session != nil) == keptAfter }
149 try? await Task.sleep(for: .milliseconds(50))
150 XCTAssertEqual(store.session != nil, keptAfter, "\(answer)")
151 XCTAssertEqual(model.canStart, canStart, "\(answer)")
152 }
153 }
154
155 @MainActor func testSigningOutForgetsAtOnceAndTellsTheInstance() async {
156 let store = MemorySessionStore(kept)
157 let server = ScriptedRemote(checks: [.success("member")])
158 let model = CallsModel(store: store, remote: server.remote)
159 model.restore()
160 await settle { model.signedIn }
161 model.room = "abc123"
162 model.signOut()
163 XCTAssertFalse(model.signedIn)
164 XCTAssertFalse(model.canStart)
165 XCTAssertEqual(model.room, "")
166 XCTAssertEqual(model.origin, "")
167 await settle { store.session == nil && server.requests.count == 2 }
168 XCTAssertNil(store.session)
169 XCTAssertEqual(server.requests.last, "signout \(kept.origin) \(kept.cookies)")
170 }
171
172 @MainActor func testSigningOutIsRefusedDuringACall() {
173 let store = MemorySessionStore(kept)
174 let model = CallsModel(store: store, remote: ScriptedRemote().remote)
175 model.signedIn = true
176 model.joining = true
177 model.signOut()
178 XCTAssertTrue(model.signedIn)
179 XCTAssertEqual(store.session, kept)
180 }
181
182 @MainActor func testALinkOnTheSameInstanceOpensWithTheSessionNotTheBrowser() async {
183 let server = ScriptedRemote(checks: [.success("member")], opens: [#"{"room":""}"#])
184 let model = CallsModel(store: MemorySessionStore(kept), remote: server.remote)
185 model.restore()
186 await settle { model.signedIn }
187 model.room = "old123"
188 model.openMeetingURL(
189 URL(string: "oficina-calls://join?url=https%3A%2F%2Fcalls.oficina.works%2Fj%2Finvitation")!)
190 XCTAssertFalse(model.authenticating, "no browser sign-in for an instance already signed in to")
191 XCTAssertTrue(model.signedIn)
192 await settle { !model.opening }
193 XCTAssertEqual(server.requests.last, "open \(kept.origin) https://calls.oficina.works/j/invitation")
194 XCTAssertEqual(model.room, "", "the answer replaces the meeting in hand")
195 XCTAssertEqual(model.address, "")
196 }
197
198 @MainActor func testALinkThatLaunchedTheAppWaitsForTheCheckAndThenOpens() async {
199 let server = ScriptedRemote(checks: [.success("member")], opens: [#"{"room":""}"#])
200 let model = CallsModel(store: MemorySessionStore(kept), remote: server.remote)
201 model.restore()
202 model.openMeetingURL(
203 URL(string: "oficina-calls://join?url=https%3A%2F%2Fcalls.oficina.works%2Fm%2Fabc123")!)
204 XCTAssertFalse(model.authenticating)
205 await settle { server.requests.count == 2 && !model.opening }
206 XCTAssertEqual(
207 server.requests,
208 [
209 "check \(kept.origin) \(kept.cookies)",
210 "open \(kept.origin) https://calls.oficina.works/m/abc123",
211 ])
212 }
213
214 @MainActor func testTheInstanceIsComparedWithItsPortAndWithoutItsPath() {
215 XCTAssertEqual(
216 CallsModel.instance(of: "https://calls.oficina.works/j/abc"), "https://calls.oficina.works")
217 XCTAssertEqual(CallsModel.instance(of: "http://127.0.0.1:8080/m/x"), "http://127.0.0.1:8080")
218 XCTAssertNil(CallsModel.instance(of: "not a meeting"))
219 }
220
221 /// Not a unit test: the real Keychain, on the machine running it. Opt
222 /// in with CALLS_KEYCHAIN_TEST=1, because on a Mac a development build
223 /// falls back to the login keychain and a CI runner may not have one.
224 func testTheKeychainKeepsAndForgets() throws {
225 guard ProcessInfo.processInfo.environment["CALLS_KEYCHAIN_TEST"] == "1" else {
226 throw XCTSkip("Set CALLS_KEYCHAIN_TEST=1 to use this machine's Keychain.")
227 }
228 let store = KeychainSessionStore(service: "net.amadan.oficina.calls.test-\(UUID().uuidString)")
229 defer { store.clear() }
230 XCTAssertNil(store.load())
231 store.save(kept)
232 XCTAssertEqual(store.load(), kept)
233 let replacement = StoredSession(origin: "https://other.test", cookies: "[]")
234 store.save(replacement)
235 XCTAssertEqual(store.load(), replacement, "one sign-in at a time")
236 store.clear()
237 XCTAssertNil(store.load())
238 }
239}
240