luminate/Tests/LuminateServicesTests/ServerConnectionProbeTests.swift

162 lines
6 KiB
Swift

//
// ServerConnectionProbeTests.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
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)
}
}