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
6private let other = "https://calls.example.org"
7private let kept = StoredSession(
8 origin: CallsServer.defaultOrigin,
9 cookies: #"[{"name":"__Host-rastrillo_session","value":"kept"}]"#)
10
11/// A defaults suite of its own per test, so nothing here reads or writes
12/// the setting of whoever runs the tests.
13private func freshDefaults() -> UserDefaults {
14 let name = "CallsKitTests.\(UUID().uuidString)"
15 let defaults = UserDefaults(suiteName: name)!
16 defaults.removePersistentDomain(forName: name)
17 return defaults
18}
19
20@MainActor private func settle(_ done: @MainActor () -> Bool) async {
21 let deadline = Date().addingTimeInterval(5)
22 while !done(), Date() < deadline { try? await Task.sleep(for: .milliseconds(10)) }
23}
24
25/// What a browser sign-in hands back, in the bridge's shape.
26private func signInResult(origin: String, room: String = "") -> String {
27 #"{"origin":"\#(origin)","room":"\#(room)","cookies":[{"name":"__Host-rastrillo_session","value":"fresh"}]}"#
28}
29
30final class DefaultServerTests: XCTestCase {
31 // MARK: The setting
32
33 @MainActor func testTheServerIsCallsOficinaWorksUntilChosen() {
34 let model = CallsModel(store: MemorySessionStore(), defaults: freshDefaults())
35 XCTAssertEqual(CallsServer.defaultOrigin, "https://calls.oficina.works")
36 XCTAssertEqual(model.server, "https://calls.oficina.works")
37 XCTAssertEqual(model.serverHost, "calls.oficina.works")
38 }
39
40 func testServerAddressesAreValidatedAndWrittenAsAnOrigin() {
41 let accepted: [(String, String)] = [
42 ("https://calls.oficina.works", "https://calls.oficina.works"),
43 (" https://calls.example.org/ ", "https://calls.example.org"),
44 ("https://calls.example.org//", "https://calls.example.org"),
45 ("HTTPS://Calls.Example.org", "https://calls.example.org"),
46 ("calls.example.org", "https://calls.example.org"),
47 ("https://calls.example.org:8443", "https://calls.example.org:8443"),
48 ("http://localhost:8080", "http://localhost:8080"),
49 ("http://127.0.0.1:8080/", "http://127.0.0.1:8080"),
50 ]
51 for (text, origin) in accepted {
52 XCTAssertEqual(try? CallsServer.validate(text).get(), origin, text)
53 }
54 let refused: [(String, CallsServer.Problem)] = [
55 ("", .empty),
56 (" ", .empty),
57 ("http://calls.example.org", .insecure),
58 ("ftp://calls.example.org", .notAnAddress),
59 ("https://", .notAnAddress),
60 ("not an address", .notAnAddress),
61 ("https://calls.example.org/m/abc123", .notJustTheServer),
62 ("https://calls.example.org/?next=1", .notJustTheServer),
63 ("https://calls.example.org#top", .notJustTheServer),
64 ("https://paul:secret@calls.example.org", .credentials),
65 ]
66 for (text, problem) in refused {
67 XCTAssertEqual(CallsServer.validate(text), .failure(problem), text)
68 XCTAssertFalse(problem.message.isEmpty)
69 }
70 }
71
72 func testTheSavedServerIsReadBackAndTheDefaultIsSavedAsNothing() {
73 let defaults = freshDefaults()
74 CallsServer.save(other, to: defaults)
75 XCTAssertEqual(CallsServer.load(from: defaults), other)
76 CallsServer.save(CallsServer.defaultOrigin, to: defaults)
77 XCTAssertNil(defaults.object(forKey: CallsServer.key))
78 defaults.set("http://calls.example.org", forKey: CallsServer.key)
79 XCTAssertEqual(
80 CallsServer.load(from: defaults), CallsServer.defaultOrigin,
81 "an invalid saved value falls back to the default")
82 }
83
84 // MARK: The signed-out home
85
86 @MainActor func testSignedOutHomeIsTheServerAndNothingToType() async {
87 let server = ScriptedRemote()
88 let model = CallsModel(
89 store: MemorySessionStore(), remote: server.remote, defaults: freshDefaults())
90 model.restore()
91 await settle { !model.restoring }
92 XCTAssertFalse(model.signedIn)
93 XCTAssertFalse(model.authenticating)
94 XCTAssertEqual(model.pendingLink, "")
95 XCTAssertEqual(model.address, "")
96 XCTAssertEqual(model.server, CallsServer.defaultOrigin)
97 XCTAssertTrue(server.requests.isEmpty)
98 }
99
100 @MainActor func testALinkOnTheServerIsASignInForTheServer() {
101 let model = CallsModel(store: MemorySessionStore(), defaults: freshDefaults())
102 model.openMeetingURL(
103 URL(string: "oficina-calls://join?url=https%3A%2F%2Fcalls.oficina.works%2Fj%2Finvitation")!)
104 XCTAssertEqual(model.pendingLink, "https://calls.oficina.works/j/invitation")
105 XCTAssertTrue(model.pendingOnServer)
106 XCTAssertEqual(model.pendingHost, "calls.oficina.works")
107 XCTAssertFalse(model.authenticating, "the person sees where it is before a browser opens")
108 }
109
110 // MARK: Changing the server
111
112 @MainActor func testChangingTheServerSignsOutOfTheOldOne() async {
113 let defaults = freshDefaults()
114 let store = MemorySessionStore(kept)
115 let server = ScriptedRemote(checks: [.success("member")])
116 let model = CallsModel(store: store, remote: server.remote, defaults: defaults)
117 model.restore()
118 await settle { model.signedIn }
119 XCTAssertTrue(model.changingServerSignsOut, "the settings ask first")
120 XCTAssertNil(model.changeServer(to: " calls.example.org/ "))
121 XCTAssertEqual(model.server, other)
122 XCTAssertEqual(defaults.string(forKey: CallsServer.key), other)
123 XCTAssertFalse(model.signedIn)
124 XCTAssertEqual(model.origin, "")
125 await settle { store.session == nil && server.requests.count == 2 }
126 XCTAssertNil(store.session, "the Keychain copy is forgotten")
127 XCTAssertEqual(
128 server.requests.last, "signout \(kept.origin) \(kept.cookies)",
129 "and the old server is asked to revoke it")
130 // A new model — the next launch — reads the new server.
131 XCTAssertEqual(CallsModel(store: store, defaults: defaults).server, other)
132 }
133
134 @MainActor func testAnInvalidServerChangesNothing() async {
135 let defaults = freshDefaults()
136 let store = MemorySessionStore(kept)
137 let server = ScriptedRemote(checks: [.success("member")])
138 let model = CallsModel(store: store, remote: server.remote, defaults: defaults)
139 model.restore()
140 await settle { model.signedIn }
141 XCTAssertEqual(model.changeServer(to: "http://calls.example.org"), .insecure)
142 XCTAssertEqual(model.server, CallsServer.defaultOrigin)
143 XCTAssertTrue(model.signedIn)
144 XCTAssertEqual(store.session, kept)
145 XCTAssertNil(defaults.object(forKey: CallsServer.key))
146 }
147
148 @MainActor func testTheServerIsNotChangedDuringACall() {
149 let defaults = freshDefaults()
150 let store = MemorySessionStore(kept)
151 let model = CallsModel(store: store, remote: ScriptedRemote().remote, defaults: defaults)
152 model.signedIn = true
153 model.joining = true
154 XCTAssertNil(model.changeServer(to: other))
155 XCTAssertEqual(model.server, CallsServer.defaultOrigin)
156 XCTAssertTrue(model.signedIn)
157 XCTAssertEqual(store.session, kept)
158 XCTAssertFalse(model.error.isEmpty)
159 }
160
161 @MainActor func testResettingToTheDefaultRemovesTheSetting() {
162 let defaults = freshDefaults()
163 CallsServer.save(other, to: defaults)
164 let model = CallsModel(store: MemorySessionStore(), defaults: defaults)
165 XCTAssertEqual(model.server, other)
166 XCTAssertFalse(model.changingServerSignsOut)
167 XCTAssertNil(model.changeServer(to: CallsServer.defaultOrigin))
168 XCTAssertEqual(model.server, CallsServer.defaultOrigin)
169 XCTAssertNil(defaults.object(forKey: CallsServer.key))
170 }
171
172 @MainActor func testASessionKeptForAnotherServerIsSignedOutAtLaunch() async {
173 // The setting was changed outside the app, leaving the Keychain
174 // holding the old server's session.
175 let defaults = freshDefaults()
176 CallsServer.save(other, to: defaults)
177 let store = MemorySessionStore(kept)
178 let server = ScriptedRemote()
179 let model = CallsModel(store: store, remote: server.remote, defaults: defaults)
180 model.restore()
181 await settle { !model.restoring && store.session == nil && !server.requests.isEmpty }
182 XCTAssertFalse(model.signedIn)
183 XCTAssertNil(store.session)
184 XCTAssertEqual(server.requests, ["signout \(kept.origin) \(kept.cookies)"])
185 }
186
187 // MARK: Guests and other servers
188
189 func testInviteLinksAreValidatedInPlainWords() {
190 for text in [
191 "https://calls.oficina.works/j/abcdef", "https://calls.example.org/m/abc123",
192 " https://calls.example.org/m/abc123/ ", "calls.example.org/j/token",
193 "http://127.0.0.1:8080/j/token",
194 ] {
195 XCTAssertNotNil(try? MeetingLink.validate(text).get(), text)
196 }
197 let refused: [(String, MeetingLink.Problem)] = [
198 ("", .empty),
199 ("not a meeting", .notALink),
200 ("file:///j/token", .notALink),
201 ("http://calls.example.org/j/token", .insecure),
202 ("https://paul:secret@calls.example.org/j/token", .credentials),
203 ("https://calls.example.org", .notACall),
204 ("https://calls.example.org/settings", .notACall),
205 ("https://calls.example.org/j/token?x=1", .notACall),
206 ("https://calls.example.org/x/token", .notACall),
207 ]
208 for (text, problem) in refused {
209 XCTAssertEqual(MeetingLink.validate(text), .failure(problem), text)
210 XCTAssertFalse(problem.message.isEmpty)
211 }
212 XCTAssertEqual(
213 MeetingLink.validate("http://127.0.0.1:8080/j/token", allowLoopback: false),
214 .failure(.insecure), "a link from outside the app is https only")
215 }
216
217 @MainActor func testAnInvalidGuestLinkStartsNothing() {
218 let model = CallsModel(store: MemorySessionStore(), defaults: freshDefaults())
219 XCTAssertEqual(model.joinAsGuest("https://calls.example.org/settings"), .notACall)
220 XCTAssertFalse(model.authenticating)
221 XCTAssertEqual(model.server, CallsServer.defaultOrigin)
222 }
223
224 @MainActor func testAGuestSessionIsNeverKeptEvenAMembersOnTheServer() async throws {
225 let store = MemorySessionStore()
226 let server = ScriptedRemote(checks: [.success("member")])
227 let model = CallsModel(store: store, remote: server.remote, defaults: freshDefaults())
228 try model.adopt(signInResult(origin: CallsServer.defaultOrigin, room: "abc123"), visit: true)
229 XCTAssertTrue(model.signedIn)
230 XCTAssertTrue(model.visiting)
231 XCTAssertFalse(model.canStart, "a guest is here for one meeting")
232 XCTAssertEqual(model.room, "abc123")
233 try? await Task.sleep(for: .milliseconds(100))
234 XCTAssertNil(store.session)
235 XCTAssertTrue(server.requests.isEmpty, "not even asked whether it is worth keeping")
236 }
237
238 @MainActor func testASignInOnAnotherServerIsAVisitAndChangesNoSetting() async throws {
239 let defaults = freshDefaults()
240 let store = MemorySessionStore(kept)
241 let server = ScriptedRemote(checks: [.success("member")])
242 let model = CallsModel(store: store, remote: server.remote, defaults: defaults)
243 try model.adopt(signInResult(origin: other, room: "abc123"), visit: false)
244 XCTAssertTrue(model.visiting, "another server is a visit however it began")
245 try? await Task.sleep(for: .milliseconds(100))
246 XCTAssertEqual(store.session, kept, "the server's own sign-in is not replaced")
247 XCTAssertEqual(model.server, CallsServer.defaultOrigin)
248 XCTAssertNil(defaults.object(forKey: CallsServer.key))
249 }
250
251 @MainActor func testTheServersOwnSignInIsKept() async throws {
252 let store = MemorySessionStore()
253 let server = ScriptedRemote(checks: [.success("member")])
254 let model = CallsModel(store: store, remote: server.remote, defaults: freshDefaults())
255 try model.adopt(signInResult(origin: CallsServer.defaultOrigin), visit: false)
256 XCTAssertFalse(model.visiting)
257 XCTAssertTrue(model.canStart)
258 await settle { store.session != nil }
259 XCTAssertEqual(store.session?.origin, CallsServer.defaultOrigin)
260 }
261
262 @MainActor func testEndingAVisitComesBackToTheMemberHome() async throws {
263 let store = MemorySessionStore(kept)
264 let server = ScriptedRemote(checks: [.success("member")])
265 let model = CallsModel(store: store, remote: server.remote, defaults: freshDefaults())
266 try model.adopt(signInResult(origin: other, room: "abc123"), visit: true)
267 model.signOut()
268 await settle { model.signedIn && !model.restoring }
269 XCTAssertFalse(model.visiting)
270 XCTAssertEqual(model.origin, CallsServer.defaultOrigin, "the kept sign-in is back")
271 XCTAssertEqual(store.session, kept)
272 XCTAssertTrue(server.requests.contains { $0.hasPrefix("signout \(other) ") })
273 XCTAssertFalse(server.requests.contains { $0.hasPrefix("signout \(kept.origin)") })
274 }
275
276 @MainActor func testEndingAVisitWithNothingKeptIsTheSignedOutHome() async throws {
277 let store = MemorySessionStore()
278 let server = ScriptedRemote()
279 let model = CallsModel(store: store, remote: server.remote, defaults: freshDefaults())
280 try model.adopt(signInResult(origin: other, room: "abc123"), visit: true)
281 model.leave()
282 await settle { !model.signedIn && !model.restoring }
283 XCTAssertFalse(model.signedIn)
284 XCTAssertFalse(model.visiting)
285 XCTAssertEqual(model.room, "")
286 XCTAssertNil(store.session)
287 }
288}
289