// // ServerAddressTests.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 Testing @testable import LuminateCore /// Covers normalization, validation, and candidate selection for onboarding server addresses. @Suite struct ServerAddressTests { @Test("Normalizes a bare host and permits HTTPS fallback") func bareHost() throws { let address = try ServerAddress.normalize(address: " jellyfin.example.com ", port: "", policy: .automatic) #expect(address.baseURL.absoluteString == "https://jellyfin.example.com/") #expect( address.probeCandidates.map(\.absoluteString) == [ "https://jellyfin.example.com/", "http://jellyfin.example.com/", ]) } @Test("Preserves a custom port on a bare host") func customPort() throws { let address = try ServerAddress.normalize(address: "jellyfin.example.com", port: "8096", policy: .automatic) #expect(address.baseURL.absoluteString == "https://jellyfin.example.com:8096/") } @Test("Honors an explicit HTTP scheme without fallback") func explicitHTTP() throws { let address = try ServerAddress.normalize( address: "http://192.168.1.20:8096", port: "", policy: .automatic ) #expect(address.baseURL.absoluteString == "http://192.168.1.20:8096/") #expect(address.probeCandidates.map(\.absoluteString) == ["http://192.168.1.20:8096/"]) } @Test("Force HTTPS preserves an explicit nonstandard port") func forceHTTPSPreservesPort() throws { let address = try ServerAddress.normalize( address: "http://192.168.1.20:8096", port: "", policy: .httpsOnly ) #expect(address.baseURL.absoluteString == "https://192.168.1.20:8096/") #expect(address.probeCandidates.map(\.absoluteString) == ["https://192.168.1.20:8096/"]) } @Test("Lowercases an explicit HTTPS scheme without changing its host") func lowercasesScheme() throws { let address = try ServerAddress.normalize(address: "HTTPS://Host.Example", port: "", policy: .automatic) #expect(address.baseURL.absoluteString == "https://Host.Example/") } @Test("Preserves a reverse proxy path prefix") func preservesPathPrefix() throws { let address = try ServerAddress.normalize( address: "https://example.com/jellyfin", port: "", policy: .automatic ) #expect(address.baseURL.absoluteString == "https://example.com/jellyfin/") } @Test("Strips a redundant HTTPS default port") func stripsDefaultPort() throws { let address = try ServerAddress.normalize(address: "https://example.com:443", port: "", policy: .automatic) #expect(address.baseURL.absoluteString == "https://example.com/") } @Test("Preserves IPv6 literal brackets") func preservesIPv6() throws { let address = try ServerAddress.normalize( address: "http://[2001:db8::10]:8096", port: "", policy: .automatic ) #expect(address.baseURL.absoluteString == "http://[2001:db8::10]:8096/") } @Test("Force HTTPS disables fallback for a bare host") func forceHTTPSDisablesFallback() throws { let address = try ServerAddress.normalize(address: "jellyfin.example.com", port: "", policy: .httpsOnly) #expect(address.probeCandidates.map(\.absoluteString) == ["https://jellyfin.example.com/"]) } @Test("Rejects an empty address") func emptyAddress() { #expect(throws: ServerAddressError.emptyAddress) { _ = try ServerAddress.normalize(address: "", port: "", policy: .automatic) } } @Test("Rejects an unsupported scheme") func unsupportedScheme() { #expect(throws: ServerAddressError.unsupportedScheme("ftp")) { _ = try ServerAddress.normalize(address: "ftp://host", port: "", policy: .automatic) } } @Test("Rejects conflicting port fields") func conflictingPorts() { #expect(throws: ServerAddressError.conflictingPorts) { _ = try ServerAddress.normalize(address: "https://host:8920", port: "8096", policy: .automatic) } } @Test("Rejects ports outside the TCP range") func invalidPortRange() { #expect(throws: ServerAddressError.invalidPort("70000")) { _ = try ServerAddress.normalize(address: "host", port: "70000", policy: .automatic) } #expect(throws: ServerAddressError.invalidPort("0")) { _ = try ServerAddress.normalize(address: "host", port: "0", policy: .automatic) } } @Test("Rejects nonnumeric port text") func invalidPortText() { #expect(throws: ServerAddressError.invalidPort("80abc")) { _ = try ServerAddress.normalize(address: "host", port: "80abc", policy: .automatic) } } @Test("Rejects credentials in the server address") func rejectsCredentials() { #expect(throws: ServerAddressError.credentialsNotAllowed) { _ = try ServerAddress.normalize(address: "https://user:pw@host", port: "", policy: .automatic) } } @Test("Rejects query strings and fragments") func rejectsQueryAndFragment() { #expect(throws: ServerAddressError.queryOrFragmentNotAllowed) { _ = try ServerAddress.normalize(address: "https://host/?token=x", port: "", policy: .automatic) } #expect(throws: ServerAddressError.queryOrFragmentNotAllowed) { _ = try ServerAddress.normalize(address: "https://host/#login", port: "", policy: .automatic) } } @Test("Rejects an address with no host") func missingHost() { #expect(throws: ServerAddressError.missingHost) { _ = try ServerAddress.normalize(address: "https://", port: "", policy: .automatic) } } @Test("Explains invalid address input") func errorMessages() { #expect(ServerAddressError.emptyAddress.message == "Enter a server address") #expect(ServerAddressError.unsupportedScheme("ftp").message == "ftp is not supported. Use http or https") #expect( ServerAddressError.invalidPort("70000").message == "70000 is not a valid port. Enter a number from 1 to 65535") } }