gitbayTests/SessionClientTests.swift

v1.10.0
gitbay-ios/gitbayTests/SessionClientTests.swift history · blame · raw

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}