Files
Flaro/Packages/FlixyKit/Tests/PlexKitTests/ConnectionSelectorTests.swift
T
GuidoandClaude Opus 5 d0a84f35de Drei Fehler aus dem ersten Praxistest
Anmelde-Adresse wurde doppelt kodiert
  URLComponents.fragment kodiert beim Setzen erneut, aus %5B wurde
  %255B. Plex antwortete mit „Diese Anfrage konnte nicht abgeschlossen
  werden". Die Adresse wird jetzt von Hand zusammengesetzt.

Verbindungssuche hing bis zu anderthalb Minuten
  probeTimeout war deklariert, aber nirgends angewendet — es galt der
  URLSession-Standard von 60 Sekunden. Zusammen mit der Prüfung Rang
  für Rang kostete eine schweigende lokale Adresse die volle Zeit,
  bevor der direkte Weg überhaupt begann. Jetzt laufen alle Adressen
  gleichzeitig, mit Zeitgrenze, und die Rangordnung entscheidet unter
  den erreichbaren.

Kein Zurück während des Ladens
  Die Kopfzeile mit dem Schließen-Knopf wurde nur im Zustand .playing
  gezeichnet. Bei langsamer Verbindung saß man ohne Ausweg vor einem
  Ladekreis. Sie liegt jetzt als eigene Ebene darüber und erscheint
  auch beim Vorbereiten — samt Titel, damit erkennbar bleibt, worauf
  gewartet wird.

Sechs neue Tests, darunter drei, die die Zeitgrenze der
Verbindungssuche an einer schweigenden Adresse nachstellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:17:18 +02:00

258 lines
10 KiB
Swift

import Foundation
import Testing
@testable import PlexKit
@Suite("Verbindungsauswahl")
struct ConnectionSelectorTests {
private let serverID = "server-abc-123"
private func identityResponse(_ machineIdentifier: String) -> String {
#"{"MediaContainer":{"machineIdentifier":"\#(machineIdentifier)","version":"1.42.0"}}"#
}
private func resource(connections: [PlexConnection]) -> PlexResource {
PlexResource(
name: "Wohnzimmer",
clientIdentifier: serverID,
accessToken: "server-token",
connections: connections
)
}
@Test("Der lokale Weg gewinnt, auch wenn die Vermittlung erreichbar ist")
func localWinsOverRelay() async {
// Der eigentliche Zweck der Rangordnung: die Vermittlung von Plex
// antwortet im Zweifel schneller, ist aber gedrosselt. Ein Rennen ohne
// Rang würde ausgerechnet den langsamsten Weg krönen.
let transport = FakeTransport.routes(["/identity": identityResponse(serverID)])
let selector = PlexConnectionSelector(identity: .test, transport: transport)
let chosen = await selector.selectConnection(for: resource(connections: [
PlexConnection(uri: "https://relay.plex.direct:443", relay: true),
PlexConnection(uri: "http://192.168.1.10:32400", local: true),
PlexConnection(uri: "https://1-2-3-4.abc.plex.direct:32400"),
]))
#expect(chosen?.uri == "http://192.168.1.10:32400")
}
@Test("Der direkte Weg von außen kommt vor der Vermittlung")
func directRemoteBeatsRelay() async {
let transport = FakeTransport.routes(["/identity": identityResponse(serverID)])
let selector = PlexConnectionSelector(identity: .test, transport: transport)
let chosen = await selector.selectConnection(for: resource(connections: [
PlexConnection(uri: "https://relay.plex.direct:443", relay: true),
PlexConnection(uri: "https://1-2-3-4.abc.plex.direct:32400"),
]))
#expect(chosen?.uri == "https://1-2-3-4.abc.plex.direct:32400")
}
@Test("Ein Server mit fremder Kennung wird abgelehnt")
func rejectsWrongServer() async {
// Eine veraltete plex.direct-Adresse kann auf einen fremden Server
// zeigen. Ohne Abgleich der Kennung zeigte die App fremde Mediatheken.
let transport = FakeTransport.routes(["/identity": identityResponse("ein-anderer-server")])
let selector = PlexConnectionSelector(identity: .test, transport: transport)
let chosen = await selector.selectConnection(for: resource(connections: [
PlexConnection(uri: "http://192.168.1.10:32400", local: true),
]))
#expect(chosen == nil)
}
@Test("Ohne erreichbaren Weg gibt es kein Ergebnis")
func noReachableConnection() async {
let selector = PlexConnectionSelector(identity: .test, transport: FakeTransport.unreachable())
let chosen = await selector.selectConnection(for: resource(connections: [
PlexConnection(uri: "http://192.168.1.10:32400", local: true),
PlexConnection(uri: "https://relay.plex.direct:443", relay: true),
]))
#expect(chosen == nil)
}
@Test("Ohne Server-Token wird gar nicht erst probiert")
func missingAccessTokenFailsFast() async {
// Bei geteilten Servern liefert plex.tv gelegentlich keinen Token —
// etwa wenn die Freigabe zurückgezogen wurde. Anfragen ohne ihn
// beantwortet der Server wortlos mit 401.
let selector = PlexConnectionSelector(identity: .test, transport: FakeTransport.unreachable())
let resource = PlexResource(
name: "Geteilt",
clientIdentifier: serverID,
accessToken: nil,
connections: [PlexConnection(uri: "http://192.168.1.10:32400", local: true)]
)
#expect(await selector.selectConnection(for: resource) == nil)
}
@Test("Die Rangordnung der Wege stimmt")
func tierOrdering() {
#expect(PlexConnection(uri: "x", local: true).tier == 0)
#expect(PlexConnection(uri: "x").tier == 1)
#expect(PlexConnection(uri: "x", relay: true).tier == 2)
}
}
@Suite("Serverliste")
struct ServerDirectoryTests {
private let resourcesJSON = """
[
{
"name": "Wohnzimmer",
"product": "Plex Media Server",
"clientIdentifier": "server-abc",
"provides": "server",
"owned": true,
"accessToken": "token-eigen",
"connections": [
{ "uri": "http://192.168.1.10:32400", "address": "192.168.1.10", "port": 32400, "local": true, "relay": false, "protocol": "http", "IPv6": false },
{ "uri": "https://1-2-3-4.abc.plex.direct:32400", "local": false, "relay": false, "protocol": "https" }
]
},
{
"name": "iPhone von Guido",
"clientIdentifier": "client-xyz",
"provides": "client,player",
"owned": true,
"connections": []
},
{
"name": "Server eines Freundes",
"clientIdentifier": "server-def",
"provides": "server",
"owned": false,
"accessToken": "token-geteilt",
"connections": [
{ "uri": "https://5-6-7-8.def.plex.direct:32400", "local": false, "relay": false }
]
}
]
"""
@Test("Nur Server erscheinen in der Liste, keine Abspielgeräte")
func filtersOutNonServers() async throws {
// Das Konto listet auch jedes iPhone und jeden Fernseher, der je
// angemeldet war. Ungefiltert stünden sie alle in der Serverauswahl.
let directory = PlexServerDirectory(
identity: .test,
token: "konto-token",
transport: FakeTransport.routes(["/api/v2/resources": resourcesJSON])
)
let servers = try await directory.servers()
#expect(servers.count == 2)
#expect(!servers.contains { $0.clientIdentifier == "client-xyz" })
}
@Test("Geteilte Server behalten ihren eigenen Token")
func sharedServersKeepTheirOwnToken() async throws {
let directory = PlexServerDirectory(
identity: .test,
token: "konto-token",
transport: FakeTransport.routes(["/api/v2/resources": resourcesJSON])
)
let servers = try await directory.servers()
let shared = try #require(servers.first { !$0.owned })
#expect(shared.accessToken == "token-geteilt")
#expect(shared.accessToken != "konto-token")
}
}
@Suite("Verbindungsauswahl unter Zeitdruck")
struct ConnectionSelectorTimeoutTests {
private let serverID = "server-abc-123"
private func identityResponse(_ machineIdentifier: String) -> String {
#"{"MediaContainer":{"machineIdentifier":"\#(machineIdentifier)","version":"1.42.0"}}"#
}
private var resource: PlexResource {
PlexResource(
name: "Wohnzimmer",
clientIdentifier: serverID,
accessToken: "server-token",
connections: [
PlexConnection(uri: "http://192.168.1.10:32400", local: true),
PlexConnection(uri: "https://1-2-3-4.abc.plex.direct:32400"),
PlexConnection(uri: "https://relay.plex.direct:443", relay: true),
]
)
}
@Test("Eine schweigende lokale Adresse blockiert die Anmeldung nicht", .timeLimit(.minutes(1)))
func hangingLocalDoesNotBlock() async {
// Der Fehler aus der Praxis: die Ränge wurden nacheinander geprüft,
// also kostete eine schweigende lokale Adresse erst die volle
// Zeitgrenze, bevor der direkte Weg überhaupt begann. Ergebnis war
// eine Anmeldung, die anderthalb Minuten hing.
let transport = FakeTransport.hosts(
answering: [
"1-2-3-4.abc.plex.direct": identityResponse(serverID),
"relay.plex.direct": identityResponse(serverID),
],
hanging: ["192.168.1.10"]
)
let selector = PlexConnectionSelector(
identity: .test,
transport: transport,
probeTimeout: .milliseconds(200)
)
let started = ContinuousClock.now
let chosen = await selector.selectConnection(for: resource)
let elapsed = ContinuousClock.now - started
// Der direkte Weg gewinnt, weil lokal nicht antwortet.
#expect(chosen?.uri == "https://1-2-3-4.abc.plex.direct:32400")
// Und zwar in der Größenordnung der Zeitgrenze, nicht in deren
// Vielfachem.
#expect(elapsed < .seconds(2), "dauerte \(elapsed)")
}
@Test("Die Vermittlung kommt nur zum Zug, wenn sonst nichts geht", .timeLimit(.minutes(1)))
func relayOnlyWhenNothingElseAnswers() async {
let transport = FakeTransport.hosts(
answering: ["relay.plex.direct": identityResponse(serverID)],
hanging: ["192.168.1.10"]
)
let selector = PlexConnectionSelector(
identity: .test,
transport: transport,
probeTimeout: .milliseconds(200)
)
let chosen = await selector.selectConnection(for: resource)
#expect(chosen?.relay == true)
}
@Test("Eine erreichbare lokale Adresse gewinnt sofort", .timeLimit(.minutes(1)))
func reachableLocalWinsImmediately() async {
// Hier hängen die beiden anderen. Würde die Auswahl auf alle Antworten
// warten, bevor sie entscheidet, wäre auch der gute Fall langsam.
let transport = FakeTransport.hosts(
answering: ["192.168.1.10": identityResponse(serverID)],
hanging: ["1-2-3-4.abc.plex.direct", "relay.plex.direct"]
)
let selector = PlexConnectionSelector(
identity: .test,
transport: transport,
probeTimeout: .seconds(30)
)
let started = ContinuousClock.now
let chosen = await selector.selectConnection(for: resource)
let elapsed = ContinuousClock.now - started
#expect(chosen?.local == true)
#expect(elapsed < .seconds(2), "dauerte \(elapsed)")
}
}