Ein frischer JSONDecoder, drei Zeilen neben der Warnung davor

Im Web stand am Abspann „Nächste Folge", in der App nur „Abspann
überspringen". Der Server war es nicht: beide fragen denselben Endpunkt,
und der ordnet nach Staffel und dann nach Nummer — er findet also auch
die erste Folge der nächsten Staffel.

Es war `nachbar`. Die Stelle baute sich einen frischen `JSONDecoder()`,
und der kennt das Datumsformat des Servers nicht. Solange die
Nachbarfolge weder `publishAt` noch `lastPlayedAt` mitbrachte, ging es
gut — mit eingeschalteter Prüfung bringt sie ein `publishAt`. Das `try?`
schluckte den Fehler, und von aussen sah es aus, als gäbe es keine
nächste Folge: kein Knopf, kein Hinweis, nichts.

In `FlaroClient` steht seit je ein eingerichteter Dekoder, ausdrücklich
nicht privat, „damit die Prüfungen denselben Dekoder benutzen" — und
direkt darunter die Warnung, dass ein frischer an genau diesem Format
scheitert. Diese eine Stelle las sie nicht.

Vier Prüfungen dazu, darunter die mit Datumsangaben: mit dem alten
Dekoder ist sie rot, mit dem neuen grün. Gegengeprüft, nicht behauptet.

Die übrigen frischen Dekoder im Quelltext lesen nur, was dieselbe App
vorher selbst geschrieben hat — Zwischenspeicher, Abzüge, Offline-
Angaben. Die laufen rund und bleiben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-14 08:39:31 +02:00
co-authored by Claude Opus 5
parent ce4da992ea
commit 93f4dfd8e9
2 changed files with 108 additions and 1 deletions
@@ -0,0 +1,100 @@
import Foundation
import Testing
import MediaKit
@testable import FlaroServerKit
/// Die Folge nach dieser — über die Staffelgrenze hinweg.
///
/// **Der Anlass.** Im Web stand am Abspann „Nächste Folge", in der App nur
/// „Abspann überspringen". Der Server war es nicht: beide fragen denselben
/// Endpunkt, und er ordnet nach Staffel und dann nach Nummer, findet also
/// auch die erste Folge der nächsten Staffel.
///
/// **Es war der Dekoder.** `nachbar` baute sich einen frischen
/// `JSONDecoder()`, und der kennt das Datumsformat des Servers nicht.
/// Solange die Nachbarfolge weder `publishAt` noch `lastPlayedAt` hatte,
/// ging es gut — mit eingeschalteter Prüfung hat sie ein `publishAt`. Das
/// `try?` schluckte den Fehler, und von aussen sah es aus, als gäbe es
/// keine nächste Folge.
@Suite("Flaro: Nachbarfolge")
struct NachbarfolgeTests {
private func backend(_ transport: any FlaroTransport) -> FlaroBackend {
FlaroBackend(
client: FlaroClient(
baseURL: Proben.adresse, identity: Proben.kennung, token: "t",
transport: transport, cookieJar: nil),
account: FlaroAccount(
baseURL: Proben.adresse, serverName: "Wohnzimmer",
userID: "u1", userName: "guido", isAdministrator: false))
}
private static func folge(
id: String, staffel: Int, nummer: Int,
publishAt: String = "null", zuletzt: String = "null"
) -> String {
"""
{
"id": "\(id)", "season": \(staffel), "episode": \(nummer),
"title": "Neuanfang", "overview": null, "stillImage": null,
"files": [{
"id": "d-\(id)", "fileName": "F.mkv", "sizeBytes": 1,
"container": "mkv", "durationSeconds": 2700, "streams": []
}],
"state": { "positionSeconds": 0, "runtimeSeconds": 2700,
"watched": false, "playCount": 0, "favourite": false,
"rating": null, "dismissed": false,
"lastPlayedAt": \(zuletzt), "inProgress": false },
"publishAt": \(publishAt), "seriesId": "serie-1", "markers": []
}
"""
}
private static let folgeJetzt = MediaItem(
id: "s1e10", kind: .episode, title: "Finale", index: 10, parentIndex: 1)
/// Das Staffelfinale: die nächste Folge ist die erste der nächsten Staffel.
@Test("Über die Staffelgrenze hinweg")
func ueberDieStaffelgrenze() async throws {
let backend = backend(Attrappe(rumpf: Self.folge(id: "s2e1", staffel: 2, nummer: 1)))
let naechste = try #require(await backend.nextEpisode(after: Self.folgeJetzt))
#expect(naechste.id == "s2e1")
#expect(naechste.parentIndex == 2)
#expect(naechste.index == 1)
}
/// **Der Fehler selbst.** Mit `publishAt` und `lastPlayedAt` scheiterte
/// das Lesen stillschweigend, und der Knopf blieb weg. Mit dem frischen
/// `JSONDecoder()` von vorher ist diese Prüfung rot.
@Test("Auch mit Datumsangaben — daran scheiterte es")
func mitDatumsangaben() async throws {
let backend = backend(Attrappe(rumpf: Self.folge(
id: "s2e1", staffel: 2, nummer: 1,
publishAt: "\"2026-10-01T18:00:00Z\"",
zuletzt: "\"2026-10-11T21:14:03.482Z\"")))
let naechste = try #require(await backend.nextEpisode(after: Self.folgeJetzt))
#expect(naechste.id == "s2e1")
#expect(naechste.availableFrom != nil)
}
/// Nach der letzten Folge kommt keine — 204, und das ist kein Fehler.
@Test("Hinter der letzten Folge kommt nichts")
func hinterDerLetzten() async throws {
let backend = backend(Attrappe(rumpf: "", status: 204))
#expect(try await backend.nextEpisode(after: Self.folgeJetzt) == nil)
}
/// Ein Film hat keine Nachbarn — und fragt gar nicht erst.
@Test("Ein Film fragt nicht nach Nachbarn")
func filmFragtNicht() async throws {
let attrappe = Attrappe(rumpf: Self.folge(id: "x", staffel: 1, nummer: 1))
let film = MediaItem(id: "f1", kind: .movie, title: "Arrival")
#expect(try await backend(attrappe).nextEpisode(after: film) == nil)
#expect(attrappe.anfragen.isEmpty)
}
}