// // JellyfinClientAuthTests.swift // // Copyright 2026 Brendan Szymanski // // 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 . // // SPDX-License-Identifier: GPL-3.0-or-later // import Foundation import LuminateAPI import LuminateCore import Testing @testable import LuminateServices /// Covers sign-in: argument validation, payload mapping, and token adoption. @Suite struct JellyfinClientAuthTests { /// Builds a client wired to a double, pointed at a URL that is never dialled. /// /// - Parameter api: The stubbed API. /// - Returns: A client that routes every call to `api`. private func makeClient(_ api: MockJellyfinAPI) -> JellyfinClient { JellyfinClient( configuration: JellyfinClientConfiguration( serverURL: URL(string: "http://localhost")!, deviceName: "test-device", deviceID: "test-device-id" ), api: api ) } @Test("A successful sign-in maps the result and adopts the token") func signInAdoptsToken() async throws { var api = MockJellyfinAPI() api.authenticateUserByNameOutput = .ok( .init( body: .json( Components.Schemas.AuthenticationResult( user: .init(value1: Components.Schemas.UserDto(name: "echo", id: "u1", hasPassword: true)), accessToken: "tok", serverId: "srv" ) ) ) ) let client = makeClient(api) let result = try await client.authenticate(username: "echo", password: "hunter2") #expect(result.accessToken == "tok") #expect(result.serverID == "srv") #expect(result.user?.name == "echo") #expect(result.user?.id == "u1") #expect(await client.accessToken == "tok") #expect(await client.isAuthenticated) } @Test("A success with no token is a failure, not an unauthenticated success") func missingTokenThrows() async { var api = MockJellyfinAPI() api.authenticateUserByNameOutput = .ok( .init(body: .json(Components.Schemas.AuthenticationResult(accessToken: nil))) ) let client = makeClient(api) await #expect(throws: JellyfinClientError.missingPayload(operation: "AuthenticateUserByName")) { _ = try await client.authenticate(username: "echo", password: "hunter2") } #expect(await client.isAuthenticated == false) } @Test("An empty username is rejected before any request is sent") func emptyUsernameRejected() async { let client = makeClient(MockJellyfinAPI()) await #expect(throws: JellyfinClientError.invalidArgument(name: "username")) { _ = try await client.authenticate(username: "", password: "hunter2") } } @Test("An empty Quick Connect secret is rejected before any request is sent") func emptySecretRejected() async { let client = makeClient(MockJellyfinAPI()) await #expect(throws: JellyfinClientError.invalidArgument(name: "secret")) { _ = try await client.quickConnectState(secret: "") } } @Test("Clearing the token signs the client out") func clearingTokenSignsOut() async { let client = makeClient(MockJellyfinAPI()) await client.setAccessToken("tok") #expect(await client.isAuthenticated) await client.setAccessToken("") #expect(await client.accessToken == nil) #expect(await client.isAuthenticated == false) } }