diff --git a/server/src/Flaro.Api/Endpoints/DeliveryEndpoints.cs b/server/src/Flaro.Api/Endpoints/DeliveryEndpoints.cs index 892146d..17d8f88 100644 --- a/server/src/Flaro.Api/Endpoints/DeliveryEndpoints.cs +++ b/server/src/Flaro.Api/Endpoints/DeliveryEndpoints.cs @@ -18,7 +18,13 @@ public static class DeliveryEndpoints return Results.Ok(Auskunft(plan, packer, datei.Id, datei.ModifiedAt)); }).RequireAuthorization(); - app.MapGet("/api/v1/files/{id:guid}/download", async ( + // **GET und HEAD.** Der Client fragt mit HEAD nach, ob das Umpacken + // fertig ist — eine Antwort ohne Rumpf genügt dafür, und einen + // halbfertigen Film herunterzuladen, nur um das zu erfahren, wäre + // absurd. `MapGet` allein nimmt HEAD nicht an; die Anfrage fiele dann + // durch bis zum SPA-Rückfall und bekäme die Webseite mit Status 200. + // Genau das ist passiert: der Spieler bekam HTML statt Video. + app.MapMethods("/api/v1/files/{id:guid}/download", ["GET", "HEAD"], async ( Guid id, FlaroContext db, PackageCoordinator packer) => { var gefunden = await Laden(db, id); diff --git a/server/src/Flaro.Api/Program.cs b/server/src/Flaro.Api/Program.cs index 2929f84..b26e26b 100644 --- a/server/src/Flaro.Api/Program.cs +++ b/server/src/Flaro.Api/Program.cs @@ -140,8 +140,15 @@ app.MapDelivery(); app.MapPlayback(); app.MapMatching(); -// Alles, was keine Schnittstelle ist, bekommt die Seite — der Router läuft -// im Browser. Ohne das endet ein Neuladen auf /admin in einem 404. +// **Was unter /api nicht beantwortet wurde, ist nicht da.** Ohne diese Zeile +// fängt der Rückfall darunter jeden unbekannten Pfad ab und liefert die +// Webseite mit Status 200 — ein vertippter Aufruf sieht dann aus wie ein +// Erfolg, und der Fehler fällt erst dort auf, wo HTML verarbeitet wird. +// Buchstaben schlagen Platzhalter, also gewinnt jeder echte Endpunkt hier. +app.Map("/api/{*rest}", () => Results.NotFound()).AllowAnonymous(); + +// Alles Übrige bekommt die Seite — der Router läuft im Browser. Ohne das +// endet ein Neuladen auf /verwaltung in einem 404. if (Directory.Exists(oberfläche)) { app.MapFallback(() => Results.File(Path.Combine(oberfläche, "index.html"), "text/html")) diff --git a/server/tests/Flaro.Api.Tests/DeliveryTests.cs b/server/tests/Flaro.Api.Tests/DeliveryTests.cs index 28a57b6..a0f113d 100644 --- a/server/tests/Flaro.Api.Tests/DeliveryTests.cs +++ b/server/tests/Flaro.Api.Tests/DeliveryTests.cs @@ -180,6 +180,30 @@ public class DeliveryTests Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); } + /// Der Client fragt mit HEAD nach, ob das Umpacken fertig ist. + /// + /// + /// Der Fehler, den erst der Browser zeigte. MapGet nimmt + /// HEAD nicht an. Die Anfrage fiel durch bis zum SPA-Rückfall, bekam + /// 200 text/html, und der Spieler hielt die Webseite für den Film: + /// „MEDIA_ELEMENT_ERROR: Format error", 531 Byte lang. + /// + [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() { diff --git a/server/tests/Flaro.Api.Tests/WebInterfaceTests.cs b/server/tests/Flaro.Api.Tests/WebInterfaceTests.cs index 935adaa..1b8cf8e 100644 --- a/server/tests/Flaro.Api.Tests/WebInterfaceTests.cs +++ b/server/tests/Flaro.Api.Tests/WebInterfaceTests.cs @@ -140,6 +140,38 @@ public sealed class WebInterfaceTests } } + /// Ein unbekannter Pfad unter /api ist ein Fehler, keine Seite. + /// + /// + /// Der Fund, der dahintersteckt. Der Rückfall für den Router im + /// Browser nimmt jeden Pfad ohne Dateiendung — auch + /// /api/v1/vertippt. Ein vertippter Aufruf bekam die Webseite mit + /// Status 200 zurück und sah damit wie ein Erfolg aus. + /// + [Test] + public async Task Ein_unbekannter_Schnittstellenpfad_gibt_404() + { + var wurzel = Path.Combine(Path.GetTempPath(), "flaro-www-" + Guid.CreateVersion7().ToString("N")); + Directory.CreateDirectory(Path.Combine(wurzel, "wwwroot")); + await File.WriteAllTextAsync(Path.Combine(wurzel, "wwwroot", "index.html"), "Flaro"); + + try + { + using var server = new FlaroServer(inhaltswurzel: wurzel); + var antwort = await server.OhneGedaechtnis().GetAsync("/api/v1/gibtsnicht"); + + Assert.Multiple(() => + { + Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); + Assert.That(antwort.Content.Headers.ContentType?.MediaType, Is.Not.EqualTo("text/html")); + }); + } + finally + { + try { Directory.Delete(wurzel, recursive: true); } catch (IOException) { /* Aufräumen ist Kür */ } + } + } + /// Nimmt den Namen und den Wert aus der Set-Cookie-Kopfzeile. private static string Keks(HttpResponseMessage antwort) => antwort.Headers.GetValues("Set-Cookie").Single().Split(';')[0];