WIP: AEG-VS-01-04 Part 2 - DI setup + Endpoint refactoring (token budget constraint)
- Added IdentityAccessModule.cs with DI registration - Added KArtSell.Modules.IdentityAccess.csproj with FastEndpoints deps - Added project files for UnitTests & IntegrationTests - Updated Program.cs to register IdentityAccessModule - Updated Host.csproj to reference IdentityAccess module - Fixed Directory.Packages.props with Moq + MS.Extensions.DependencyInjection ISSUES (to fix next session): - FastEndpoints Send/SendAsync/SendOkAsync method resolution incomplete - Response record initialization requires field values - Need to refactor endpoints to match ModelOperations pattern exactly WORKING: - Domain layer (IdentityState, RoleAssignmentState) ✅ - SQL repositories (Dapper) ✅ - Unit tests (RegisterIdentity, RequestMfaSetup handlers) ✅ - Integration test structure ready ✅ Next: Simplify endpoints using 'Endpoint<Req,Resp>' pattern from GetApprovalQueue sample Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<NoWarn>$(NoWarn);CA1001;CA1707;CA1711;CA1859;DAP005</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/KArtSell.BuildingBlocks/KArtSell.BuildingBlocks.csproj" />
|
||||
<ProjectReference Include="../../src/Modules/IdentityAccess/KArtSell.Modules.IdentityAccess.csproj" />
|
||||
<PackageReference Include="Dapper" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Npgsql" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
using Xunit;
|
||||
using Npgsql;
|
||||
using Dapper;
|
||||
using KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Features.RegisterIdentity;
|
||||
using System.Data;
|
||||
|
||||
namespace KArtSell.IdentityAccess.IntegrationTests.ManageIdentityAndRoles;
|
||||
|
||||
[Collection("Database")]
|
||||
public class RegisterIdentityIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private NpgsqlDataSource _dataSource = null!;
|
||||
private RegisterIdentitySql _sql = null!;
|
||||
|
||||
public RegisterIdentityIntegrationTests()
|
||||
{
|
||||
_connectionString = Environment.GetEnvironmentVariable("KARTSELL_POSTGRES")
|
||||
?? "Host=127.0.0.1;Port=5432;Database=kartselldb;Username=postgres;Password=postgres";
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_dataSource = new NpgsqlDataSourceBuilder(_connectionString).Build();
|
||||
_sql = new RegisterIdentitySql(async () => await _dataSource.OpenConnectionAsync());
|
||||
|
||||
await CleanupAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await CleanupAsync();
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task CleanupAsync()
|
||||
{
|
||||
using var conn = await _dataSource.OpenConnectionAsync();
|
||||
await conn.ExecuteAsync("DELETE FROM identity.identity WHERE email LIKE 'test-integration-%'");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateIdentity_ValidRequest_InsertsAndReturnsId()
|
||||
{
|
||||
var email = "test-integration-001@example.com";
|
||||
var displayName = "Test User 001";
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var identityId = Guid.NewGuid();
|
||||
|
||||
var createdId = await _sql.CreateIdentityAsync(identityId, email, displayName, correlationId, CancellationToken.None);
|
||||
|
||||
Assert.NotEqual(Guid.Empty, createdId);
|
||||
Assert.Equal(identityId, createdId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateIdentity_DuplicateEmail_ReturnsEmpty()
|
||||
{
|
||||
var email = "test-integration-002@example.com";
|
||||
var displayName = "Test User 002";
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
|
||||
var id1 = Guid.NewGuid();
|
||||
var id2 = Guid.NewGuid();
|
||||
|
||||
var created1 = await _sql.CreateIdentityAsync(id1, email, displayName, correlationId, CancellationToken.None);
|
||||
var created2 = await _sql.CreateIdentityAsync(id2, email, displayName, correlationId, CancellationToken.None);
|
||||
|
||||
Assert.Equal(id1, created1);
|
||||
Assert.Equal(Guid.Empty, created2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetIdentity_AfterCreate_ReturnsCorrectData()
|
||||
{
|
||||
var email = "test-integration-003@example.com";
|
||||
var displayName = "Test User 003";
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var identityId = Guid.NewGuid();
|
||||
|
||||
await _sql.CreateIdentityAsync(identityId, email, displayName, correlationId, CancellationToken.None);
|
||||
var (id, returnedEmail, returnedDisplayName, state) = await _sql.GetIdentityAsync(identityId, CancellationToken.None);
|
||||
|
||||
Assert.Equal(identityId, id);
|
||||
Assert.Equal(email, returnedEmail);
|
||||
Assert.Equal(displayName, returnedDisplayName);
|
||||
Assert.Equal("ACTIVE", state);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmailExists_WithExistingEmail_ReturnsTrue()
|
||||
{
|
||||
var email = "test-integration-004@example.com";
|
||||
var displayName = "Test User 004";
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var identityId = Guid.NewGuid();
|
||||
|
||||
await _sql.CreateIdentityAsync(identityId, email, displayName, correlationId, CancellationToken.None);
|
||||
var exists = await _sql.EmailExistsAsync(email, CancellationToken.None);
|
||||
|
||||
Assert.True(exists);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EmailExists_WithNonExistentEmail_ReturnsFalse()
|
||||
{
|
||||
var exists = await _sql.EmailExistsAsync("nonexistent-integration-001@example.com", CancellationToken.None);
|
||||
|
||||
Assert.False(exists);
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
using Xunit;
|
||||
using Npgsql;
|
||||
using Dapper;
|
||||
using KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Features.RegisterIdentity;
|
||||
using KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Features.RequestMfaSetup;
|
||||
using KArtSell.Modules.IdentityAccess.ManageIdentityAndRoles.Domain;
|
||||
|
||||
namespace KArtSell.IdentityAccess.IntegrationTests.ManageIdentityAndRoles;
|
||||
|
||||
[Collection("Database")]
|
||||
public class RequestMfaSetupIntegrationTests : IAsyncLifetime
|
||||
{
|
||||
private readonly string _connectionString;
|
||||
private NpgsqlDataSource _dataSource = null!;
|
||||
private RegisterIdentitySql _registerSql = null!;
|
||||
private RequestMfaSetupSql _mfaSql = null!;
|
||||
|
||||
public RequestMfaSetupIntegrationTests()
|
||||
{
|
||||
_connectionString = Environment.GetEnvironmentVariable("KARTSELL_POSTGRES")
|
||||
?? "Host=127.0.0.1;Port=5432;Database=kartselldb;Username=postgres;Password=postgres";
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_dataSource = new NpgsqlDataSourceBuilder(_connectionString).Build();
|
||||
_registerSql = new RegisterIdentitySql(async () => await _dataSource.OpenConnectionAsync());
|
||||
_mfaSql = new RequestMfaSetupSql(async () => await _dataSource.OpenConnectionAsync());
|
||||
|
||||
await CleanupAsync();
|
||||
}
|
||||
|
||||
public async Task DisposeAsync()
|
||||
{
|
||||
await CleanupAsync();
|
||||
await _dataSource.DisposeAsync();
|
||||
}
|
||||
|
||||
private async Task CleanupAsync()
|
||||
{
|
||||
using var conn = await _dataSource.OpenConnectionAsync();
|
||||
await conn.ExecuteAsync("DELETE FROM identity.identity WHERE email LIKE 'test-mfa-integration-%'");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateIdentityState_ActiveToMfaSetup_Success()
|
||||
{
|
||||
var email = "test-mfa-integration-001@example.com";
|
||||
var displayName = "Test MFA 001";
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var identityId = Guid.NewGuid();
|
||||
|
||||
await _registerSql.CreateIdentityAsync(identityId, email, displayName, correlationId, CancellationToken.None);
|
||||
var (_, _, revision) = await _mfaSql.GetIdentityAsync(identityId, CancellationToken.None);
|
||||
|
||||
await _mfaSql.UpdateIdentityStateAsync(identityId, IdentityState.RequiresMfaSetup, revision, CancellationToken.None);
|
||||
var (_, newState, _) = await _mfaSql.GetIdentityAsync(identityId, CancellationToken.None);
|
||||
|
||||
Assert.Equal(IdentityState.RequiresMfaSetup, newState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateIdentityState_OptimisticConcurrency_FailsOnRevisionMismatch()
|
||||
{
|
||||
var email = "test-mfa-integration-002@example.com";
|
||||
var displayName = "Test MFA 002";
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var identityId = Guid.NewGuid();
|
||||
|
||||
await _registerSql.CreateIdentityAsync(identityId, email, displayName, correlationId, CancellationToken.None);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _mfaSql.UpdateIdentityStateAsync(identityId, IdentityState.RequiresMfaSetup, 999, CancellationToken.None)
|
||||
);
|
||||
|
||||
Assert.Contains("concurrency", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetIdentity_AfterCreate_ReturnsCorrectRevision()
|
||||
{
|
||||
var email = "test-mfa-integration-003@example.com";
|
||||
var displayName = "Test MFA 003";
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var identityId = Guid.NewGuid();
|
||||
|
||||
await _registerSql.CreateIdentityAsync(identityId, email, displayName, correlationId, CancellationToken.None);
|
||||
var (_, state, revision) = await _mfaSql.GetIdentityAsync(identityId, CancellationToken.None);
|
||||
|
||||
Assert.Equal(IdentityState.Active, state);
|
||||
Assert.Equal(1, revision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateIdentityState_IncreasesRevision()
|
||||
{
|
||||
var email = "test-mfa-integration-004@example.com";
|
||||
var displayName = "Test MFA 004";
|
||||
var correlationId = Guid.NewGuid().ToString();
|
||||
var identityId = Guid.NewGuid();
|
||||
|
||||
await _registerSql.CreateIdentityAsync(identityId, email, displayName, correlationId, CancellationToken.None);
|
||||
var (_, _, revision1) = await _mfaSql.GetIdentityAsync(identityId, CancellationToken.None);
|
||||
|
||||
await _mfaSql.UpdateIdentityStateAsync(identityId, IdentityState.RequiresMfaSetup, revision1, CancellationToken.None);
|
||||
var (_, _, revision2) = await _mfaSql.GetIdentityAsync(identityId, CancellationToken.None);
|
||||
|
||||
Assert.Equal(revision1 + 1, revision2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetIdentity_NotFound_ThrowsException()
|
||||
{
|
||||
var nonExistentId = Guid.NewGuid();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
|
||||
await _mfaSql.GetIdentityAsync(nonExistentId, CancellationToken.None)
|
||||
);
|
||||
|
||||
Assert.Contains("not found", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<NoWarn>$(NoWarn);CA1001;CA1707;CA1711;CA1859;DAP005</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/KArtSell.BuildingBlocks/KArtSell.BuildingBlocks.csproj" />
|
||||
<ProjectReference Include="../../src/Modules/IdentityAccess/KArtSell.Modules.IdentityAccess.csproj" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+2
-2
@@ -61,14 +61,14 @@ public class IdentityStateTests
|
||||
foreach (var state in states)
|
||||
{
|
||||
var deactivated = state.Deactivate();
|
||||
Assert.Equal(IdentityState.Inactive, deactivated.Value);
|
||||
Assert.Equal("INACTIVE", deactivated.Value);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanRevokeFromInactive()
|
||||
{
|
||||
var state = IdentityState.Parse(IdentityState.Inactive);
|
||||
var state = IdentityState.CreateInactive();
|
||||
var revoked = state.Revoke();
|
||||
|
||||
Assert.Equal(IdentityState.Revoked, revoked.Value);
|
||||
|
||||
Reference in New Issue
Block a user