2026-02-13 21:20:05 -06:00
|
|
|
namespace FiscalOS.API.Tests.Integration.Auth;
|
2026-02-01 04:27:06 -06:00
|
|
|
|
2026-02-01 08:07:23 -06:00
|
|
|
public class LoginTests(TestApi testApi) : IntegrationTest(testApi)
|
2026-02-01 04:27:06 -06:00
|
|
|
{
|
2026-02-13 21:20:05 -06:00
|
|
|
private static readonly Uri LoginUri = new("/auth/login", UriKind.Relative);
|
2026-02-03 11:48:48 -06:00
|
|
|
|
2026-02-01 06:12:49 -06:00
|
|
|
[Theory]
|
|
|
|
|
[ClassData<LoginValidationTestCases>]
|
|
|
|
|
public async Task Login_WhenUserSubmitsInvalidRequest_ItShouldReturn400WithProblemDetails(LoginValidationTestCase tc)
|
2026-02-01 04:27:06 -06:00
|
|
|
{
|
2026-02-13 21:20:05 -06:00
|
|
|
using var request = HttpRequestBuilder.New()
|
|
|
|
|
.Post(LoginUri)
|
|
|
|
|
.WithBody(new
|
|
|
|
|
{
|
|
|
|
|
username = tc.Username,
|
|
|
|
|
password = tc.Password,
|
|
|
|
|
})
|
|
|
|
|
.Build();
|
2026-02-01 04:27:06 -06:00
|
|
|
|
2026-02-13 21:20:05 -06:00
|
|
|
var res = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
2026-02-01 06:12:49 -06:00
|
|
|
|
2026-02-03 21:03:41 -06:00
|
|
|
await res.Should().BeValidationProblemDetails(tc.ExpectedErrors);
|
2026-02-01 06:12:49 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-02-01 11:31:46 -06:00
|
|
|
public async Task Login_WhenUserDoesNotExist_ItShouldReturn401WithProblemDetails()
|
2026-02-01 06:12:49 -06:00
|
|
|
{
|
2026-02-13 21:20:05 -06:00
|
|
|
using var request = HttpRequestBuilder.New()
|
|
|
|
|
.Post(LoginUri)
|
|
|
|
|
.WithBody(new
|
|
|
|
|
{
|
|
|
|
|
username = "Test",
|
|
|
|
|
password = "@Password2",
|
|
|
|
|
})
|
|
|
|
|
.Build();
|
2026-02-01 06:12:49 -06:00
|
|
|
|
2026-02-13 21:20:05 -06:00
|
|
|
var res = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
2026-02-01 11:31:46 -06:00
|
|
|
|
2026-02-03 21:03:41 -06:00
|
|
|
await res.Should().BeProblemDetails(HttpStatusCode.Unauthorized);
|
2026-02-01 11:31:46 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Login_WhenUserExistsButPasswordIsIncorrect_ItShouldReturn401WithProblemDetails()
|
|
|
|
|
{
|
2026-03-08 09:15:45 -05:00
|
|
|
await Api.ExecuteAsync(static async (context, ct, sp) =>
|
2026-02-01 11:31:46 -06:00
|
|
|
{
|
2026-02-02 04:49:00 -06:00
|
|
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
2026-02-06 06:48:34 -06:00
|
|
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
2026-02-02 04:49:00 -06:00
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
2026-02-06 06:48:34 -06:00
|
|
|
context.Add(User.From("Stevan", passwordHasher.Hash("@Password1"), userEncryptionKey));
|
2026-02-01 11:31:46 -06:00
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
await context.SaveChangesAsync(ct);
|
|
|
|
|
}, TestContext.Current.CancellationToken);
|
2026-02-01 11:31:46 -06:00
|
|
|
|
2026-02-13 21:20:05 -06:00
|
|
|
using var request = HttpRequestBuilder.New()
|
|
|
|
|
.Post(LoginUri)
|
|
|
|
|
.WithBody(new
|
|
|
|
|
{
|
|
|
|
|
username = "Stevan",
|
|
|
|
|
password = "@Password2",
|
|
|
|
|
})
|
|
|
|
|
.Build();
|
2026-02-01 11:31:46 -06:00
|
|
|
|
2026-02-13 21:20:05 -06:00
|
|
|
var res = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
2026-02-01 06:12:49 -06:00
|
|
|
|
2026-02-03 21:03:41 -06:00
|
|
|
await res.Should().BeProblemDetails(HttpStatusCode.Unauthorized);
|
2026-02-01 06:12:49 -06:00
|
|
|
}
|
2026-02-02 13:33:23 -06:00
|
|
|
|
|
|
|
|
[Fact]
|
2026-02-03 11:48:48 -06:00
|
|
|
public async Task Login_WhenUserExistsAndPasswordIsCorrect_ItShouldReturn200WithJwtTokenAndSetRefreshCookie()
|
2026-02-02 13:33:23 -06:00
|
|
|
{
|
2026-03-08 09:15:45 -05:00
|
|
|
await Api.ExecuteAsync(static async (context, ct, sp) =>
|
2026-02-02 13:33:23 -06:00
|
|
|
{
|
|
|
|
|
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
2026-02-06 06:48:34 -06:00
|
|
|
var encryptor = sp.GetRequiredService<IEncryptor>();
|
2026-02-02 13:33:23 -06:00
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
var userEncryptionKey = await encryptor.GenerateEncryptedKeyAsync(ct);
|
2026-02-06 06:48:34 -06:00
|
|
|
context.Add(User.From("Stevan", passwordHasher.Hash("@Password1"), userEncryptionKey));
|
2026-02-02 13:33:23 -06:00
|
|
|
|
2026-02-12 20:47:40 -06:00
|
|
|
await context.SaveChangesAsync(ct);
|
|
|
|
|
}, TestContext.Current.CancellationToken);
|
2026-02-02 13:33:23 -06:00
|
|
|
|
2026-02-13 21:20:05 -06:00
|
|
|
using var request = HttpRequestBuilder.New()
|
|
|
|
|
.Post(LoginUri)
|
|
|
|
|
.WithBody(new
|
|
|
|
|
{
|
|
|
|
|
username = "Stevan",
|
|
|
|
|
password = "@Password1",
|
|
|
|
|
})
|
|
|
|
|
.Build();
|
2026-02-02 13:33:23 -06:00
|
|
|
|
2026-02-13 21:20:05 -06:00
|
|
|
var res = await Client.SendAsync(request, TestContext.Current.CancellationToken);
|
2026-02-02 13:33:23 -06:00
|
|
|
|
2026-02-03 21:03:41 -06:00
|
|
|
res.Should().HaveSetCookieHeader("fiscalos_refresh_cookie");
|
2026-02-13 21:20:05 -06:00
|
|
|
await res.Should().BeJsonContentOfType<API.Auth.Login.Response>(HttpStatusCode.OK);
|
2026-02-02 13:33:23 -06:00
|
|
|
}
|
2026-02-01 06:12:49 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public class LoginValidationTestCases : TheoryData<LoginValidationTestCase>
|
|
|
|
|
{
|
|
|
|
|
public LoginValidationTestCases()
|
|
|
|
|
{
|
|
|
|
|
Add(new LoginValidationTestCase(
|
|
|
|
|
"No username or password",
|
|
|
|
|
string.Empty,
|
|
|
|
|
string.Empty,
|
2026-02-12 16:18:25 -06:00
|
|
|
new SerializableDictionary<string, string[]>()
|
2026-02-01 06:12:49 -06:00
|
|
|
{
|
|
|
|
|
["Username"] = ["The Username field is required."],
|
|
|
|
|
["Password"] = ["The Password field is required."]
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
Add(new LoginValidationTestCase(
|
|
|
|
|
"No username",
|
|
|
|
|
string.Empty,
|
|
|
|
|
"@Password1",
|
2026-02-12 16:18:25 -06:00
|
|
|
new SerializableDictionary<string, string[]>()
|
2026-02-01 06:12:49 -06:00
|
|
|
{
|
|
|
|
|
["Username"] = ["The Username field is required."],
|
|
|
|
|
}
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
Add(new LoginValidationTestCase(
|
|
|
|
|
"No password",
|
|
|
|
|
"Stevan",
|
|
|
|
|
string.Empty,
|
2026-02-12 16:18:25 -06:00
|
|
|
new SerializableDictionary<string, string[]>()
|
2026-02-01 06:12:49 -06:00
|
|
|
{
|
|
|
|
|
["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;
|
2026-02-12 16:18:25 -06:00
|
|
|
public SerializableDictionary<string, string[]> ExpectedErrors { get; private set; } = [];
|
2026-02-01 06:12:49 -06:00
|
|
|
|
|
|
|
|
public override string ToString()
|
|
|
|
|
{
|
|
|
|
|
return Name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public LoginValidationTestCase()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public LoginValidationTestCase(
|
|
|
|
|
string name,
|
|
|
|
|
string username,
|
|
|
|
|
string password,
|
2026-02-12 16:18:25 -06:00
|
|
|
SerializableDictionary<string, string[]> expectedErrors
|
2026-02-01 06:12:49 -06:00
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
Name = name;
|
|
|
|
|
Username = username;
|
|
|
|
|
Password = password;
|
|
|
|
|
ExpectedErrors = expectedErrors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Deserialize(IXunitSerializationInfo info)
|
|
|
|
|
{
|
|
|
|
|
Name = info.GetValue<string>(nameof(Name)) ?? string.Empty;
|
|
|
|
|
Username = info.GetValue<string>(nameof(Username)) ?? string.Empty;
|
|
|
|
|
Password = info.GetValue<string>(nameof(Password)) ?? string.Empty;
|
2026-02-12 16:18:25 -06:00
|
|
|
ExpectedErrors = info.GetValue<SerializableDictionary<string, string[]>>(nameof(ExpectedErrors)) ?? [];
|
2026-02-01 06:12:49 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
2026-02-01 04:27:06 -06:00
|
|
|
}
|
2026-02-12 20:47:40 -06:00
|
|
|
}
|