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() } } }