using System.Net; using System.Net.Http.Json; using Microsoft.Extensions.DependencyInjection; using Flaro.Api.Endpoints; namespace Flaro.Api.Tests; /// Einladen statt ein Konto für jemanden erfinden. /// /// /// Der Test, der hier wirklich zählt, ist der mit den zwei gleichzeitigen /// Einlösungen. Ein einmaliger Link, zweimal im selben Augenblick /// geöffnet, ist der einzige Fall, den man von Hand nie findet — und der /// einzige, bei dem ein falscher Entwurf zwei Konten statt einem anlegt. /// public class EinladungTests { private static async Task Einladen( HttpClient alsAdmin, int tage = 7, int? wieOft = 1) { var antwort = await alsAdmin.PostAsJsonAsync("/api/v1/invites", new EinladungRequest(tage, wieOft)); antwort.EnsureSuccessStatusCode(); return (await antwort.Content.ReadFromJsonAsync())!; } private static Task Einloesen( HttpClient ohne, string token, string name, string passwort = "einlangespasswort") => ohne.PostAsJsonAsync($"/api/v1/invites/{token}/redeem", new EinloesenRequest(name, passwort)); [Test] public async Task Wer_den_Link_hat_legt_sich_ein_Konto_an() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var einladung = await Einladen(alsAdmin); var fremder = server.OhneGedaechtnis(); var schau = await fremder.GetFromJsonAsync( $"/api/v1/invites/{einladung.Token}"); var angelegt = await Einloesen(fremder, einladung.Token, "nachbarin"); var konten = await alsAdmin.GetFromJsonAsync>("/api/v1/users"); Assert.Multiple(() => { Assert.That(schau!.InvitedBy, Is.EqualTo("guido")); Assert.That(angelegt.StatusCode, Is.EqualTo(HttpStatusCode.NoContent)); Assert.That(konten!.Select(k => k.UserName), Does.Contain("nachbarin")); }); } [Test] public async Task Wer_eingeladen_wird_ist_kein_Administrator() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var einladung = await Einladen(alsAdmin); await Einloesen(server.OhneGedaechtnis(), einladung.Token, "nachbarin"); var anmeldung = await server.SignIn("nachbarin", "einlangespasswort", "Prüfgerät", "p1"); var alsGast = server.AlsAngemeldeter(anmeldung.Token); var verwaltung = await alsGast.GetAsync("/api/v1/invites"); Assert.Multiple(() => { Assert.That(anmeldung.User.IsAdministrator, Is.False); Assert.That(verwaltung.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden)); }); } [Test] public async Task Ein_eingeloester_Link_gilt_nicht_zweimal() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var einladung = await Einladen(alsAdmin); await Einloesen(server.OhneGedaechtnis(), einladung.Token, "erste"); var zweite = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "zweite"); Assert.That(zweite.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); Assert.That(await zweite.Content.ReadAsStringAsync(), Does.Contain("schon benutzt")); } /// Zwei im selben Augenblick — und trotzdem nur ein Konto. [Test] public async Task Zwei_gleichzeitige_Einloesungen_ergeben_ein_Konto() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var einladung = await Einladen(alsAdmin); var beide = await Task.WhenAll( Einloesen(server.OhneGedaechtnis(), einladung.Token, "eine"), Einloesen(server.OhneGedaechtnis(), einladung.Token, "andere")); var konten = await alsAdmin.GetFromJsonAsync>("/api/v1/users"); var dazu = konten!.Count(k => k.UserName is "eine" or "andere"); Assert.Multiple(() => { Assert.That(beide.Count(a => a.IsSuccessStatusCode), Is.EqualTo(1), "Genau eine Einlösung darf durchkommen."); Assert.That(dazu, Is.EqualTo(1), "Und genau ein Konto darf entstanden sein."); }); } [Test] public async Task Mehrfachlinks_gelten_so_oft_wie_angegeben() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var einladung = await Einladen(alsAdmin, wieOft: 2); var eine = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "eine"); var zwei = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "zwei"); var drei = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "drei"); var liste = await alsAdmin.GetFromJsonAsync>("/api/v1/invites"); Assert.Multiple(() => { Assert.That(eine.StatusCode, Is.EqualTo(HttpStatusCode.NoContent)); Assert.That(zwei.StatusCode, Is.EqualTo(HttpStatusCode.NoContent)); Assert.That(drei.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); Assert.That(liste![0].UsedCount, Is.EqualTo(2)); Assert.That(liste[0].Valid, Is.False); }); } [Test] public async Task Ein_zurueckgezogener_Link_wird_abgewiesen() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var einladung = await Einladen(alsAdmin); (await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}")).EnsureSuccessStatusCode(); var versuch = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "zuspaet"); var schau = await server.OhneGedaechtnis().GetAsync($"/api/v1/invites/{einladung.Token}"); Assert.Multiple(() => { Assert.That(versuch.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); Assert.That(schau.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); }); Assert.That(await versuch.Content.ReadAsStringAsync(), Does.Contain("zurückgezogen")); } [Test] public async Task Ein_abgelaufener_Link_wird_abgewiesen() { var uhr = new Stelluhr(DateTimeOffset.UtcNow); using var server = new FlaroServer(dienste => dienste.AddSingleton(uhr)); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var einladung = await Einladen(alsAdmin, tage: 1); uhr.Weiter(TimeSpan.FromDays(2)); var versuch = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "zuspaet"); Assert.That(versuch.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); Assert.That(await versuch.Content.ReadAsStringAsync(), Does.Contain("abgelaufen")); } [Test] public async Task Einen_Link_den_es_nicht_gibt_kennt_niemand() { using var server = new FlaroServer(); await server.SetUpAndSignIn(); var versuch = await Einloesen(server.OhneGedaechtnis(), "ausgedacht", "wer"); var schau = await server.OhneGedaechtnis().GetAsync("/api/v1/invites/ausgedacht"); Assert.Multiple(() => { Assert.That(versuch.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); Assert.That(schau.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); }); } [Test] public async Task Einladen_darf_nur_die_Verwaltung() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var gast = await server.NeuerBenutzer(alsAdmin, "gast", "gastpasswortlang"); var erstellen = await gast.PostAsJsonAsync("/api/v1/invites", new EinladungRequest()); var lesen = await gast.GetAsync("/api/v1/invites"); Assert.Multiple(() => { Assert.That(erstellen.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden)); Assert.That(lesen.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden)); }); } /// Der Link steht genau einmal da — danach nicht einmal mehr dem Admin. [Test] public async Task Der_Token_steht_nur_im_Erstellen_nicht_in_der_Liste() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var einladung = await Einladen(alsAdmin); var roh = await (await alsAdmin.GetAsync("/api/v1/invites")).Content.ReadAsStringAsync(); Assert.That(einladung.Token, Is.Not.Empty); Assert.That(roh, Does.Not.Contain(einladung.Token), "Gespeichert ist nur der Fingerabdruck — aus der Liste darf kein Link zu bauen sein."); } /// Ein Tippfehler im Namen darf den Link nicht verbrennen. [Test] public async Task Ein_vergebener_Name_verbraucht_die_Einladung_nicht() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var einladung = await Einladen(alsAdmin); var belegt = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "guido"); var danach = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "nachbarin"); Assert.Multiple(() => { Assert.That(belegt.StatusCode, Is.EqualTo(HttpStatusCode.Conflict)); Assert.That(danach.StatusCode, Is.EqualTo(HttpStatusCode.NoContent), "Der Link muss nach dem Fehlgriff noch gelten."); }); } [Test] public async Task Ohne_langes_Passwort_geht_es_nicht() { using var server = new FlaroServer(); var admin = await server.SetUpAndSignIn(); var alsAdmin = server.AlsAngemeldeter(admin.Token); var einladung = await Einladen(alsAdmin); var kurz = await Einloesen(server.OhneGedaechtnis(), einladung.Token, "wer", "1234"); var leer = await Einloesen(server.OhneGedaechtnis(), einladung.Token, " "); Assert.Multiple(() => { Assert.That(kurz.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); Assert.That(leer.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); }); } }