using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using Flaro.Api.Endpoints; namespace Flaro.Api.Tests; /// Das eigene Konto — und wer daran darf. [TestFixture] public sealed class ProfilTests { /// Ein PNG, das nur ein PNG sein muss. /// /// /// Die ersten acht Bytes sind die Signatur; mehr prüft der Server nicht, /// und mehr braucht der Test nicht. Ein echtes Bild hier abzulegen hiesse, /// eine Binärdatei ins Verzeichnis zu legen, die niemand lesen kann. /// private static byte[] Bild(byte kennzeichen = 1) => [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, kennzeichen, 2, 3, 4]; private static MultipartFormDataContent Formular(byte[] inhalt, string name = "ich.png") { var teil = new ByteArrayContent(inhalt); teil.Headers.ContentType = new MediaTypeHeaderValue("image/png"); return new MultipartFormDataContent { { teil, "datei", name } }; } /// Hochladen, abrufen, entfernen — der ganze Weg. /// /// /// Dieser Weg war kaputt, und niemand merkte es. Die Ablage baute /// einen Schlüssel aus 32 Hexzeichen plus Endung, die Ausgabe verlangte /// genau 64 Hexzeichen — jeder Upload endete in einem 502. Deshalb steht /// hier nicht nur „200", sondern auch, dass das Bild danach wirklich /// herauskommt. /// [Test] public async Task Ein_Profilbild_laesst_sich_setzen_abrufen_und_entfernen() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var client = server.AlsAngemeldeter(admin.Token); var gesetzt = await client.PostAsync("/api/v1/users/me/avatar", Formular(Bild())); var antwort = await gesetzt.Content.ReadFromJsonAsync(); Assert.That(gesetzt.StatusCode, Is.EqualTo(HttpStatusCode.OK)); Assert.That(antwort!.Image, Has.Length.EqualTo(64), "der Schlüssel ist der ganze Hash"); var konto = await client.GetFromJsonAsync("/api/v1/users/me"); Assert.That(konto!.AvatarImage, Is.EqualTo(antwort.Image)); // **Und es kommt auch wieder heraus.** Ein Schlüssel, den die // Bildausgabe nicht gelten lässt, wäre so gut wie keiner. var abgerufen = await client.GetAsync($"/api/v1/images/{antwort.Image}"); Assert.Multiple(() => { Assert.That(abgerufen.StatusCode, Is.EqualTo(HttpStatusCode.OK)); Assert.That(abgerufen.Content.Headers.ContentType?.MediaType, Is.EqualTo("image/png"), "der Medientyp kommt aus dem Inhalt, nicht aus dem Dateinamen"); }); var entfernt = await client.DeleteAsync("/api/v1/users/me/avatar"); var danach = await client.GetFromJsonAsync("/api/v1/users/me"); Assert.Multiple(() => { Assert.That(entfernt.StatusCode, Is.EqualTo(HttpStatusCode.NoContent)); Assert.That(danach!.AvatarImage, Is.Null); }); } /// Das Bild steht nach dem Anmelden sofort zur Verfügung. /// /// /// Die Kopfleiste zeigt es auf jeder Seite. Käme es erst mit einer zweiten /// Anfrage, blitzte bei jedem Laden kurz der Buchstabenkreis auf. /// [Test] public async Task Die_Anmeldung_bringt_das_Bild_gleich_mit() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var client = server.AlsAngemeldeter(admin.Token); await client.PostAsync("/api/v1/users/me/avatar", Formular(Bild())); var erneut = await server.SignIn("guido", "ein-langes-passwort", "Zweitgerät", "zweit"); var ueberMich = await client.GetFromJsonAsync("/api/v1/auth/me"); Assert.Multiple(() => { Assert.That(erneut.User.AvatarImage, Is.Not.Null, "in der Anmeldeantwort"); Assert.That(ueberMich!.AvatarImage, Is.EqualTo(erneut.User.AvatarImage), "und in /auth/me"); }); } /// Wer kein Administrator ist, darf sein eigenes Konto trotzdem pflegen. /// /// /// Der Anlass. „Meine Sprache" stand in der Verwaltung, und die /// zeigt der Client nur Administratoren — der Endpunkt erlaubte sie längst /// jedem. Wer kein Admin war, konnte seine Sprache also nirgends ändern. /// [Test] public async Task Ohne_Adminrecht_gehoert_einem_das_eigene_Konto_trotzdem() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var gast = await server.NeuerBenutzer( server.AlsAngemeldeter(admin.Token), "gast", "gastpasswortlang"); var gesetzt = await gast.PostAsync("/api/v1/users/me/avatar", Formular(Bild(9))); var sprache = await gast.PutAsJsonAsync("/api/v1/users/me/language", new SprachRequest("en")); var konto = await gast.GetFromJsonAsync("/api/v1/users/me"); // Und die Verwaltung bleibt zu. var verwaltung = await gast.GetAsync("/api/v1/settings/playback"); Assert.Multiple(() => { Assert.That(gesetzt.StatusCode, Is.EqualTo(HttpStatusCode.OK)); Assert.That(sprache.StatusCode, Is.EqualTo(HttpStatusCode.OK)); Assert.That(konto!.Language, Is.EqualTo("en")); Assert.That(konto.IsAdministrator, Is.False); Assert.That(verwaltung.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden)); }); } /// Das Bild des einen ist nicht das des anderen. /// /// /// Die Kennung kommt aus dem Token und nie aus der Adresse — es gibt also /// gar keinen Weg, ein fremdes Konto zu meinen. Der Test hält fest, dass /// das so bleibt. /// [Test] public async Task Ein_Bild_gehoert_genau_einem_Konto() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var gast = await server.NeuerBenutzer(alsAdmin, "gast", "gastpasswortlang"); await alsAdmin.PostAsync("/api/v1/users/me/avatar", Formular(Bild(1))); await gast.PostAsync("/api/v1/users/me/avatar", Formular(Bild(2))); var eines = await alsAdmin.GetFromJsonAsync("/api/v1/users/me"); var anderes = await gast.GetFromJsonAsync("/api/v1/users/me"); Assert.That(eines!.AvatarImage, Is.Not.EqualTo(anderes!.AvatarImage)); } /// Eine zu grosse Datei wird abgelehnt, nicht abgelegt. [Test] public async Task Ein_zu_grosses_Bild_wird_abgelehnt() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var client = server.AlsAngemeldeter(admin.Token); var zuGross = new byte[6 * 1024 * 1024]; var antwort = await client.PostAsync("/api/v1/users/me/avatar", Formular(zuGross)); Assert.That(antwort.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); } private sealed record Bildantwort(string Image); }