b3cb9032ac
- RegisterIdentity endpoint (POST /api/identities)
- RequestMfaSetup endpoint (PUT /api/identities/{id}/request-mfa)
- SQL repositories w/ optimistic concurrency (revision tracking)
- Application handlers (IEndpointHandler pattern)
- ValidationException + ProblemDetails error handling
- Unit tests: RegisterIdentityHandlerTests (4), RequestMfaSetupHandlerTests (4)
- Domain state machines integrated (IdentityState lifecycle)
- AGENTS.md v16.0: endpoint authority, idempotency, correlation ID ready
DI registration & integration tests deferred to next session.
17 new files, 500+ LOC, 8/8 unit tests ready to run
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
73 lines
3.1 KiB
C#
73 lines
3.1 KiB
C#
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));
|
|
}
|
|
}
|