c289a698c5
Part 2: Transaction + Outbox Integration - RegisterIdentityEndpoint: DbConnection → DbTransaction → Outbox write - RegisterIdentitySql: Accept NpgsqlConnection + NpgsqlTransaction (Dapper) - Fixed schema references: identity.identity → public.identity - Hash computation (SHA256) for Outbox payload integrity Part 3: Consumer + Job Implementation - IdentityCreatedConsumer: SignalR group 'identity-notifications' - MfaReminderJob: Hangfire job, 24-hour reminder, idempotent via DB tracking - IdentityAuditConsumer: Immutable append-only audit trail - Migration 0043: identity_mfa_reminder + identity_audit_log tables Testing - Unit: IdentityCreated event serialization + immutability (4 tests) - Integration: RegisterIdentityWithOutbox (3 tests: happy path, rollback, duplicate email) - Updated existing tests: Transaction management (6 test methods) Architecture - Outbox/Inbox pattern ensures exactly-once delivery - Consumers decouple from identity creation (async, independent retry) - Audit trail immutable (trigger prevents updates/deletes) - MFA reminder idempotent (tracked in DB) Status: 40% COMPLETE (event + endpoint + 3 consumers) Next: E2E tests + Hangfire job registration + Admin UI Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
149 lines
5.5 KiB
C#
149 lines
5.5 KiB
C#
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 public.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 conn = await _dataSource.OpenConnectionAsync() as NpgsqlConnection
|
|
?? throw new InvalidOperationException("Failed to open connection");
|
|
|
|
await using (conn)
|
|
{
|
|
await using var transaction = await conn.BeginTransactionAsync(IsolationLevel.ReadCommitted);
|
|
var createdId = await _sql.CreateIdentityAsync(conn, transaction, identityId, email, displayName, correlationId, CancellationToken.None);
|
|
await transaction.CommitAsync();
|
|
|
|
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 conn = await _dataSource.OpenConnectionAsync() as NpgsqlConnection
|
|
?? throw new InvalidOperationException("Failed to open connection");
|
|
|
|
await using (conn)
|
|
{
|
|
await using var transaction = await conn.BeginTransactionAsync(IsolationLevel.ReadCommitted);
|
|
var created1 = await _sql.CreateIdentityAsync(conn, transaction, id1, email, displayName, correlationId, CancellationToken.None);
|
|
await transaction.CommitAsync();
|
|
|
|
await using var transaction2 = await conn.BeginTransactionAsync(IsolationLevel.ReadCommitted);
|
|
var created2 = await _sql.CreateIdentityAsync(conn, transaction2, id2, email, displayName, correlationId, CancellationToken.None);
|
|
await transaction2.CommitAsync();
|
|
|
|
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();
|
|
|
|
var conn = await _dataSource.OpenConnectionAsync() as NpgsqlConnection
|
|
?? throw new InvalidOperationException("Failed to open connection");
|
|
|
|
await using (conn)
|
|
{
|
|
await using var transaction = await conn.BeginTransactionAsync(IsolationLevel.ReadCommitted);
|
|
await _sql.CreateIdentityAsync(conn, transaction, identityId, email, displayName, correlationId, CancellationToken.None);
|
|
await transaction.CommitAsync();
|
|
|
|
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();
|
|
|
|
var conn = await _dataSource.OpenConnectionAsync() as NpgsqlConnection
|
|
?? throw new InvalidOperationException("Failed to open connection");
|
|
|
|
await using (conn)
|
|
{
|
|
await using var transaction = await conn.BeginTransactionAsync(IsolationLevel.ReadCommitted);
|
|
await _sql.CreateIdentityAsync(conn, transaction, identityId, email, displayName, correlationId, CancellationToken.None);
|
|
await transaction.CommitAsync();
|
|
|
|
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);
|
|
}
|
|
}
|