namespace FiscalOS.API.Tests.Integration.Auth; public class LoginTests(TestApi testApi) : IntegrationTest(testApi) { private static readonly Uri LoginUri = new("/auth/login", UriKind.Relative); [Theory] [ClassData] public async Task Login_WhenUserSubmitsInvalidRequest_ItShouldReturn400WithProblemDetails(LoginValidationTestCase tc) { using var request = HttpRequestBuilder.New() .Post(LoginUri) .WithBody(new { username = tc.Username, password = tc.Password, }) .Build(); var res = await Client.SendAsync(request, TestContext.Current.CancellationToken); await res.Should().BeValidationProblemDetails(tc.ExpectedErrors); } [Fact] public async Task Login_WhenUserDoesNotExist_ItShouldReturn401WithProblemDetails() { using var request = HttpRequestBuilder.New() .Post(LoginUri) .WithBody(new { username = "Test", password = "@Password2", }) .Build(); var res = await Client.SendAsync(request, TestContext.Current.CancellationToken); await res.Should().BeProblemDetails(HttpStatusCode.Unauthorized); } [Fact] public async Task Login_WhenUserExistsButPasswordIsIncorrect_ItShouldReturn401WithProblemDetails() { await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct); context.Add(User.From("Stevan", passwordHasher.Hash("@Password1"), userEncryptionKey)); await context.SaveChangesAsync(ct); }, TestContext.Current.CancellationToken); using var request = HttpRequestBuilder.New() .Post(LoginUri) .WithBody(new { username = "Stevan", password = "@Password2", }) .Build(); var res = await Client.SendAsync(request, TestContext.Current.CancellationToken); await res.Should().BeProblemDetails(HttpStatusCode.Unauthorized); } [Fact] public async Task Login_WhenUserExistsAndPasswordIsCorrect_ItShouldReturn200WithJwtTokenAndSetRefreshCookie() { await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct); context.Add(User.From("Stevan", passwordHasher.Hash("@Password1"), userEncryptionKey)); await context.SaveChangesAsync(ct); }, TestContext.Current.CancellationToken); using var request = HttpRequestBuilder.New() .Post(LoginUri) .WithBody(new { username = "Stevan", password = "@Password1", }) .Build(); var res = await Client.SendAsync(request, TestContext.Current.CancellationToken); res.Should().HaveSetCookieHeader("fiscalos_refresh_cookie"); await res.Should().BeJsonContentOfType(HttpStatusCode.OK); } } public class LoginValidationTestCases : TheoryData { public LoginValidationTestCases() { Add(new LoginValidationTestCase( "No username or password", string.Empty, string.Empty, new SerializableDictionary() { ["Username"] = ["The Username field is required."], ["Password"] = ["The Password field is required."] } )); Add(new LoginValidationTestCase( "No username", string.Empty, "@Password1", new SerializableDictionary() { ["Username"] = ["The Username field is required."], } )); Add(new LoginValidationTestCase( "No password", "Stevan", string.Empty, new SerializableDictionary() { ["Password"] = ["The Password field is required."] } )); } } public record LoginValidationTestCase : IXunitSerializable { public string Name { get; private set; } = string.Empty; public string Username { get; private set; } = string.Empty; public string Password { get; private set; } = string.Empty; public SerializableDictionary ExpectedErrors { get; private set; } = []; public override string ToString() { return Name; } public LoginValidationTestCase() { } public LoginValidationTestCase( string name, string username, string password, SerializableDictionary expectedErrors ) { Name = name; Username = username; Password = password; ExpectedErrors = expectedErrors; } public void Deserialize(IXunitSerializationInfo info) { Name = info.GetValue(nameof(Name)) ?? string.Empty; Username = info.GetValue(nameof(Username)) ?? string.Empty; Password = info.GetValue(nameof(Password)) ?? string.Empty; ExpectedErrors = info.GetValue>(nameof(ExpectedErrors)) ?? []; } public void Serialize(IXunitSerializationInfo info) { info.AddValue(nameof(Name), Name); info.AddValue(nameof(Username), Username); info.AddValue(nameof(Password), Password); info.AddValue(nameof(ExpectedErrors), ExpectedErrors); } }