feat(wbs): AEG-VS-01-04 BE Vertical Slice - Part 2 Complete (DI + Endpoints + Tests)
✅ Part 1: Domain layer (IdentityState, RoleAssignmentState) ✅ Part 2: DI setup + Endpoints + Integration tests CHANGES: - Fixed FastEndpoints API: Send.OkAsync() pattern (was SendOkAsync) - Removed Handler layer (simplified to endpoint-only pattern) - Updated Response records with default field values - Added IdentityAccessModule.cs for DI registration - Added unit test projects + integration test projects - Fixed TypeScript error in useFormFieldNavigation (HTMLElement[] cast) - Removed old Handler test files ARCHITECTURE: Endpoint (FastEndpoints) → IRegisterIdentitySql/IRequestMfaSetupSql (Dapper) → Domain state machines (IdentityState, RoleAssignmentState) → PostgreSQL (optimistic concurrency via revision_version) BUILD: ✅ SUCCESS (0 errors, 0 warnings, 59 seconds) TESTS: ✅ READY (IdentityStateTests 9, integration tests 10) Endpoints: - POST /api/identities (RegisterIdentity) - PUT /api/identities/{id}/request-mfa (RequestMfaSetup) AGENTS.md v16.0 Compliance: ✅ Endpoint authority (validation in endpoint) ✅ Optimistic concurrency (revision tracking) ✅ Error handling (Send.StatusCodeAsync) ✅ Domain-driven state machines ✅ Dapper SQL with ON CONFLICT patterns S1 Progress: 4/7 (57%) - 01-01 ✅ Policy/Scope - 01-02 ✅ Identity Data Contract - 01-03 ✅ Domain Policy - 01-04 ✅ BE Vertical Slice (COMPLETE) - 01-05/06/07 ⏳ Remaining slices Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
-81
@@ -1,81 +0,0 @@
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Features.RegisterIdentity;
|
||||
|
||||
namespace KArtSell.IdentityAccess.UnitTests.ManageIdentityAndRoles;
|
||||
|
||||
public class RegisterIdentityHandlerTests
|
||||
{
|
||||
private readonly Mock<IRegisterIdentitySql> _sqlMock;
|
||||
private readonly RegisterIdentityHandler _handler;
|
||||
|
||||
public RegisterIdentityHandlerTests()
|
||||
{
|
||||
_sqlMock = new Mock<IRegisterIdentitySql>();
|
||||
_handler = new RegisterIdentityHandler(_sqlMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_ValidRequest_CreatesIdentity()
|
||||
{
|
||||
var request = new RegisterIdentityRequest
|
||||
{
|
||||
Email = "test@example.com",
|
||||
DisplayName = "Test User"
|
||||
};
|
||||
|
||||
var identityId = Guid.NewGuid();
|
||||
_sqlMock.Setup(s => s.EmailExistsAsync("test@example.com", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(false);
|
||||
_sqlMock.Setup(s => s.CreateIdentityAsync(It.IsAny<Guid>(), "test@example.com", "Test User", It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(identityId);
|
||||
_sqlMock.Setup(s => s.GetIdentityAsync(identityId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((identityId, "test@example.com", "Test User", Domain.IdentityState.Active));
|
||||
|
||||
var response = await _handler.HandleAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(response);
|
||||
Assert.Equal(identityId, response.Id);
|
||||
Assert.Equal("test@example.com", response.Email);
|
||||
Assert.Equal(Domain.IdentityState.Active, response.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_InvalidEmail_ThrowsValidationException()
|
||||
{
|
||||
var request = new RegisterIdentityRequest
|
||||
{
|
||||
Email = "invalid-email",
|
||||
DisplayName = "Test User"
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<ValidationException>(() => _handler.HandleAsync(request, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_EmailExists_ThrowsValidationException()
|
||||
{
|
||||
var request = new RegisterIdentityRequest
|
||||
{
|
||||
Email = "existing@example.com",
|
||||
DisplayName = "Test User"
|
||||
};
|
||||
|
||||
_sqlMock.Setup(s => s.EmailExistsAsync("existing@example.com", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
await Assert.ThrowsAsync<ValidationException>(() => _handler.HandleAsync(request, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_EmptyDisplayName_ThrowsValidationException()
|
||||
{
|
||||
var request = new RegisterIdentityRequest
|
||||
{
|
||||
Email = "test@example.com",
|
||||
DisplayName = ""
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<ValidationException>(() => _handler.HandleAsync(request, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Features.RequestMfaSetup;
|
||||
using KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Domain;
|
||||
|
||||
namespace KArtSell.IdentityAccess.UnitTests.ManageIdentityAndRoles;
|
||||
|
||||
public class RequestMfaSetupHandlerTests
|
||||
{
|
||||
private readonly Mock<IRequestMfaSetupSql> _sqlMock;
|
||||
private readonly RequestMfaSetupHandler _handler;
|
||||
|
||||
public RequestMfaSetupHandlerTests()
|
||||
{
|
||||
_sqlMock = new Mock<IRequestMfaSetupSql>();
|
||||
_handler = new RequestMfaSetupHandler(_sqlMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_ActiveIdentity_TransitionsToRequiresMfaSetup()
|
||||
{
|
||||
var identityId = Guid.NewGuid();
|
||||
var request = new RequestMfaSetupRequest { IdentityId = identityId };
|
||||
|
||||
_sqlMock.Setup(s => s.GetIdentityAsync(identityId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((identityId, IdentityState.Active, 1));
|
||||
_sqlMock.Setup(s => s.UpdateIdentityStateAsync(identityId, IdentityState.RequiresMfaSetup, 1, It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var response = await _handler.HandleAsync(request, CancellationToken.None);
|
||||
|
||||
Assert.Equal(identityId, response.IdentityId);
|
||||
Assert.Equal(IdentityState.Active, response.PreviousState);
|
||||
Assert.Equal(IdentityState.RequiresMfaSetup, response.NewState);
|
||||
|
||||
_sqlMock.Verify(s => s.UpdateIdentityStateAsync(identityId, IdentityState.RequiresMfaSetup, 1, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_EmptyIdentityId_ThrowsValidationException()
|
||||
{
|
||||
var request = new RequestMfaSetupRequest { IdentityId = Guid.Empty };
|
||||
|
||||
await Assert.ThrowsAsync<ValidationException>(() => _handler.HandleAsync(request, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_IdentityNotFound_ThrowsInvalidOperationException()
|
||||
{
|
||||
var identityId = Guid.NewGuid();
|
||||
var request = new RequestMfaSetupRequest { IdentityId = identityId };
|
||||
|
||||
_sqlMock.Setup(s => s.GetIdentityAsync(identityId, It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new InvalidOperationException("Identity not found"));
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _handler.HandleAsync(request, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_OptimisticConcurrencyViolation_ThrowsInvalidOperationException()
|
||||
{
|
||||
var identityId = Guid.NewGuid();
|
||||
var request = new RequestMfaSetupRequest { IdentityId = identityId };
|
||||
|
||||
_sqlMock.Setup(s => s.GetIdentityAsync(identityId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((identityId, IdentityState.Active, 1));
|
||||
_sqlMock.Setup(s => s.UpdateIdentityStateAsync(identityId, IdentityState.RequiresMfaSetup, 1, It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new InvalidOperationException("Optimistic concurrency violation: state changed"));
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _handler.HandleAsync(request, CancellationToken.None));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user