113 lines
4.9 KiB
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
|