gitbayTests/SessionClientTests.swift
106 lines · 4376 bytes
1import Foundation
2import Testing
3@testable import gitbay
4
5private let whoami = """
6 {"protocol_version":1,"data":{"username":"cmc","admin":false,"key_scope":"full"},"exit_code":0}
7 """
8
9@MainActor
10private func makeSession() -> (SessionStore, StubProtocol.Box) {
11 let box = StubProtocol.box()
12 let defaults = UserDefaults(suiteName: "test.\(UUID().uuidString)")!
13 let session = SessionStore(store: MemoryTokenStore(), defaults: defaults) { instance, token in
14 GitbayClient(instance: instance, token: token, session: box.session())
15 }
16 return (session, box)
17}
18
19/// Signs two accounts in, on different instances.
20@MainActor
21private func twoAccounts() async throws -> (SessionStore, StubProtocol.Box) {
22 let (session, box) = makeSession()
23 box.enqueue(.init(status: 200, json: whoami))
24 try await session.signIn(instanceURL: "https://gitbay.org", token: "t1")
25 box.enqueue(.init(status: 200, json: whoami))
26 try await session.signIn(instanceURL: "https://dev.local", token: "t2")
27 return (session, box)
28}
29
30@Test @MainActor func sessionVendsAClientForAnyStoredAccount() async throws {
31 let (session, _) = try await twoAccounts()
32 let inactive = try #require(session.accounts.first { $0.id != session.current?.id })
33 #expect(session.client(for: inactive) != nil)
34 // Vending does not change which account is active.
35 #expect(session.current?.id != inactive.id)
36}
37
38@Test @MainActor func sessionVendsNothingForAnUnknownAccount() async throws {
39 let (session, _) = try await twoAccounts()
40 let stranger = Account(instance: try GitbayInstance(url: "https://nowhere.example"), username: "x")
41 #expect(session.client(for: stranger) == nil)
42}
43
44// The hook runs before the token is discarded: deregistering a device
45// needs the credential remove() is about to delete.
46@Test @MainActor func removeAwaitsTheHookBeforeDiscardingTheToken() async throws {
47 let (session, _) = try await twoAccounts()
48 let victim = try #require(session.accounts.first)
49 var sawTokenDuringHook: Bool?
50 // [weak session] because this closure is stored ON session; the
51 // production wiring in gitbayApp captures the registrar weakly for
52 // the same reason.
53 session.willRemoveAccount = { [weak session] account in
54 sawTokenDuringHook = session?.client(for: account) != nil
55 }
56 await session.remove(victim)
57 #expect(sawTokenDuringHook == true)
58 #expect(session.client(for: victim) == nil)
59}
60
61// The counterpart to the removal hook: an account added while the app
62// runs has no push device row until something registers it, and
63// nothing else does before the next cold launch.
64@Test @MainActor func signInRunsTheAddHook() async throws {
65 let (session, box) = makeSession()
66 var added: [String] = []
67 session.didAddAccount = { added.append($0.id) }
68
69 box.enqueue(.init(status: 200, json: whoami))
70 try await session.signIn(instanceURL: "https://gitbay.org", token: "t1")
71
72 #expect(added == [try #require(session.current).id])
73}
74
75// A sign-in that stored nothing must not announce an account.
76@Test @MainActor func aFailedSignInDoesNotRunTheAddHook() async throws {
77 let (session, box) = makeSession()
78 var hookRan = false
79 session.didAddAccount = { _ in hookRan = true }
80
81 box.enqueue(.init(status: 401, json: #"{"protocol_version":1,"error":"nope","exit_code":4}"#))
82 await #expect(throws: (any Error).self) {
83 try await session.signIn(instanceURL: "https://gitbay.org", token: "bad")
84 }
85
86 #expect(hookRan == false)
87 #expect(session.accounts.isEmpty)
88}
89
90// expire is reached only after the server has already rejected the
91// account's token with a 401 (revocation, or handle's
92// requiresReauthentication path), so a deregistration call carrying
93// that token would fail the same way. It deliberately does not run
94// the hook; the stale device row is cleared from the web settings
95// page instead. See the comment on expire(_:) in SessionStore.
96@Test @MainActor func expireDoesNotRunTheRemovalHook() async throws {
97 let (session, _) = try await twoAccounts()
98 let account = try #require(session.current)
99 var hookRan = false
100 session.willRemoveAccount = { _ in hookRan = true }
101
102 session.handle(GitbayError.unauthorized("invalid or expired token"))
103
104 #expect(hookRan == false)
105 #expect(session.accounts.contains(account) == false)
106}