162 lines
6 KiB
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)
|
|
}
|
|
}
|