„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>
151 lines
6.1 KiB
Swift
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()
|
|
}
|
|
}
|
|
}
|