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);
}