luminate/Tests/LuminateTests/ClientSessionBinderTests.swift

113 lines
4.9 KiB
Swift

//
// ClientSessionBinderTests.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
@_spi(SGTKInternal) import Gtk
import LuminateCore
import LuminateServices
import LuminateUI
import Testing
@testable import Luminate
/// Exercises the wiring between session preferences and the live Jellyfin client.
@Suite(.serialized) @MainActor struct ClientSessionBinderTests {
@Test("Writing the server URL preference reconfigures the client")
func serverURLPreferenceReconfiguresClient() async {
guard Gtk.initCheck() else { return }
let preferences = Preferences(store: EphemeralPreferenceStore())
let client = JellyfinClient(configuration: JellyfinClientConfiguration(serverURL: nil))
let binder = ClientSessionBinder(client: client, preferences: preferences)
let expected = URL(string: "http://jellyfin.test:8096")!
preferences[.serverURL] = expected
await asyncPump { await client.serverURL == expected }
#expect(await client.serverURL == expected)
withExtendedLifetime(binder) {}
}
@Test("Writing and clearing the access token updates authentication state")
func accessTokenPreferenceUpdatesAuthenticationState() async {
guard Gtk.initCheck() else { return }
let preferences = Preferences(store: EphemeralPreferenceStore())
let client = JellyfinClient(configuration: JellyfinClientConfiguration(serverURL: nil))
let binder = ClientSessionBinder(client: client, preferences: preferences)
preferences[.accessToken] = "tok"
await asyncPump { await client.accessToken == "tok" }
#expect(await client.accessToken == "tok")
#expect(await client.isAuthenticated)
preferences[.accessToken] = nil
await asyncPump { await client.accessToken == nil }
#expect(await client.accessToken == nil)
#expect(await client.isAuthenticated == false)
withExtendedLifetime(binder) {}
}
@Test("A nil server URL write is ignored")
func nilServerURLWriteIsIgnored() async {
guard Gtk.initCheck() else { return }
let preferences = Preferences(store: EphemeralPreferenceStore())
let client = JellyfinClient(configuration: JellyfinClientConfiguration(serverURL: nil))
let binder = ClientSessionBinder(client: client, preferences: preferences)
let established = URL(string: "http://jellyfin.test:8096")!
preferences[.serverURL] = established
await asyncPump { await client.serverURL == established }
#expect(await client.serverURL == established)
preferences[.serverURL] = nil
await asyncPump(turns: 50) { false }
#expect(await client.serverURL == established)
withExtendedLifetime(binder) {}
}
/// Drives the GLib default main context, yielding the task each turn, until `condition`
/// reports true or `turns` is exhausted.
///
/// Two independent async hops must both complete before the client actor reflects a
/// preference write: Portico's idle-deferred Observation flush (which invokes the binder's
/// subscription closure) and the `Task { await client.set... }` that closure spawns. A single
/// main-context pump only satisfies the first hop, so this loops - repeating both the GLib
/// pump and `Task.yield()` - and lets `condition` `await` the actor's state directly, since a
/// synchronous condition cannot observe it. Mirrors
/// `Tests/LuminateUITests/PreferenceTests.swift`'s `asyncPump`, which exists for the same
/// swift-testing-does-not-run-@MainActor-bodies-on-the-initial-thread reason.
private func asyncPump(turns: Int = 500, until condition: () async -> Bool) async {
for _ in 0..<turns {
if await condition() { return }
_ = clientSessionBinder_g_main_context_iteration(nil, 0)
await _Concurrency.Task.yield()
}
}
}
// UPSTREAM: see Tests/LuminateUITests/PreferenceTests.swift - Portico should expose an async
// main-loop pump for tests so suites do not need @_silgen_name.
@_silgen_name("g_main_context_iteration")
private nonisolated func clientSessionBinder_g_main_context_iteration(
_ context: UnsafeMutableRawPointer?,
_ mayBlock: Int32
) -> Int32