182 lines
6.6 KiB
Swift
182 lines
6.6 KiB
Swift
//
|
|
// LaunchSessionTests.swift
|
|
//
|
|
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
|
|
import Foundation
|
|
import LuminateCore
|
|
import LuminateUI
|
|
import Synchronization
|
|
import Testing
|
|
|
|
@testable import Luminate
|
|
|
|
/// Exercises the launch-time token verification gate in isolation from any real network call.
|
|
@Suite @MainActor struct LaunchSessionTests {
|
|
@Test("A launch with no stored token skips verification")
|
|
func noStoredTokenSkipsVerification() async {
|
|
let preferences = Preferences(store: EphemeralPreferenceStore())
|
|
let callCount = Mutex(0)
|
|
let session = LaunchSession(phase: .resolved, preferences: preferences) {
|
|
callCount.withLock { $0 += 1 }
|
|
}
|
|
|
|
#expect(session.phase == .resolved)
|
|
|
|
await session.verifyStoredToken()
|
|
|
|
#expect(callCount.withLock { $0 } == 0)
|
|
}
|
|
|
|
@Test("A valid token keeps the session and resolves")
|
|
func validTokenResolves() async {
|
|
let preferences = Preferences(store: EphemeralPreferenceStore())
|
|
preferences[.accessToken] = "tok"
|
|
let session = LaunchSession(phase: .verifying, preferences: preferences) {}
|
|
|
|
#expect(session.phase == .verifying)
|
|
|
|
await session.verifyStoredToken()
|
|
|
|
#expect(session.phase == .resolved)
|
|
#expect(preferences[.accessToken] == "tok")
|
|
}
|
|
|
|
@Test("A rejected token is cleared and returns the user to onboarding")
|
|
func rejectedTokenIsCleared() async {
|
|
let preferences = Preferences(store: EphemeralPreferenceStore())
|
|
preferences[.accessToken] = "tok"
|
|
let session = LaunchSession(phase: .verifying, preferences: preferences) {
|
|
throw JellyfinClientError.unauthorized
|
|
}
|
|
|
|
await session.verifyStoredToken()
|
|
|
|
#expect(session.phase == .resolved)
|
|
#expect(preferences[.accessToken] == nil)
|
|
}
|
|
|
|
@Test("An unreachable server keeps the token and offers a retry")
|
|
func unreachableServerOffersRetry() async {
|
|
let preferences = Preferences(store: EphemeralPreferenceStore())
|
|
preferences[.accessToken] = "tok"
|
|
let session = LaunchSession(phase: .verifying, preferences: preferences) {
|
|
throw URLError(.cannotConnectToHost)
|
|
}
|
|
|
|
await session.verifyStoredToken()
|
|
|
|
guard case .unreachable = session.phase else {
|
|
Issue.record("Expected .unreachable, got \(session.phase)")
|
|
return
|
|
}
|
|
#expect(session.failureMessage != nil)
|
|
#expect(preferences[.accessToken] == "tok")
|
|
}
|
|
|
|
@Test("Retrying after an outage re-runs verification")
|
|
func retryReRunsVerification() async {
|
|
let preferences = Preferences(store: EphemeralPreferenceStore())
|
|
preferences[.accessToken] = "tok"
|
|
let callCount = Mutex(0)
|
|
let session = LaunchSession(phase: .verifying, preferences: preferences) {
|
|
let attempt = callCount.withLock { count -> Int in
|
|
count += 1
|
|
return count
|
|
}
|
|
if attempt == 1 { throw URLError(.cannotConnectToHost) }
|
|
}
|
|
|
|
await session.verifyStoredToken()
|
|
guard case .unreachable = session.phase else {
|
|
Issue.record("Expected .unreachable after the first attempt, got \(session.phase)")
|
|
return
|
|
}
|
|
|
|
await session.retry()
|
|
|
|
#expect(session.phase == .resolved)
|
|
#expect(preferences[.accessToken] == "tok")
|
|
}
|
|
|
|
@Test("Signing out from the retry screen clears the token")
|
|
func signOutFromRetryScreenClearsToken() async {
|
|
let preferences = Preferences(store: EphemeralPreferenceStore())
|
|
preferences[.accessToken] = "tok"
|
|
let session = LaunchSession(phase: .verifying, preferences: preferences) {
|
|
throw URLError(.cannotConnectToHost)
|
|
}
|
|
|
|
await session.verifyStoredToken()
|
|
guard case .unreachable = session.phase else {
|
|
Issue.record("Expected .unreachable, got \(session.phase)")
|
|
return
|
|
}
|
|
|
|
session.signOut()
|
|
|
|
#expect(session.phase == .resolved)
|
|
#expect(preferences[.accessToken] == nil)
|
|
}
|
|
|
|
@Test("Launching with no stored token skips the probe entirely")
|
|
func launchWithNoStoredTokenSkipsProbe() {
|
|
let preferences = Preferences(store: EphemeralPreferenceStore())
|
|
let callCount = Mutex(0)
|
|
let session = LaunchSession.launch(preferences: preferences) {
|
|
callCount.withLock { $0 += 1 }
|
|
}
|
|
#expect(session.phase == .resolved)
|
|
#expect(callCount.withLock { $0 } == 0)
|
|
}
|
|
|
|
@Test("Launching with a valid token resolves synchronously")
|
|
func launchWithValidTokenResolves() {
|
|
let preferences = Preferences(store: EphemeralPreferenceStore())
|
|
preferences[.accessToken] = "tok"
|
|
let session = LaunchSession.launch(preferences: preferences) {}
|
|
#expect(session.phase == .resolved)
|
|
#expect(preferences[.accessToken] == "tok")
|
|
}
|
|
|
|
@Test("Launching with a rejected token clears it synchronously")
|
|
func launchWithRejectedTokenClearsIt() {
|
|
let preferences = Preferences(store: EphemeralPreferenceStore())
|
|
preferences[.accessToken] = "tok"
|
|
let session = LaunchSession.launch(preferences: preferences) {
|
|
throw JellyfinClientError.unauthorized
|
|
}
|
|
#expect(session.phase == .resolved)
|
|
#expect(preferences[.accessToken] == nil)
|
|
}
|
|
|
|
@Test("Launching against an unreachable server resolves to a retry phase")
|
|
func launchAgainstUnreachableServerResolvesToRetry() {
|
|
let preferences = Preferences(store: EphemeralPreferenceStore())
|
|
preferences[.accessToken] = "tok"
|
|
let session = LaunchSession.launch(preferences: preferences) {
|
|
throw URLError(.cannotConnectToHost)
|
|
}
|
|
guard case .unreachable = session.phase else {
|
|
Issue.record("Expected .unreachable, got \(session.phase)")
|
|
return
|
|
}
|
|
#expect(preferences[.accessToken] == "tok")
|
|
}
|
|
}
|