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));
});
}
/// Eine zurückgezogene Einladung lässt sich endgültig entfernen.
///
///
/// Warum das überhaupt nötig ist. Zurückziehen behält die Spur
/// absichtlich — man will wissen, dass es den Link gab, wenn jemand
/// Unerwartetes im Haushalt auftaucht. Nur verlässt die Zeile die Liste
/// dann nie, und eine Liste, aus der nichts verschwindet, wird
/// irgendwann unlesbar. Weggeworfen wird deshalb nur, was ohnehin keine
/// Tür mehr öffnet.
///
[Test]
public async Task Eine_zurueckgezogene_Einladung_laesst_sich_entfernen()
{
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 weg = await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}?purge=true");
var liste = await alsAdmin.GetFromJsonAsync>("/api/v1/invites");
Assert.Multiple(() =>
{
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
Assert.That(liste, Is.Empty, "die Zeile muss die Liste verlassen");
});
}
/// Ein gültiger Link lässt sich nicht wegwischen.
///
///
/// Sonst wäre „Entfernen" ein stilles Zurückziehen mit Gedächtnisverlust:
/// der Link gälte weiter, stünde aber nirgends mehr. Erst zurückziehen,
/// dann entfernen — ein Handgriff mehr, mit Absicht.
///
[Test]
public async Task Ein_gueltiger_Link_laesst_sich_nicht_entfernen()
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var alsAdmin = server.AlsAngemeldeter(admin.Token);
var einladung = await Einladen(alsAdmin);
var weg = await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}?purge=true");
var liste = await alsAdmin.GetFromJsonAsync>("/api/v1/invites");
Assert.Multiple(() =>
{
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
Assert.That(liste, Has.Count.EqualTo(1));
Assert.That(liste![0].Valid, Is.True, "und er gilt danach immer noch");
});
}
/// Ein aufgebrauchter Link darf weg, auch ohne Zurückziehen.
[Test]
public async Task Ein_aufgebrauchter_Link_laesst_sich_entfernen()
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var alsAdmin = server.AlsAngemeldeter(admin.Token);
var einladung = await Einladen(alsAdmin, wieOft: 1);
(await Einloesen(server.OhneGedaechtnis(), einladung.Token, "gast")).EnsureSuccessStatusCode();
var weg = await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}?purge=true");
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
}
}