Zwei Fehler, derselbe Grund — und beide zeigte erst der Browser.
**Der Spieler bekam die Webseite statt des Films.** „MEDIA_ELEMENT_ERROR:
Format error", 531 Byte, `Content-Type: text/html`. Das ist genau die Grösse
von `index.html`.
`MapGet` nimmt HEAD nicht an. Die Oberfläche fragt aber mit HEAD nach, ob das
Umpacken fertig ist — einen halbfertigen Film herunterzuladen, nur um das zu
erfahren, wäre absurd. Die Anfrage fiel durch den Methodenabgleich hindurch
bis zum SPA-Rückfall, und der nimmt jeden Pfad ohne Dateiendung. Antwort:
200, mit der Startseite im Rumpf. Der Client hielt das Umpacken für fertig
und reichte HTML an `<video>` weiter.
Jetzt `MapMethods` mit GET und HEAD. Kestrel lässt den Rumpf bei HEAD selbst
weg.
**Und ein vertippter Aufruf sah aus wie ein Erfolg.** `GET /api/v1/gibtsnicht`
gab 200 und die Webseite zurück — derselbe Rückfall. Ein Fehler, der erst
dort auffällt, wo jemand HTML durch einen JSON-Parser schickt.
`app.Map("/api/{*rest}", 404)` davor. Buchstaben schlagen Platzhalter, also
gewinnt jeder echte Endpunkt; was übrig bleibt, ist nicht da und sagt das
auch.
350 Tests grün, die zwei neuen eingerechnet. Am Container nachgemessen: HEAD
auf den Download gibt 200 `video/mp4` mit 71 920 Byte, `/api/v1/gibtsnicht`
gibt 404, und ein Neuladen auf `/verwaltung` bekommt weiterhin die Seite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
220 lines
9.1 KiB
C#
220 lines
9.1 KiB
C#
using System.Diagnostics;
|
|
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using System.Net.Http.Json;
|
|
using Flaro.Api.Endpoints;
|
|
|
|
namespace Flaro.Api.Tests;
|
|
|
|
/// <summary>Der Weg vom Scan bis zur heruntergeladenen Datei.</summary>
|
|
public class DeliveryTests
|
|
{
|
|
private string _bestand = "";
|
|
|
|
private static string? Werkzeug(string name) =>
|
|
(Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator)
|
|
.Select(o => o.Length == 0 ? null : Path.Combine(o, name))
|
|
.FirstOrDefault(p => p is not null && File.Exists(p));
|
|
|
|
[SetUp]
|
|
public void Anlegen()
|
|
{
|
|
if (Werkzeug("ffmpeg") is null) Assert.Ignore("ffmpeg ist auf diesem Rechner nicht installiert.");
|
|
_bestand = Directory.CreateDirectory(
|
|
Path.Combine(Path.GetTempPath(), "flaro-liefern-" + Guid.CreateVersion7().ToString("N"))).FullName;
|
|
}
|
|
|
|
[TearDown]
|
|
public void Aufraeumen()
|
|
{
|
|
try { if (_bestand.Length > 0) Directory.Delete(_bestand, recursive: true); } catch (IOException) { }
|
|
}
|
|
|
|
/// <summary>Eine Datei mit zwei Tonspuren, in der gewünschten Schachtel.</summary>
|
|
private string Film(string name, string endung)
|
|
{
|
|
var ziel = Path.Combine(_bestand, $"{name}.{endung}");
|
|
var start = new ProcessStartInfo(Werkzeug("ffmpeg")!) { RedirectStandardError = true, UseShellExecute = false };
|
|
foreach (var a in new[]
|
|
{
|
|
"-y", "-loglevel", "error",
|
|
"-f", "lavfi", "-i", "testsrc=duration=1:size=160x120:rate=10",
|
|
"-f", "lavfi", "-i", "sine=frequency=440:duration=1",
|
|
"-f", "lavfi", "-i", "sine=frequency=880:duration=1",
|
|
"-map", "0:v", "-map", "1:a", "-map", "2:a",
|
|
"-metadata:s:a:0", "language=deu", "-metadata:s:a:0", "title=Deutsch",
|
|
"-metadata:s:a:1", "language=deu", "-metadata:s:a:1", "title=Audiodeskription",
|
|
"-c:v", "libx264", "-preset", "ultrafast", "-c:a", "aac", ziel,
|
|
}) start.ArgumentList.Add(a);
|
|
|
|
using var prozess = Process.Start(start)!;
|
|
var fehler = prozess.StandardError.ReadToEnd();
|
|
prozess.WaitForExit();
|
|
if (prozess.ExitCode != 0) throw new InvalidOperationException(fehler);
|
|
return ziel;
|
|
}
|
|
|
|
private async Task<(HttpClient Client, MediaFileResponse Datei)> Eingelesen()
|
|
{
|
|
var server = _server = new FlaroServer();
|
|
var admin = await server.SetUpAndSignIn();
|
|
var client = server.AlsAngemeldeter(admin.Token);
|
|
|
|
var angelegt = await client.PostAsJsonAsync("/api/v1/libraries",
|
|
new CreateLibraryRequest("Prüfbestand", "Movies", _bestand));
|
|
angelegt.EnsureSuccessStatusCode();
|
|
var bibliothek = (await angelegt.Content.ReadFromJsonAsync<LibraryResponse>())!;
|
|
|
|
(await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null)).EnsureSuccessStatusCode();
|
|
for (var i = 0; i < 150; i++)
|
|
{
|
|
var stand = await client.GetFromJsonAsync<ScanStatus>($"/api/v1/libraries/{bibliothek.Id}/scan");
|
|
if (stand!.State == "finished") break;
|
|
await Task.Delay(50);
|
|
}
|
|
|
|
var filme = await client.GetFromJsonAsync<Page<MovieResponse>>("/api/v1/movies");
|
|
Assert.That(filme!.Items, Is.Not.Empty, "Der Scan hat nichts gefunden");
|
|
return (client, filme.Items[0].Files.Single());
|
|
}
|
|
|
|
private FlaroServer? _server;
|
|
|
|
[TearDown]
|
|
public void ServerSchliessen() { _server?.Dispose(); _server = null; }
|
|
|
|
/// <summary>Der Scan trägt die Spuren ein — darunter die beiden Namen.</summary>
|
|
[Test]
|
|
public async Task Die_Spuren_stehen_in_der_Mediathek()
|
|
{
|
|
Film("Zwei Spuren (2020)", "mkv");
|
|
var (_, datei) = await Eingelesen();
|
|
|
|
Assert.That(datei.Container, Is.EqualTo("mkv"));
|
|
Assert.That(datei.DurationSeconds, Is.EqualTo(1).Within(0.5));
|
|
|
|
var ton = datei.Streams.Where(s => s.Kind == "Audio").ToList();
|
|
Assert.That(ton, Has.Count.EqualTo(2));
|
|
Assert.That(ton.Select(t => t.Title), Is.EqualTo(new[] { "Deutsch", "Audiodeskription" }));
|
|
}
|
|
|
|
/// <summary>Der volle Pfad des Servers geht niemanden etwas an.</summary>
|
|
[Test]
|
|
public async Task Nur_der_Dateiname_kommt_heraus_nicht_der_Pfad()
|
|
{
|
|
Film("Pfadprobe (2020)", "mkv");
|
|
var (_, datei) = await Eingelesen();
|
|
|
|
Assert.That(datei.FileName, Is.EqualTo("Pfadprobe (2020).mkv"));
|
|
Assert.That(datei.FileName, Does.Not.Contain("/"));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Eine_passende_MP4_geht_unveraendert_hinaus()
|
|
{
|
|
Film("Passend (2020)", "mp4");
|
|
var (client, datei) = await Eingelesen();
|
|
|
|
var auskunft = await client.GetFromJsonAsync<DeliveryInfo>($"/api/v1/files/{datei.Id}/delivery");
|
|
Assert.That(auskunft!.Method, Is.EqualTo("directFile"));
|
|
Assert.That(auskunft.PackageState, Is.EqualTo("notNeeded"));
|
|
|
|
var antwort = await client.GetAsync($"/api/v1/files/{datei.Id}/download");
|
|
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
|
Assert.That(antwort.Content.Headers.ContentLength, Is.GreaterThan(0),
|
|
"Ohne Längenangabe gäbe es keinen Fortschrittsbalken");
|
|
}
|
|
|
|
/// <summary>Eine Matroska wird umgepackt — und der Client erfährt es vorher.</summary>
|
|
[Test]
|
|
public async Task Eine_Matroska_wird_angekuendigt_und_dann_umgepackt()
|
|
{
|
|
Film("Umzupacken (2020)", "mkv");
|
|
var (client, datei) = await Eingelesen();
|
|
|
|
var auskunft = await client.GetFromJsonAsync<DeliveryInfo>($"/api/v1/files/{datei.Id}/delivery");
|
|
Assert.That(auskunft!.Method, Is.EqualTo("remux"));
|
|
Assert.That(auskunft.Reasons, Does.Contain("containerUnsupported"));
|
|
Assert.That(auskunft.AudioToEncode, Is.Empty, "Beide Tonspuren passen und werden nur umgepackt");
|
|
|
|
// Der erste Abruf stösst an und wartet nicht.
|
|
var erster = await client.GetAsync($"/api/v1/files/{datei.Id}/download");
|
|
Assert.That(erster.StatusCode, Is.EqualTo(HttpStatusCode.Accepted));
|
|
|
|
HttpResponseMessage? fertig = null;
|
|
for (var i = 0; i < 150; i++)
|
|
{
|
|
fertig = await client.GetAsync($"/api/v1/files/{datei.Id}/download");
|
|
if (fertig.StatusCode != HttpStatusCode.Accepted) break;
|
|
await Task.Delay(100);
|
|
}
|
|
|
|
Assert.That(fertig!.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
|
Assert.That(fertig.Content.Headers.ContentLength, Is.GreaterThan(0));
|
|
Assert.That(fertig.Content.Headers.ContentDisposition?.FileNameStar ?? "",
|
|
Does.EndWith(".mp4"), "Heruntergeladen wird eine MP4, nicht eine MKV");
|
|
}
|
|
|
|
/// <summary>Fortsetzen nach einem Abbruch — die Forderung aus dem Fehlerbericht.</summary>
|
|
[Test]
|
|
public async Task Die_Auslieferung_versteht_Bereichsanfragen()
|
|
{
|
|
Film("Bereich (2020)", "mp4");
|
|
var (client, datei) = await Eingelesen();
|
|
|
|
var anfrage = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/files/{datei.Id}/download");
|
|
anfrage.Headers.Range = new RangeHeaderValue(100, 199);
|
|
var antwort = await client.SendAsync(anfrage);
|
|
|
|
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.PartialContent));
|
|
Assert.That((await antwort.Content.ReadAsByteArrayAsync()), Has.Length.EqualTo(100));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Eine_Datei_die_es_nicht_gibt_ergibt_404()
|
|
{
|
|
Film("Egal (2020)", "mp4");
|
|
var (client, _) = await Eingelesen();
|
|
|
|
var antwort = await client.GetAsync($"/api/v1/files/{Guid.CreateVersion7()}/download");
|
|
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
|
}
|
|
|
|
/// <summary>Der Client fragt mit HEAD nach, ob das Umpacken fertig ist.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Der Fehler, den erst der Browser zeigte.</b> <c>MapGet</c> nimmt
|
|
/// HEAD nicht an. Die Anfrage fiel durch bis zum SPA-Rückfall, bekam
|
|
/// <c>200 text/html</c>, und der Spieler hielt die Webseite für den Film:
|
|
/// „MEDIA_ELEMENT_ERROR: Format error", 531 Byte lang.
|
|
/// </remarks>
|
|
[Test]
|
|
public async Task HEAD_auf_den_Download_antwortet_mit_dem_Download()
|
|
{
|
|
Film("Kopfpruefung (2021)", "mp4");
|
|
var (client, datei) = await Eingelesen();
|
|
|
|
var antwort = await client.SendAsync(
|
|
new HttpRequestMessage(HttpMethod.Head, $"/api/v1/files/{datei.Id}/download"));
|
|
|
|
Assert.Multiple(() =>
|
|
{
|
|
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.OK));
|
|
Assert.That(antwort.Content.Headers.ContentType?.MediaType, Is.EqualTo("video/mp4"));
|
|
});
|
|
}
|
|
|
|
[Test]
|
|
public async Task Ohne_Anmeldung_gibt_es_nichts()
|
|
{
|
|
Film("Geschuetzt (2020)", "mp4");
|
|
var (_, datei) = await Eingelesen();
|
|
|
|
// **Ohne Gedächtnis, weil der Client nach der Anmeldung ein Cookie
|
|
// hält.** Sonst prüfte dieser Test nicht den Unangemeldeten, sondern
|
|
// den Angemeldeten — und bestünde nie wieder.
|
|
var antwort = await _server!.OhneGedaechtnis().GetAsync($"/api/v1/files/{datei.Id}/download");
|
|
Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
|
|
}
|
|
}
|