|
|
|
@@ -0,0 +1,213 @@
|
|
|
|
|
namespace HrynCo.DAL.EF.Tests;
|
|
|
|
|
|
|
|
|
|
using HrynCo.DAL.Abstract;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using Xunit;
|
|
|
|
|
|
|
|
|
|
public sealed class EfUnitOfWorkTests
|
|
|
|
|
{
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData(false)]
|
|
|
|
|
[InlineData(true)]
|
|
|
|
|
public async Task ActionFailure_ReportsBothExceptionsWhenRollbackFails(bool returnsResult)
|
|
|
|
|
{
|
|
|
|
|
var original = new InvalidOperationException("Action failed.");
|
|
|
|
|
var rollbackFailure = new InvalidOperationException("Transaction completed.");
|
|
|
|
|
var interceptor = new TransactionFailureInterceptor
|
|
|
|
|
{
|
|
|
|
|
RollbackException = rollbackFailure
|
|
|
|
|
};
|
|
|
|
|
await using DbContext context = await CreateContextAsync(interceptor);
|
|
|
|
|
var unitOfWork = new TestUnitOfWork(context);
|
|
|
|
|
|
|
|
|
|
AggregateException actual = await Assert.ThrowsAsync<AggregateException>(() =>
|
|
|
|
|
ExecuteAsync(unitOfWork, returnsResult, () => Task.FromException(original)));
|
|
|
|
|
|
|
|
|
|
Assert.Collection(actual.InnerExceptions,
|
|
|
|
|
error => Assert.Same(original, error),
|
|
|
|
|
error => Assert.Same(rollbackFailure, error));
|
|
|
|
|
Assert.Equal(1, interceptor.RollbackAttempts);
|
|
|
|
|
Assert.Null(unitOfWork.GetCurrentTransaction());
|
|
|
|
|
Assert.Null(context.Database.CurrentTransaction);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData(false)]
|
|
|
|
|
[InlineData(true)]
|
|
|
|
|
public async Task CommitFailure_ReportsBothExceptionsWhenRollbackFails(bool returnsResult)
|
|
|
|
|
{
|
|
|
|
|
var original = new InvalidOperationException("Commit failed.");
|
|
|
|
|
var rollbackFailure = new InvalidOperationException("Transaction completed.");
|
|
|
|
|
var interceptor = new TransactionFailureInterceptor
|
|
|
|
|
{
|
|
|
|
|
CommitException = original,
|
|
|
|
|
RollbackException = rollbackFailure
|
|
|
|
|
};
|
|
|
|
|
await using DbContext context = await CreateContextAsync(interceptor);
|
|
|
|
|
var unitOfWork = new TestUnitOfWork(context);
|
|
|
|
|
|
|
|
|
|
AggregateException actual = await Assert.ThrowsAsync<AggregateException>(() =>
|
|
|
|
|
ExecuteAsync(unitOfWork, returnsResult, () => Task.CompletedTask));
|
|
|
|
|
|
|
|
|
|
Assert.Collection(actual.InnerExceptions,
|
|
|
|
|
error => Assert.Same(original, error),
|
|
|
|
|
error => Assert.Same(rollbackFailure, error));
|
|
|
|
|
Assert.Equal(1, interceptor.CommitAttempts);
|
|
|
|
|
Assert.Equal(1, interceptor.RollbackAttempts);
|
|
|
|
|
Assert.Null(unitOfWork.GetCurrentTransaction());
|
|
|
|
|
Assert.Null(context.Database.CurrentTransaction);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData(false)]
|
|
|
|
|
[InlineData(true)]
|
|
|
|
|
public async Task CompletedTransaction_ReportsActionAndRollbackExceptions(bool returnsResult)
|
|
|
|
|
{
|
|
|
|
|
await using DbContext context = await CreateContextAsync();
|
|
|
|
|
var unitOfWork = new TestUnitOfWork(context);
|
|
|
|
|
var original = new InvalidOperationException("Action failed after the provider completed the transaction.");
|
|
|
|
|
|
|
|
|
|
AggregateException actual = await Assert.ThrowsAsync<AggregateException>(() =>
|
|
|
|
|
ExecuteAsync(unitOfWork, returnsResult, async () =>
|
|
|
|
|
{
|
|
|
|
|
await context.Database.CurrentTransaction!.CommitAsync();
|
|
|
|
|
throw original;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
Assert.Same(original, actual.InnerExceptions[0]);
|
|
|
|
|
Assert.IsType<InvalidOperationException>(actual.InnerExceptions[1]);
|
|
|
|
|
Assert.Null(unitOfWork.GetCurrentTransaction());
|
|
|
|
|
Assert.Null(context.Database.CurrentTransaction);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData(false)]
|
|
|
|
|
[InlineData(true)]
|
|
|
|
|
public async Task ActionFailure_RollsBackWritesAndAllowsNextTransaction(bool returnsResult)
|
|
|
|
|
{
|
|
|
|
|
await using DbContext context = await CreateContextAsync();
|
|
|
|
|
var unitOfWork = new TestUnitOfWork(context);
|
|
|
|
|
var original = new InvalidOperationException("Action failed.");
|
|
|
|
|
|
|
|
|
|
Exception actual = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
|
|
|
ExecuteAsync(unitOfWork, returnsResult, async () =>
|
|
|
|
|
{
|
|
|
|
|
await context.Database.ExecuteSqlRawAsync("INSERT INTO records VALUES (1)");
|
|
|
|
|
throw original;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
Assert.Same(original, actual);
|
|
|
|
|
Assert.Equal(0, await CountRecordsAsync(context));
|
|
|
|
|
Assert.Null(unitOfWork.GetCurrentTransaction());
|
|
|
|
|
await ExecuteAsync(unitOfWork, returnsResult,
|
|
|
|
|
() => context.Database.ExecuteSqlRawAsync("INSERT INTO records VALUES (2)"));
|
|
|
|
|
Assert.Equal(1, await CountRecordsAsync(context));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData(false)]
|
|
|
|
|
[InlineData(true)]
|
|
|
|
|
public async Task Success_CommitsWritesAndCreatesFreshTransactionForNextCall(bool returnsResult)
|
|
|
|
|
{
|
|
|
|
|
var interceptor = new TransactionFailureInterceptor();
|
|
|
|
|
await using DbContext context = await CreateContextAsync(interceptor);
|
|
|
|
|
var unitOfWork = new TestUnitOfWork(context);
|
|
|
|
|
ITransaction? first = null;
|
|
|
|
|
|
|
|
|
|
await ExecuteAsync(unitOfWork, returnsResult, async () =>
|
|
|
|
|
{
|
|
|
|
|
first = unitOfWork.GetCurrentTransaction();
|
|
|
|
|
await context.Database.ExecuteSqlRawAsync("INSERT INTO records VALUES (1)");
|
|
|
|
|
});
|
|
|
|
|
Assert.Null(unitOfWork.GetCurrentTransaction());
|
|
|
|
|
Assert.Null(context.Database.CurrentTransaction);
|
|
|
|
|
await ExecuteAsync(unitOfWork, returnsResult, async () =>
|
|
|
|
|
{
|
|
|
|
|
Assert.NotSame(first, unitOfWork.GetCurrentTransaction());
|
|
|
|
|
await context.Database.ExecuteSqlRawAsync("INSERT INTO records VALUES (2)");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Assert.Equal(2, await CountRecordsAsync(context));
|
|
|
|
|
Assert.Equal(2, interceptor.CommitAttempts);
|
|
|
|
|
Assert.Equal(0, interceptor.RollbackAttempts);
|
|
|
|
|
Assert.Null(unitOfWork.GetCurrentTransaction());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData(false)]
|
|
|
|
|
[InlineData(true)]
|
|
|
|
|
public async Task CallerOwnedTransaction_SuccessLeavesLifecycleToCaller(bool returnsResult)
|
|
|
|
|
{
|
|
|
|
|
var interceptor = new TransactionFailureInterceptor();
|
|
|
|
|
await using DbContext context = await CreateContextAsync(interceptor);
|
|
|
|
|
var unitOfWork = new TestUnitOfWork(context);
|
|
|
|
|
await using ITransaction transaction = await unitOfWork.BeginTransactionAsync();
|
|
|
|
|
|
|
|
|
|
await ExecuteAsync(unitOfWork, returnsResult,
|
|
|
|
|
() => context.Database.ExecuteSqlRawAsync("INSERT INTO records VALUES (1)"));
|
|
|
|
|
|
|
|
|
|
Assert.Same(transaction, unitOfWork.GetCurrentTransaction());
|
|
|
|
|
Assert.NotNull(context.Database.CurrentTransaction);
|
|
|
|
|
Assert.Equal(0, interceptor.CommitAttempts);
|
|
|
|
|
Assert.Equal(0, interceptor.RollbackAttempts);
|
|
|
|
|
await transaction.RollbackAsync();
|
|
|
|
|
Assert.Equal(0, await CountRecordsAsync(context));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData(false)]
|
|
|
|
|
[InlineData(true)]
|
|
|
|
|
public async Task CallerOwnedTransaction_FailureLeavesLifecycleToCaller(bool returnsResult)
|
|
|
|
|
{
|
|
|
|
|
var interceptor = new TransactionFailureInterceptor();
|
|
|
|
|
await using DbContext context = await CreateContextAsync(interceptor);
|
|
|
|
|
var unitOfWork = new TestUnitOfWork(context);
|
|
|
|
|
await using ITransaction transaction = await unitOfWork.BeginTransactionAsync();
|
|
|
|
|
var original = new InvalidOperationException("Action failed.");
|
|
|
|
|
|
|
|
|
|
Exception actual = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
|
|
|
|
ExecuteAsync(unitOfWork, returnsResult, () => Task.FromException(original)));
|
|
|
|
|
|
|
|
|
|
Assert.Same(original, actual);
|
|
|
|
|
Assert.Same(transaction, unitOfWork.GetCurrentTransaction());
|
|
|
|
|
Assert.NotNull(context.Database.CurrentTransaction);
|
|
|
|
|
Assert.Equal(0, interceptor.CommitAttempts);
|
|
|
|
|
Assert.Equal(0, interceptor.RollbackAttempts);
|
|
|
|
|
await transaction.RollbackAsync();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task ExecuteAsync(TestUnitOfWork unitOfWork, bool returnsResult, Func<Task> action)
|
|
|
|
|
{
|
|
|
|
|
if (returnsResult)
|
|
|
|
|
{
|
|
|
|
|
int result = await unitOfWork.ExecuteInTransactionAsync(async () =>
|
|
|
|
|
{
|
|
|
|
|
await action();
|
|
|
|
|
return 42;
|
|
|
|
|
});
|
|
|
|
|
Assert.Equal(42, result);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await unitOfWork.ExecuteInTransactionAsync(action);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task<DbContext> CreateContextAsync(TransactionFailureInterceptor? interceptor = null)
|
|
|
|
|
{
|
|
|
|
|
var options = new DbContextOptionsBuilder().UseSqlite("Data Source=:memory:");
|
|
|
|
|
if (interceptor is not null)
|
|
|
|
|
{
|
|
|
|
|
options.AddInterceptors(interceptor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var context = new DbContext(options.Options);
|
|
|
|
|
await context.Database.OpenConnectionAsync();
|
|
|
|
|
await context.Database.ExecuteSqlRawAsync("CREATE TABLE records (id INTEGER NOT NULL)");
|
|
|
|
|
return context;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Task<int> CountRecordsAsync(DbContext context)
|
|
|
|
|
{
|
|
|
|
|
return context.Database.SqlQueryRaw<int>("SELECT COUNT(*) AS Value FROM records").SingleAsync();
|
|
|
|
|
}
|
|
|
|
|
}
|