Files
itworx-knowledge-hub-Public/tests/backend/ItWorx.KnowledgeHub.IntegrationTests/Auth/LoginLogoutLockoutTests.cs
T

133 lines
5.4 KiB
C#

using System.Net;
using System.Net.Http.Json;
using ItWorx.KnowledgeHub.Contracts.Auth;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
namespace ItWorx.KnowledgeHub.IntegrationTests.Auth;
[Collection(PostgresTestGroup.Name)]
public sealed class LoginLogoutLockoutTests(PostgresContainerFixture postgres)
: OwnerSetupTestBase(postgres)
{
[Fact]
public async Task Login_with_correct_credentials_establishes_an_authenticated_session()
{
using var client = Factory.CreateClient();
var response = await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var session = await response.Content.ReadFromJsonAsync<SessionResponse>();
Assert.NotNull(session);
Assert.True(session.Authenticated);
Assert.Equal(OwnerEmail, session.Email);
Assert.Equal("Administrator", session.Role);
// The same client (with its session cookie) now sees itself as authenticated
// on a fresh request too, not only in the login response body.
var sessionAfter = await client.GetFromJsonAsync<SessionResponse>("/api/v1/auth/session");
Assert.NotNull(sessionAfter);
Assert.True(sessionAfter.Authenticated);
}
[Fact]
public async Task Login_with_wrong_password_is_rejected_generically()
{
using var client = Factory.CreateClient();
var response = await SetupHelper.LoginAsync(client, OwnerEmail, "totally-wrong-password");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
Assert.DoesNotContain(OwnerPassword, body, StringComparison.Ordinal);
}
[Fact]
public async Task Login_for_an_unknown_account_returns_the_same_response_as_a_wrong_password()
{
using var client = Factory.CreateClient();
var response = await SetupHelper.LoginAsync(client, "does-not-exist@itworx.test", "whatever-password");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task Login_rate_limit_is_per_remote_ip_and_ignores_untrusted_forwarded_headers()
{
using var clientA = Factory.CreateClient();
clientA.DefaultRequestHeaders.Add("X-Test-Remote-Ip", "192.0.2.10");
// Without a configured trusted proxy, these caller-supplied values must not
// create fresh rate-limit partitions. All attempts originate from client A's
// effective remote address.
for (var attempt = 0; attempt < 10; attempt++)
{
clientA.DefaultRequestHeaders.Remove("X-Forwarded-For");
clientA.DefaultRequestHeaders.Add("X-Forwarded-For", $"203.0.113.{attempt + 1}");
var response = await SetupHelper.LoginAsync(
clientA,
"unknown-rate-limit-user@itworx.test",
"invalid-password");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
clientA.DefaultRequestHeaders.Remove("X-Forwarded-For");
clientA.DefaultRequestHeaders.Add("X-Forwarded-For", "203.0.113.250");
var exhaustedClientA = await SetupHelper.LoginAsync(
clientA,
"unknown-rate-limit-user@itworx.test",
"invalid-password");
Assert.Equal(HttpStatusCode.TooManyRequests, exhaustedClientA.StatusCode);
using var clientB = Factory.CreateClient();
clientB.DefaultRequestHeaders.Add("X-Test-Remote-Ip", "192.0.2.11");
clientB.DefaultRequestHeaders.Add("X-Forwarded-For", "203.0.113.251");
var clientBLogin = await SetupHelper.LoginAsync(clientB, OwnerEmail, OwnerPassword);
Assert.Equal(HttpStatusCode.OK, clientBLogin.StatusCode);
clientA.DefaultRequestHeaders.Remove("X-Forwarded-For");
clientA.DefaultRequestHeaders.Add("X-Forwarded-For", "203.0.113.252");
var stillExhaustedClientA = await SetupHelper.LoginAsync(
clientA,
"unknown-rate-limit-user@itworx.test",
"invalid-password");
Assert.Equal(HttpStatusCode.TooManyRequests, stillExhaustedClientA.StatusCode);
}
[Fact]
public async Task Repeated_failed_logins_lock_the_account()
{
using var client = Factory.CreateClient();
// Configured lockout threshold is 5 failed attempts.
HttpResponseMessage last = null!;
for (var attempt = 0; attempt < 6; attempt++)
{
last = await SetupHelper.LoginAsync(client, OwnerEmail, "wrong-password-attempt");
}
Assert.Equal(HttpStatusCode.TooManyRequests, last.StatusCode);
// Even the correct password is now rejected while locked out.
var withCorrectPassword = await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
Assert.Equal(HttpStatusCode.TooManyRequests, withCorrectPassword.StatusCode);
}
[Fact]
public async Task Logout_ends_the_session()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var logoutResponse = await SetupHelper.PostWithCsrfAsync<object?>(client, "/api/v1/auth/logout", null);
Assert.Equal(HttpStatusCode.OK, logoutResponse.StatusCode);
var session = await client.GetFromJsonAsync<SessionResponse>("/api/v1/auth/session");
Assert.NotNull(session);
Assert.False(session.Authenticated);
}
}