Files
ScarriffleandClaude Opus 5 693a6cc867 Cookie heisst Cookie
„Keks", „Keksglas", „Kekse" — eingedeutscht, wo es nichts zu
verdeutschen gibt. Ein Cookie ist ein Cookie; wer danach sucht, sucht
nicht nach Keksen, und wer den Quelltext liest, liest daneben die
Schnittstelle, in der es `HTTPCookieStorage` heisst.

Betrifft Kommentare und Prüfungsnamen, kein Verhalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 20:42:29 +02:00

151 lines
6.1 KiB
Swift

import Foundation
import Testing
import MediaKit
@testable import FlaroServerKit
/// Adressen und Zugang — alles, was ohne Netz entsteht.
@Suite("Flaro: Adressen und Zugang")
struct AdressenTests {
private func backend(_ transport: Attrappe, cookieglas: Cookieglas? = nil) -> FlaroBackend {
FlaroBackend(
client: FlaroClient(
baseURL: Proben.adresse, identity: Proben.kennung, token: "geheim-123",
transport: transport, cookieJar: cookieglas),
account: FlaroAccount(
baseURL: Proben.adresse, serverName: "Wohnzimmer",
userID: "u1", userName: "guido", isAdministrator: true))
}
/// **Kein Token in der Adresse — das ist der Unterschied zu Jellyfin.**
///
/// Dort steckt der Zugang als `?ApiKey=` in jeder Bildadresse, weil eine
/// Bildansicht keine Kopfzeilen schickt. Er steht damit in jedem
/// Zugriffsprotokoll, in jedem Verlauf und in jedem weitergegebenen Link.
/// Flaro geht über ein Cookie.
@Test("Bildadressen tragen keinen Zugang")
func bildAdresseOhneToken() {
let url = backend(Attrappe(rumpf: "{}")).rawImageURL(path: "abc123")
#expect(url?.path == "/api/v1/images/abc123")
#expect(url?.query == nil)
#expect(url?.absoluteString.contains("geheim") == false)
}
@Test("Ein Zugang wird als Cookie abgelegt")
func tokenWirdZuCookie() async {
let glas = Cookieglas()
_ = FlaroClient(
baseURL: Proben.adresse, identity: Proben.kennung, token: "geheim-123",
transport: Attrappe(rumpf: "{}"), cookieJar: glas)
let cookie = glas.cookies.first
#expect(cookie?.name == "flaro_session")
#expect(cookie?.value == "geheim-123")
#expect(cookie?.path == "/")
#expect(cookie?.domain.hasSuffix("nas") == true)
}
/// **Nach einem Neustart meldet sich niemand neu an.**
///
/// Die App hat dann nur das Token aus dem Schlüsselbund. Käme das Cookie
/// allein aus der Antwort des Servers, wäre keines da — und jedes Plakat
/// bliebe grau, obwohl der Zugang gilt.
@Test("Auch ein wiederaufgenommenes Token legt das Cookie an")
func cookieAuchOhneAnmeldung() async throws {
let glas = Cookieglas()
let transport = Attrappe([
("/api/v1/auth/me", 200, #"{"id":"u1","userName":"guido","isAdministrator":false}"#),
("/api/v1/system/info", 200, Proben.info),
])
_ = try await FlaroSignIn.restore(
baseURL: Proben.adresse, token: "geheim-123",
identity: Proben.kennung, transport: transport, cookieJar: glas)
#expect(glas.cookies.contains { $0.name == "flaro_session" && $0.value == "geheim-123" })
}
@Test("Die Download-Adresse zeigt auf die Datei, nicht auf den Titel")
func downloadAdresse() {
let url = backend(Attrappe(rumpf: "{}")).downloadURL(
for: MediaFile(id: "datei-1", itemID: "film-1"))
#expect(url?.path == "/api/v1/files/datei-1/download")
}
/// **Der Vertrag fragt nach einer Tonspur, Flaro braucht keine.**
///
/// `remuxDownloadURL` gibt es nur, weil Jellyfins Transcoder genau eine
/// Tonspur je Sitzung abbildet. Hier wäre eine Adresse mit Spurnummer
/// eine Lüge über das, was herauskommt — also gibt es keine, und die
/// Oberfläche erkennt an der Fähigkeit, dass sie nicht fragen muss.
@Test("Kein Umpacken mit Spurwahl — stattdessen die Fähigkeit")
func alleSpuren() {
let b = backend(Attrappe(rumpf: "{}"))
#expect(b.capabilities.contains(.allTracksDownload))
#expect(b.remuxDownloadURL(
itemID: "film-1", versionIndex: 0,
audioStreamIndex: 1, subtitleStreamIndex: nil) == nil)
}
/// Zwei Server dürfen „Medien" heissen; derselbe darf umbenannt werden.
@Test("Die Kennung hängt an der Adresse, nicht am Namen")
func kennung() {
#expect(backend(Attrappe(rumpf: "{}")).identity == "flaro:http://nas:8080")
}
@Test("Anmelden schickt Gerätename und Kennung mit")
func anmeldung() async throws {
let transport = Attrappe([
("/api/v1/system/info", 200, Proben.info),
("/api/v1/auth/login", 200, Proben.anmeldung),
])
let (token, konto, _) = try await FlaroSignIn.signIn(
baseURL: Proben.adresse, userName: "guido", password: "geheim",
identity: Proben.kennung, transport: transport, cookieJar: Cookieglas())
let rumpf = try #require(transport.rumpf(von: "/api/v1/auth/login"))
#expect(token == "geheim-123")
#expect(konto.userName == "guido")
#expect(konto.isAdministrator)
#expect(konto.serverName == "Wohnzimmer")
#expect(rumpf["deviceName"] as? String == "Prüfgerät")
#expect(rumpf["deviceId"] as? String == "geraet-1")
}
/// **Ein frischer Server ist kein falsches Passwort.**
@Test("Ein nicht eingerichteter Server sagt das")
func nochNichtEingerichtet() async throws {
let transport = Attrappe(rumpf: #"{"version":"0.1.0","serverName":null,"needsSetup":true}"#)
let gruss = try await FlaroSignIn.greet(
baseURL: Proben.adresse, identity: Proben.kennung, transport: transport)
#expect(gruss.needsSetup)
}
/// **Flaro begründet seine Absagen, und der Satz zählt mehr als die Zahl.**
@Test("Eine Absage mit Begründung kommt als Begründung an")
func begruendeteAbsage() async {
let transport = Attrappe(rumpf: #"{"detail":"Für diese Bibliothek läuft bereits ein Scan."}"#, status: 409)
let b = backend(transport)
await #expect(throws: MediaBackendError.refused(
reason: "Für diese Bibliothek läuft bereits ein Scan.")) {
try await b.refreshLibrary(MediaLibrary(id: "b1", title: "Filme", kind: .movie))
}
}
@Test("Ein abgelaufener Zugang führt zur Abmeldung, nicht zu einer Meldung")
func abgelaufen() async {
let b = backend(Attrappe(rumpf: "", status: 401))
await #expect(throws: MediaBackendError.unauthorized) {
_ = try await b.libraries()
}
}
}