luminate/Tests/LuminateTests/LaunchSessionTests.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")
}
}