Files
LehrerApp/LehrerApp.Api.Tests/UserStoreTests.cs
adminandClaude Sonnet 5 ba8786906a fix: Login-userId war nicht stabil gegenüber Groß-/Kleinschreibung
/api/auth/login mintete die JWT-userId bisher aus dem roh eingegebenen Benutzernamen. LiteDBs
Standard-Collation vergleicht den eindeutigen Index auf Username aber case-insensitive - ein
Login mit nur einmal abweichender Schreibweise auf einem zweiten Gerät authentifiziert
erfolgreich, mintet aber eine andere userId. Da EventStore.GetCol(userId) diese direkt als
Dateiname für den Server-seitigen Event-Speicher nutzt, entstanden zwei komplett getrennte
Datenbestände für ein und dasselbe, aus Nutzersicht einzige Konto - Ursache dafür, dass ein
zweites Gerät trotz "since=0" durchgängig 0 Ereignisse erhielt.

UserStore.VerifyPassword(bool) durch Authenticate(UserEntry?) ersetzt, das bei Erfolg den
kanonisch gespeicherten Nutzereintrag liefert; /api/auth/login mintet Token und userId daraus
statt aus der Roheingabe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 23:51:31 +02:00

106 lines
3.6 KiB
C#

using Xunit;
namespace LehrerApp.Api.Tests;
public sealed class UserStoreTests
{
[Fact]
public void CreateUser_NeuerNutzername_GibtTrueZurueckUndKannSichAnmelden()
{
using var temp = new TempUserStore();
var created = temp.Store.CreateUser("sebastian", "einSicheresPasswort");
Assert.True(created);
Assert.NotNull(temp.Store.Authenticate("sebastian", "einSicheresPasswort"));
}
[Fact]
public void CreateUser_BereitsVorhandenerNutzername_GibtFalseZurueck()
{
using var temp = new TempUserStore();
temp.Store.CreateUser("sebastian", "einSicheresPasswort");
var created = temp.Store.CreateUser("sebastian", "einAnderesPasswort");
Assert.False(created);
}
[Fact]
public void Authenticate_FalschesPasswort_LiefertNull()
{
using var temp = new TempUserStore();
temp.Store.CreateUser("sebastian", "einSicheresPasswort");
Assert.Null(temp.Store.Authenticate("sebastian", "falschesPasswort"));
}
[Fact]
public void Authenticate_UnbekannterNutzer_LiefertNull()
{
using var temp = new TempUserStore();
Assert.Null(temp.Store.Authenticate("unbekannt", "irgendwas"));
}
/// Regression: LiteDBs Standard-Collation vergleicht den eindeutigen Index auf Username
/// case-insensitive - ein Login mit abweichender Groß-/Kleinschreibung authentifiziert also
/// erfolgreich. Würde der Aufrufer (Endpoints./api/auth/login) daraufhin den roh eingegebenen
/// Namen statt des kanonisch gespeicherten als userId für den Server-seitigen Event-Speicher
/// verwenden, würde ein einziger andersartig getippter Login auf einem zweiten Gerät einen
/// komplett getrennten, nicht überlappenden Datenbestand erzeugen (siehe TODO 10.2.5 - genau
/// das vom Nutzer beobachtete Symptom: nur ein Konto im System, aber Client 2 erhielt trotz
/// since=0 keine Ereignisse).
[Fact]
public void Authenticate_AndereGrossKleinschreibung_LiefertDenKanonischGespeichertenNamen()
{
using var temp = new TempUserStore();
temp.Store.CreateUser("sebastian", "einSicheresPasswort");
var user = temp.Store.Authenticate("Sebastian", "einSicheresPasswort");
Assert.NotNull(user);
Assert.Equal("sebastian", user!.Username);
}
[Fact]
public void SetPassword_VorhandenerNutzer_AktualisiertPasswortUndAltesFunktioniertNichtMehr()
{
using var temp = new TempUserStore();
temp.Store.CreateUser("sebastian", "altesPasswort123");
var updated = temp.Store.SetPassword("sebastian", "neuesPasswort123");
Assert.True(updated);
Assert.NotNull(temp.Store.Authenticate("sebastian", "neuesPasswort123"));
Assert.Null(temp.Store.Authenticate("sebastian", "altesPasswort123"));
}
[Fact]
public void SetPassword_UnbekannterNutzer_GibtFalseZurueck()
{
using var temp = new TempUserStore();
Assert.False(temp.Store.SetPassword("unbekannt", "irgendeinPasswort"));
}
private sealed class TempUserStore : IDisposable
{
private readonly string _directory = Path.Combine(
Path.GetTempPath(), $"lehrerapp-api-tests-{Guid.NewGuid():N}");
public UserStore Store { get; }
public TempUserStore()
{
Directory.CreateDirectory(_directory);
Store = new UserStore(_directory);
}
public void Dispose()
{
Store.Dispose();
if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true);
}
}
}