import Foundation @testable import FlaroServerKit /// Ein Transport, der vorbereitete Antworten gibt und mitschreibt. /// /// Dieselbe Trennung wie bei Jellyfin: Prüfungen brauchen keinen Server, /// sondern eine Antwort und die Frage, die gestellt wurde. final class Attrappe: FlaroTransport, @unchecked Sendable { /// Antworten je Pfad. Der erste Treffer gewinnt — so kann eine Prüfung /// `/api/v1/movies` und `/api/v1/movies/{id}` getrennt bedienen. private let antworten: [(pfad: String, status: Int, rumpf: String)] private let sperre = NSLock() private var protokoll: [URLRequest] = [] init(_ antworten: [(pfad: String, status: Int, rumpf: String)]) { self.antworten = antworten } convenience init(rumpf: String, status: Int = 200) { self.init([("", status, rumpf)]) } var anfragen: [URLRequest] { sperre.withLock { protokoll } } var letzte: URLRequest? { anfragen.last } func anfrage(auf pfad: String) -> URLRequest? { anfragen.last { $0.url?.path == pfad } } func rumpf(von pfad: String) -> [String: Any]? { guard let daten = anfrage(auf: pfad)?.httpBody else { return nil } return try? JSONSerialization.jsonObject(with: daten) as? [String: Any] } func data(for request: URLRequest) async throws -> (Data, URLResponse) { sperre.withLock { protokoll.append(request) } let pfad = request.url?.path ?? "" let treffer = antworten.first { $0.pfad.isEmpty || pfad.hasSuffix($0.pfad) || pfad == $0.pfad } ?? ("", 404, "") return ( Data(treffer.rumpf.utf8), HTTPURLResponse(url: request.url!, statusCode: treffer.status, httpVersion: nil, headerFields: nil)! ) } } /// Ein Cookieglas, das nur mitschreibt. /// /// Der Speicher des Systems anzufassen hiesse, dass eine Prüfung die nächste /// beeinflusst — und auf einem Entwicklerrechner auch den Browser. final class Cookieglas: FlaroCookieJar, @unchecked Sendable { private let sperre = NSLock() private var inhalt: [HTTPCookie] = [] var cookies: [HTTPCookie] { sperre.withLock { inhalt } } func store(_ cookie: HTTPCookie) { sperre.withLock { inhalt.append(cookie) } } func removeCookie(named name: String, host: String) { sperre.withLock { inhalt.removeAll { $0.name == name } } } } enum Proben { static let adresse = URL(string: "http://nas:8080")! static let kennung = FlaroIdentity(deviceName: "Prüfgerät", deviceID: "geraet-1") /// Ein Film mit zwei Tonspuren — der Fall, um den es geht. static let film = """ { "id": "film-1", "title": "Der Untergang", "year": 2004, "overview": "Berlin, April 1945.", "posterImage": "aaa", "backdropImage": "bbb", "files": [{ "id": "datei-1", "fileName": "Der Untergang (2004).mkv", "sizeBytes": 9000000000, "container": "matroska", "durationSeconds": 9360.0, "streams": [ {"index":0,"kind":"Video","codec":"h264","language":null,"title":null, "isDefault":true,"isForced":false,"channels":null,"width":1920,"height":1080}, {"index":1,"kind":"Audio","codec":"ac3","language":"deu","title":"Deutsch", "isDefault":true,"isForced":false,"channels":6,"width":null,"height":null}, {"index":2,"kind":"Audio","codec":"ac3","language":"deu","title":"Audiodeskription", "isDefault":false,"isForced":false,"channels":2,"width":null,"height":null}, {"index":3,"kind":"Subtitle","codec":"subrip","language":"deu","title":"Deutsch", "isDefault":false,"isForced":false,"channels":null,"width":null,"height":null} ] }], "state": {"positionSeconds":300.0,"runtimeSeconds":9360.0,"watched":false, "playCount":0,"favourite":false,"rating":null,"dismissed":false, "lastPlayedAt":"2026-10-03T19:25:04.5209555+00:00"} } """ static let info = """ {"version":"0.1.0","serverName":"Wohnzimmer","needsSetup":false, "attributions":[{"text":"This product uses TMDB","logoUrl":"https://x/t.svg","url":"https://x"}]} """ static let anmeldung = """ {"token":"geheim-123","user":{"id":"u1","userName":"guido","isAdministrator":true}} """ }