765cc632d5
Remove the Order demo (entity/feature/repo/config/gRPC/proto) and the three pre-marketplace migrations; regenerate a fresh InitialBaseline migration. Stand up the REST surface (PingController + System/Ping CQRS) proving the Mediator -> behaviors -> OperationResult -> ApiResult envelope end to end. Close wiring gaps: register LoggingBehavior (outermost) and add the built-in rate limiter (per-IP global + otp/auth/sensitive policies), placed before authentication. Add current-user + audit plumbing: ICurrentUser (HttpContext + null impls), rename BaseEntity audit fields to CreatedAt/ModifiedAt (DateTimeOffset) + CreatedById/ModifiedById, stamped by a new AuditFieldInterceptor. Introduce five cross-cutting seams (IDateTimeProvider, IFieldEncryptor, ICacheService, IObjectStorage, INotificationDispatcher) with in-memory/local mocks registered via AddCrossCuttingSeams. Add Baya.Test.Foundation (encryptor, audit interceptor, ping handler) and update docs, contracts (swagger.v1.json), handoff, report, and mocks registry.
96 lines
3.1 KiB
C#
96 lines
3.1 KiB
C#
using Baya.Application.Contracts.Common;
|
|
using Baya.Domain.Common;
|
|
using Baya.Infrastructure.Persistence.Interceptors;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NSubstitute;
|
|
|
|
namespace Baya.Test.Foundation;
|
|
|
|
public class AuditFieldInterceptorTests
|
|
{
|
|
// A throwaway auditable entity + context so the interceptor is tested in isolation, without the
|
|
// full Identity schema.
|
|
private sealed class Widget : BaseEntity
|
|
{
|
|
public string Name { get; set; } = string.Empty;
|
|
}
|
|
|
|
private sealed class AuditTestDbContext(DbContextOptions options) : DbContext(options)
|
|
{
|
|
public DbSet<Widget> Widgets => Set<Widget>();
|
|
}
|
|
|
|
private static AuditTestDbContext CreateContext(AuditFieldInterceptor interceptor)
|
|
{
|
|
var connection = new SqliteConnection("DataSource=:memory:");
|
|
connection.Open();
|
|
|
|
var options = new DbContextOptionsBuilder<AuditTestDbContext>()
|
|
.UseSqlite(connection)
|
|
.AddInterceptors(interceptor)
|
|
.Options;
|
|
|
|
var context = new AuditTestDbContext(options);
|
|
context.Database.EnsureCreated();
|
|
return context;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SavingChanges_OnAdd_StampsCreatedAtAndCreatedById()
|
|
{
|
|
// Arrange
|
|
var fixedNow = new DateTimeOffset(2026, 6, 28, 10, 0, 0, TimeSpan.Zero);
|
|
|
|
var clock = Substitute.For<IDateTimeProvider>();
|
|
clock.UtcNow.Returns(fixedNow);
|
|
|
|
var currentUser = Substitute.For<ICurrentUser>();
|
|
currentUser.UserId.Returns(42);
|
|
|
|
await using var context = CreateContext(new AuditFieldInterceptor(currentUser, clock));
|
|
var widget = new Widget { Name = "test" };
|
|
context.Widgets.Add(widget);
|
|
|
|
// Act
|
|
await context.SaveChangesAsync();
|
|
|
|
// Assert
|
|
Assert.Equal(fixedNow, widget.CreatedAt);
|
|
Assert.Equal(fixedNow, widget.ModifiedAt);
|
|
Assert.Equal(42, widget.CreatedById);
|
|
Assert.Equal(42, widget.ModifiedById);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SavingChanges_OnUpdate_StampsModifiedButNotCreatedBy()
|
|
{
|
|
// Arrange
|
|
var createNow = new DateTimeOffset(2026, 6, 28, 10, 0, 0, TimeSpan.Zero);
|
|
var updateNow = new DateTimeOffset(2026, 6, 28, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
var clock = Substitute.For<IDateTimeProvider>();
|
|
var currentUser = Substitute.For<ICurrentUser>();
|
|
|
|
clock.UtcNow.Returns(createNow);
|
|
currentUser.UserId.Returns(7);
|
|
|
|
await using var context = CreateContext(new AuditFieldInterceptor(currentUser, clock));
|
|
var widget = new Widget { Name = "before" };
|
|
context.Widgets.Add(widget);
|
|
await context.SaveChangesAsync();
|
|
|
|
// Act — a different user edits the row later.
|
|
clock.UtcNow.Returns(updateNow);
|
|
currentUser.UserId.Returns(9);
|
|
widget.Name = "after";
|
|
await context.SaveChangesAsync();
|
|
|
|
// Assert
|
|
Assert.Equal(createNow, widget.CreatedAt);
|
|
Assert.Equal(7, widget.CreatedById);
|
|
Assert.Equal(updateNow, widget.ModifiedAt);
|
|
Assert.Equal(9, widget.ModifiedById);
|
|
}
|
|
}
|