// // JellyfinClientLibraryTests.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 the DTO-to-domain mapping that every browse surface depends on. @Suite struct JellyfinClientLibraryTests { /// A movie DTO exercising the wrapped enum, image tag, and user data payloads. private var movieResult: Components.Schemas.BaseItemDtoQueryResult { Components.Schemas.BaseItemDtoQueryResult( items: [ Components.Schemas.BaseItemDto( name: "Arrival", id: "i1", overview: "Linguist meets heptapods.", productionYear: 2016, _type: .init(value1: .movie), userData: .init(value1: Components.Schemas.UserItemDataDto(isFavorite: true, played: false)), imageTags: .init(additionalProperties: ["Primary": "abc", "NotAnImageType": "zzz"]), backdropImageTags: ["bd1"] ) ], totalRecordCount: 1, startIndex: 0 ) } /// Builds a client wired to a double. /// /// - 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")!), api: api ) } @Test("Maps an item, dropping image tags the client cannot name") func mapsItems() async throws { var api = MockJellyfinAPI() api.getItemsOutput = .ok(.init(body: .json(movieResult))) let client = makeClient(api) let page = try await client.items() #expect(page.totalRecordCount == 1) let item = try #require(page.items.first) #expect(item.id == "i1") #expect(item.name == "Arrival") #expect(item.kind == .movie) #expect(item.productionYear == 2016) #expect(item.imageTags == [.primary: "abc"]) #expect(item.backdropImageTags == ["bd1"]) #expect(item.userData?.isFavorite == true) #expect(item.userData?.played == false) } @Test("Every JSON profile the server may negotiate decodes identically") func jsonProfilesCollapse() async throws { var camelCase = MockJellyfinAPI() camelCase.getItemsOutput = .ok(.init(body: .applicationJsonProfile_Quot_camelcase_quot_(movieResult))) var pascalCase = MockJellyfinAPI() pascalCase.getItemsOutput = .ok(.init(body: .applicationJsonProfile_Quot_pascalcase_quot_(movieResult))) let fromCamelCase = try await makeClient(camelCase).items() let fromPascalCase = try await makeClient(pascalCase).items() #expect(fromCamelCase == fromPascalCase) #expect(fromCamelCase.items.first?.kind == .movie) } @Test("Maps libraries and drops unsupported collection types") func mapsLibraries() async throws { var api = MockJellyfinAPI() api.getUserViewsOutput = .ok( .init( body: .json( Components.Schemas.BaseItemDtoQueryResult( items: [ Components.Schemas.BaseItemDto( name: "Shows", id: "l1", childCount: 12, collectionType: .init(value1: .tvshows), imageTags: .init(additionalProperties: ["Primary": "tag1"]) ), Components.Schemas.BaseItemDto( name: "Music", id: "l2", collectionType: .init(value1: .music) ), ] ) ) ) ) let client = makeClient(api) let libraries = try await client.libraries() #expect(libraries.count == 2) #expect(libraries[0].collectionType == .tvShows) #expect(libraries[0].primaryImageTag == "tag1") #expect(libraries[0].childCount == 12) #expect(libraries[1].collectionType == nil) } @Test("A blank search term is rejected before any request is sent") func blankSearchTermRejected() async { let client = makeClient(MockJellyfinAPI()) await #expect(throws: JellyfinClientError.invalidArgument(name: "term")) { _ = try await client.searchHints(term: " ") } } @Test("An empty series identifier is rejected before any request is sent") func emptySeriesIdentifierRejected() async { let client = makeClient(MockJellyfinAPI()) await #expect(throws: JellyfinClientError.invalidArgument(name: "seriesID")) { _ = try await client.seasons(seriesID: "") } } }