// // ServerConnectionProbeTests.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 LuminateCore import OpenAPIRuntime import Testing @testable import LuminateServices /// Records probe candidates without introducing shared mutable state into test closures. private actor ProbeRecorder { private(set) var urls: [URL] = [] func record(_ url: URL) { urls.append(url) } } /// Exercises HTTPS preference, transport fallback, and server identity classification. @Suite struct ServerConnectionProbeTests { private let httpsURL = URL(string: "https://jellyfin.test/")! private let httpURL = URL(string: "http://jellyfin.test/")! @Test("Uses the HTTPS candidate when it answers") func usesHTTPS() async throws { let address = try makeAddress() let probe = ServerConnectionProbe { url in #expect(url == self.httpsURL) return JellyfinServerInfo(serverName: "Home", productName: "Jellyfin Server") } let connection = try await probe.connect(to: address) #expect(connection.url == httpsURL) #expect(connection.isEncrypted) } @Test("Falls back to HTTP after an HTTPS transport failure") func fallsBackAfterTransportFailure() async throws { let address = try makeAddress() let recorder = ProbeRecorder() let probe = ServerConnectionProbe { url in await recorder.record(url) if url.scheme == "https" { throw URLError(.cannotConnectToHost) } return JellyfinServerInfo(productName: "Jellyfin Server") } let connection = try await probe.connect(to: address) #expect(connection.url == httpURL) #expect(!connection.isEncrypted) #expect(await recorder.urls == [httpsURL, httpURL]) } @Test("Does not fall back when HTTPS answers with an error status") func doesNotFallbackAfterAnswer() async throws { let address = try makeAddress() let recorder = ProbeRecorder() let probe = ServerConnectionProbe { url in await recorder.record(url) throw JellyfinClientError.unauthorized } do { _ = try await probe.connect(to: address) Issue.record("Expected the answered HTTPS candidate to be rejected") } catch let error as ServerConnectionError { guard case .rejected(let url, let detail) = error else { Issue.record("Expected a rejected connection, got \(error)") return } #expect(url == httpsURL) #expect(!detail.isEmpty) } #expect(await recorder.urls == [httpsURL]) } @Test("Unwraps an OpenAPI ClientError to reach the URLError") func unwrapsClientError() async throws { let address = try makeAddress() let recorder = ProbeRecorder() let probe = ServerConnectionProbe { url in await recorder.record(url) if url.scheme == "https" { throw ClientError( operationID: "GetPublicSystemInfo", operationInput: "test", causeDescription: "transport failed", underlyingError: URLError(.timedOut) ) } return JellyfinServerInfo(productName: "Jellyfin Server") } let connection = try await probe.connect(to: address) #expect(connection.url == httpURL) #expect(await recorder.urls == [httpsURL, httpURL]) } @Test("Rejects a reachable server that is not Jellyfin") func rejectsNonJellyfin() async throws { let address = try makeAddress() do { let probe = ServerConnectionProbe { _ in JellyfinServerInfo(productName: "Emby Server") } _ = try await probe.connect(to: address) Issue.record("Expected a non-Jellyfin identity to be rejected") } catch let error as ServerConnectionError { #expect(error == .notJellyfin(url: httpsURL, productName: "Emby Server")) } } @Test("Accepts a server that omits its product name") func acceptsMissingProductName() async throws { let address = try makeAddress() let probe = ServerConnectionProbe { _ in JellyfinServerInfo(serverName: "Proxy") } let connection = try await probe.connect(to: address) #expect(connection.serverInfo.serverName == "Proxy") } @Test("Reports every attempted URL when nothing answers") func reportsUnreachableCandidates() async throws { let address = try makeAddress() let recorder = ProbeRecorder() let probe = ServerConnectionProbe { url in await recorder.record(url) throw URLError(.cannotConnectToHost) } do { _ = try await probe.connect(to: address) Issue.record("Expected all candidates to be reported as unreachable") } catch let error as ServerConnectionError { #expect(error == .unreachable(attempted: [httpsURL, httpURL])) } #expect(await recorder.urls == [httpsURL, httpURL]) } private func makeAddress() throws -> ServerAddress { try ServerAddress.normalize(address: "jellyfin.test", port: "", policy: .automatic) } }