Auth tweaks, Audit implementation, Webhooks

This commit is contained in:
2026-04-13 14:17:45 -07:00
parent b9a04df861
commit 334b6cf3e1
46 changed files with 4654 additions and 61 deletions

View File

@@ -0,0 +1,185 @@
using System.Net;
using System.Net.Http.Json;
using MicCheck.Api.Authorization;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Authorization;
[TestFixture]
public class AuthEndpointsTests
{
private MicCheckWebApplicationFactory _factory = null!;
[SetUp]
public void SetUp()
{
_factory = new MicCheckWebApplicationFactory();
}
[TearDown]
public void TearDown() => _factory.Dispose();
[Test]
public async Task WhenRegisteringWithValidDetails_ThenOkIsReturnedWithTokens()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task WhenRegisteringWithDuplicateEmail_ThenConflictIsReturned()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "SecurePass1!", "Alice", "Smith", "Acme Corp"));
var response = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"alice@example.com", "AnotherPass1!", "Alice", "Jones", "Other Corp"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Conflict));
}
[Test]
public async Task WhenLoginWithValidCredentials_ThenOkIsReturnedWithTokens()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"bob@example.com", "SecurePass1!", "Bob", "Jones", "BobCo"));
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("bob@example.com", "SecurePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.Null.And.Not.Empty);
}
[Test]
public async Task WhenLoginWithWrongPassword_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"carol@example.com", "CorrectPass1!", "Carol", "White", "CarolCo"));
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("carol@example.com", "WrongPassword"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoginWithUnknownEmail_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("nobody@example.com", "SomePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoginWithEmptyEmail_ThenBadRequestIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/login",
new LoginRequest("", "SomePass1!"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
[Test]
public async Task WhenRefreshingWithValidToken_ThenOkIsReturnedWithNewTokens()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"dave@example.com", "SecurePass1!", "Dave", "Brown", "DaveCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody!.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var body = await response.Content.ReadFromJsonAsync<LoginResponse>();
Assert.That(body?.AccessToken, Is.Not.Null.And.Not.Empty);
Assert.That(body?.RefreshToken, Is.Not.EqualTo(registerBody.RefreshToken));
}
[Test]
public async Task WhenRefreshingWithInvalidToken_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest("not-a-valid-refresh-token"));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenRefreshingWithRevokedToken_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"eve@example.com", "SecurePass1!", "Eve", "Davis", "EveCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody!.RefreshToken));
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
[Test]
public async Task WhenLoggingOut_ThenNoContentIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"frank@example.com", "SecurePass1!", "Frank", "Green", "FrankCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
var response = await client.PostAsJsonAsync("/api/v1/auth/logout",
new LogoutRequest(registerBody!.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
}
[Test]
public async Task WhenLoggingOutAndRefreshing_ThenUnauthorizedIsReturned()
{
var client = _factory.CreateClient();
var registerResponse = await client.PostAsJsonAsync("/api/v1/auth/register", new RegisterRequest(
"grace@example.com", "SecurePass1!", "Grace", "Black", "GraceCo"));
var registerBody = await registerResponse.Content.ReadFromJsonAsync<LoginResponse>();
await client.PostAsJsonAsync("/api/v1/auth/logout",
new LogoutRequest(registerBody!.RefreshToken));
var response = await client.PostAsJsonAsync("/api/v1/auth/refresh",
new RefreshRequest(registerBody.RefreshToken));
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized));
}
}

View File

@@ -0,0 +1,103 @@
using System.IdentityModel.Tokens.Jwt;
using MicCheck.Api.Authorization;
using MicCheck.Api.Organizations;
using MicCheck.Api.Users;
using Microsoft.Extensions.Configuration;
using NUnit.Framework;
namespace MicCheck.Api.Tests.Unit.Authorization;
[TestFixture]
public class TokenServiceTests
{
private IConfiguration _configuration = null!;
private TokenService _tokenService = null!;
[SetUp]
public void SetUp()
{
_configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Jwt:SecretKey"] = "test-secret-key-that-is-long-enough-32c",
["Jwt:Issuer"] = "MicCheck",
["Jwt:Audience"] = "MicCheck",
["Jwt:ExpiryMinutes"] = "60",
})
.Build();
_tokenService = new TokenService(_configuration);
}
private static User BuildUser(int id = 1, string email = "alice@example.com") =>
new()
{
Email = email,
FirstName = "Alice",
LastName = "Smith",
PasswordHash = "hashed",
IsActive = true,
CreatedAt = DateTimeOffset.UtcNow
};
[Test]
public void WhenAUserIsProvided_ThenATokenIsReturned()
{
var result = _tokenService.GenerateToken(BuildUser());
Assert.That(result.Token, Is.Not.Null.And.Not.Empty);
}
[Test]
public void WhenATokenIsGenerated_ThenItContainsTheUserEmailClaim()
{
var user = BuildUser(email: "bob@example.com");
var result = _tokenService.GenerateToken(user);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Claims.Any(c => c.Type == JwtRegisteredClaimNames.Email && c.Value == "bob@example.com"),
Is.True);
}
[Test]
public void WhenATokenIsGenerated_ThenItHasAFutureExpiry()
{
var result = _tokenService.GenerateToken(BuildUser());
Assert.That(result.ExpiresAt, Is.GreaterThan(DateTime.UtcNow));
}
[Test]
public void WhenATokenIsGenerated_ThenItHasTheConfiguredIssuer()
{
var result = _tokenService.GenerateToken(BuildUser());
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Issuer, Is.EqualTo("MicCheck"));
}
[Test]
public void WhenUserBelongsToOrganizations_ThenTokenContainsOrganizationClaims()
{
var user = BuildUser();
user.Organizations.Add(new OrganizationUser
{
OrganizationId = 42,
UserId = user.Id,
Role = OrganizationRole.Admin
});
var result = _tokenService.GenerateToken(user);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(result.Token);
Assert.That(jwt.Claims.Any(c => c.Type == "OrganizationId" && c.Value == "42"), Is.True);
Assert.That(jwt.Claims.Any(c => c.Type == "OrganizationRole" && c.Value == "Admin"), Is.True);
}
}