feat: add audited one-shot prepare gate
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
#nullable enable
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;
|
||||
|
||||
internal sealed class DenyAllDatabaseMutationAuthorization
|
||||
: IDatabaseMutationAuthorization
|
||||
{
|
||||
internal List<DatabaseMutationAuthorizationRequest> Requests { get; } = [];
|
||||
|
||||
public bool IsAuthorized(DatabaseMutationAuthorizationRequest request)
|
||||
{
|
||||
Requests.Add(request);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -290,8 +290,37 @@ public sealed class OperatorCatalogMutationExecutorTests
|
||||
Assert.Equal(0, plan.BeginCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteTransactionAsync_DeniedAuthorizationCreatesNoConnectionOrTransaction()
|
||||
{
|
||||
var plan = new CatalogMutationConnectionPlan();
|
||||
var factory = new CatalogMutationConnectionFactory(plan);
|
||||
var authorization = new DenyAllDatabaseMutationAuthorization();
|
||||
var service = new LegacyExpertCatalogPersistenceService(
|
||||
new CatalogNoQueryExecutor(),
|
||||
CreateExecutor(factory, authorization));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<OperatorCatalogMutationException>(() =>
|
||||
service.CreateAsync(new ExpertCatalogDefinition(
|
||||
new ExpertSelectionIdentity("0001", "Expert"),
|
||||
[])));
|
||||
|
||||
Assert.False(exception.OutcomeUnknown);
|
||||
var denied = Assert.IsType<DatabaseMutationAuthorizationException>(
|
||||
exception.InnerException);
|
||||
Assert.Equal(DatabaseMutationTarget.OperatorCatalog, denied.Request.Target);
|
||||
Assert.Equal(DataSourceKind.Oracle, denied.Request.Source);
|
||||
Assert.Equal(nameof(OperatorCatalogMutationKind.CreateExpert), denied.Request.MutationKind);
|
||||
Assert.NotEqual(Guid.Empty, denied.Request.OperationId);
|
||||
Assert.Single(authorization.Requests);
|
||||
Assert.Equal(0, factory.CreateCount);
|
||||
Assert.Equal(0, plan.BeginCount);
|
||||
Assert.Empty(plan.Commands);
|
||||
}
|
||||
|
||||
private static OperatorCatalogMutationExecutor CreateExecutor(
|
||||
IDatabaseConnectionFactory factory) =>
|
||||
IDatabaseConnectionFactory factory,
|
||||
IDatabaseMutationAuthorization? mutationAuthorization = null) =>
|
||||
new(
|
||||
factory,
|
||||
new DatabaseResilienceOptions
|
||||
@@ -301,7 +330,8 @@ public sealed class OperatorCatalogMutationExecutorTests
|
||||
InitialRetryDelayMilliseconds = 1
|
||||
},
|
||||
new StubErrorDetector(transient: true),
|
||||
static (_, _) => { });
|
||||
static (_, _) => { },
|
||||
mutationAuthorization ?? DatabaseMutationAuthorization.AllowAll);
|
||||
|
||||
private static ObservedCatalogMutationParameter Parameter(
|
||||
ObservedCatalogMutationCommand command,
|
||||
|
||||
@@ -68,6 +68,31 @@ public sealed class OracleManualFinancialMutationExecutorTests
|
||||
Assert.True(plan.TransactionDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeniedAuthorizationCreatesNoConnectionOrTransaction()
|
||||
{
|
||||
var plan = new Plan();
|
||||
var factory = new Factory(plan);
|
||||
var authorization = new DenyAllDatabaseMutationAuthorization();
|
||||
var service = Service(Executor(factory, mutationAuthorization: authorization));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<ManualFinancialMutationException>(() =>
|
||||
service.CreateAsync(Sales("Alpha")));
|
||||
|
||||
Assert.False(exception.OutcomeUnknown);
|
||||
var denied = Assert.IsType<DatabaseMutationAuthorizationException>(
|
||||
exception.InnerException);
|
||||
Assert.Equal(DatabaseMutationTarget.ManualFinancial, denied.Request.Target);
|
||||
Assert.Equal(DataSourceKind.Oracle, denied.Request.Source);
|
||||
Assert.Equal(nameof(ManualFinancialMutationKind.Create), denied.Request.MutationKind);
|
||||
Assert.NotEqual(Guid.Empty, denied.Request.OperationId);
|
||||
Assert.Single(authorization.Requests);
|
||||
Assert.Equal(0, factory.CreateCount);
|
||||
Assert.Equal(0, plan.OpenCount);
|
||||
Assert.Equal(0, plan.BeginCount);
|
||||
Assert.Empty(plan.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zero_affected_create_rolls_back_as_known_duplicate_before_commit()
|
||||
{
|
||||
@@ -141,7 +166,8 @@ public sealed class OracleManualFinancialMutationExecutorTests
|
||||
|
||||
private static OracleManualFinancialMutationExecutor Executor(
|
||||
Factory factory,
|
||||
int operationTimeoutSeconds = 30) =>
|
||||
int operationTimeoutSeconds = 30,
|
||||
IDatabaseMutationAuthorization? mutationAuthorization = null) =>
|
||||
new(
|
||||
factory,
|
||||
new DatabaseResilienceOptions
|
||||
@@ -151,7 +177,8 @@ public sealed class OracleManualFinancialMutationExecutorTests
|
||||
InitialRetryDelayMilliseconds = 30_000
|
||||
},
|
||||
new StubErrorDetector(false),
|
||||
command => Assert.IsType<FakeCommand>(command).BindByName = true);
|
||||
command => Assert.IsType<FakeCommand>(command).BindByName = true,
|
||||
mutationAuthorization ?? DatabaseMutationAuthorization.AllowAll);
|
||||
|
||||
private static ManualSalesRecord Sales(string stockName) =>
|
||||
new(
|
||||
|
||||
@@ -283,6 +283,31 @@ public sealed class OracleNamedPlaylistMutationExecutorTests
|
||||
Assert.Empty(plan.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeniedAuthorizationCreatesNoConnectionOrTransaction()
|
||||
{
|
||||
var plan = new MutationPlan();
|
||||
var factory = new MutationConnectionFactory(plan);
|
||||
var authorization = new DenyAllDatabaseMutationAuthorization();
|
||||
var service = CreateService(CreateExecutor(factory, mutationAuthorization: authorization));
|
||||
|
||||
var exception = await Assert.ThrowsAsync<NamedPlaylistMutationException>(
|
||||
() => service.CreateDefinitionAsync("00000011", "Morning"));
|
||||
|
||||
Assert.False(exception.OutcomeUnknown);
|
||||
var denied = Assert.IsType<DatabaseMutationAuthorizationException>(
|
||||
exception.InnerException);
|
||||
Assert.Equal(DatabaseMutationTarget.NamedPlaylist, denied.Request.Target);
|
||||
Assert.Equal(DataSourceKind.Oracle, denied.Request.Source);
|
||||
Assert.Equal(nameof(NamedPlaylistMutationKind.CreateDefinition), denied.Request.MutationKind);
|
||||
Assert.NotEqual(Guid.Empty, denied.Request.OperationId);
|
||||
Assert.Single(authorization.Requests);
|
||||
Assert.Equal(0, factory.CreateCount);
|
||||
Assert.Equal(0, plan.OpenCount);
|
||||
Assert.Equal(0, plan.BeginCount);
|
||||
Assert.Empty(plan.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_RejectsInvalidCommandTimeout()
|
||||
{
|
||||
@@ -296,7 +321,8 @@ public sealed class OracleNamedPlaylistMutationExecutorTests
|
||||
|
||||
private static OracleNamedPlaylistMutationExecutor CreateExecutor(
|
||||
MutationConnectionFactory factory,
|
||||
int operationTimeoutSeconds = 30) =>
|
||||
int operationTimeoutSeconds = 30,
|
||||
IDatabaseMutationAuthorization? mutationAuthorization = null) =>
|
||||
new(
|
||||
factory,
|
||||
new DatabaseResilienceOptions
|
||||
@@ -306,7 +332,8 @@ public sealed class OracleNamedPlaylistMutationExecutorTests
|
||||
InitialRetryDelayMilliseconds = 30_000
|
||||
},
|
||||
new TransientDatabaseErrorDetector(),
|
||||
command => Assert.IsType<MutationFakeCommand>(command).BindByName = true);
|
||||
command => Assert.IsType<MutationFakeCommand>(command).BindByName = true,
|
||||
mutationAuthorization ?? DatabaseMutationAuthorization.AllowAll);
|
||||
|
||||
private static LegacyNamedPlaylistPersistenceService CreateService(
|
||||
INamedPlaylistMutationExecutor mutations) =>
|
||||
|
||||
Reference in New Issue
Block a user