namespace FiscalOS.API.Tests.Integration; public class LoginTests(TestApi testApi) : IntegrationTest(testApi) { private static readonly Uri LoginUri = new("/login", UriKind.Relative); [Theory] [ClassData] public async Task Login_WhenUserSubmitsInvalidRequest_ItShouldReturn400WithProblemDetails(LoginValidationTestCase tc) { var req = new { username = tc.Username, password = tc.Password, }; var res = await Client.PostAsJsonAsync(LoginUri, req, TestContext.Current.CancellationToken); res.StatusCode.Should().Be(HttpStatusCode.BadRequest); var problem = await res.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); problem!.Errors.Should().BeEquivalentTo(tc.ExpectedErrors); } [Fact] public async Task Login_WhenUserDoesNotExist_ItShouldReturn401WithProblemDetails() { var req = new { username = "Test", password = "@Password2", }; var res = await Client.PostAsJsonAsync(LoginUri, req, TestContext.Current.CancellationToken); res.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] public async Task Login_WhenUserExistsButPasswordIsIncorrect_ItShouldReturn401WithProblemDetails() { await ExecuteDbContextAsync(static async (context, sp) => { var passwordHasher = sp.GetRequiredService(); context.Add(User.From("Stevan", passwordHasher.Hash("@Password1"))); await context.SaveChangesAsync(TestContext.Current.CancellationToken); }); var req = new { username = "Stevan", password = "@Password2", }; var res = await Client.PostAsJsonAsync(LoginUri, req, TestContext.Current.CancellationToken); res.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] public async Task Login_WhenUserExistsAndPasswordIsCorrect_ItShouldReturn200WithJwtTokenAndSetRefreshCookie() { await ExecuteDbContextAsync(static async (context, sp) => { var passwordHasher = sp.GetRequiredService(); context.Add(User.From("Stevan", passwordHasher.Hash("@Password1"))); await context.SaveChangesAsync(TestContext.Current.CancellationToken); }); var req = new { username = "Stevan", password = "@Password1", }; var res = await Client.PostAsJsonAsync(LoginUri, req, TestContext.Current.CancellationToken); res.StatusCode.Should().Be(HttpStatusCode.OK); var content = await res.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); content!.AccessToken.Should().NotBeNullOrEmpty(); res.Headers.TryGetValues("Set-Cookie", out var cookies).Should().BeTrue(); var hasRefreshTokenCookie = cookies! .Any(static c => c.StartsWith("fiscalos_refresh_cookie=", StringComparison.OrdinalIgnoreCase)); hasRefreshTokenCookie.Should().BeTrue(); } } public class LoginValidationTestCases : TheoryData { public LoginValidationTestCases() { Add(new LoginValidationTestCase( "No username or password", string.Empty, string.Empty, new Dictionary() { ["Username"] = ["The Username field is required."], ["Password"] = ["The Password field is required."] } )); Add(new LoginValidationTestCase( "No username", string.Empty, "@Password1", new Dictionary() { ["Username"] = ["The Username field is required."], } )); Add(new LoginValidationTestCase( "No password", "Stevan", string.Empty, new Dictionary() { ["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 Dictionary ExpectedErrors { get; private set; } = []; public override string ToString() { return Name; } public LoginValidationTestCase() { } public LoginValidationTestCase( string name, string username, string password, Dictionary 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); } }