using System.Diagnostics; using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Flaro.Api.Endpoints; namespace Flaro.Api.Tests; /// Der Weg vom Scan bis zur heruntergeladenen Datei. 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) { } } /// Eine Datei mit zwei Tonspuren, in der gewünschten Schachtel. 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())!; (await client.PostAsync($"/api/v1/libraries/{bibliothek.Id}/scan", null)).EnsureSuccessStatusCode(); for (var i = 0; i < 150; i++) { var stand = await client.GetFromJsonAsync($"/api/v1/libraries/{bibliothek.Id}/scan"); if (stand!.State == "finished") break; await Task.Delay(50); } var filme = await client.GetFromJsonAsync>("/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; } /// Der Scan trägt die Spuren ein — darunter die beiden Namen. [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" })); } /// Der volle Pfad des Servers geht niemanden etwas an. [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($"/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"); } /// Eine Matroska wird umgepackt — und der Client erfährt es vorher. [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($"/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"); } /// Fortsetzen nach einem Abbruch — die Forderung aus dem Fehlerbericht. [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)); } [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)); } }