adding complete db migrations from posgres
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.9" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.20" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
|
||||
@@ -30,7 +30,7 @@ public class NotificationConfigServiceTests
|
||||
|
||||
var result = await service.GetAsync();
|
||||
|
||||
Assert.Equal(7u, result.RowVersion);
|
||||
Assert.Equal(1u, result.RowVersion);
|
||||
Assert.Contains(result.Value.Modules, m => m.ModuleKey == "sale.create" && m.EmailEnabled && !m.SmsEnabled && m.InAppEnabled);
|
||||
Assert.Contains(result.Value.Modules, m => m.ModuleKey == "grn.confirmed" && !m.EmailEnabled && !m.SmsEnabled && !m.InAppEnabled);
|
||||
}
|
||||
@@ -55,9 +55,9 @@ public class NotificationConfigServiceTests
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var updated = await service.UpdateAsync(request, 7);
|
||||
var updated = await service.UpdateAsync(request, 1);
|
||||
|
||||
Assert.Equal(7u, updated.RowVersion);
|
||||
Assert.Equal(2u, updated.RowVersion);
|
||||
Assert.Contains(updated.Value.Modules, m => m.ModuleKey == catalog.GetAll()[0].Key && m.EmailEnabled && !m.SmsEnabled && !m.InAppEnabled);
|
||||
Assert.Contains(updated.Value.Modules, m => m.ModuleKey == catalog.GetAll()[1].Key && !m.EmailEnabled && m.SmsEnabled && m.InAppEnabled);
|
||||
|
||||
@@ -133,7 +133,7 @@ public class NotificationConfigServiceTests
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var ex = await Assert.ThrowsAsync<DomainException>(() => service.UpdateAsync(request, 7));
|
||||
var ex = await Assert.ThrowsAsync<DomainException>(() => service.UpdateAsync(request, 1));
|
||||
Assert.Equal(ErrorCodes.ConcurrencyConflict, ex.Code);
|
||||
Assert.Equal(412, ex.StatusCode);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Production;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Infra.Persistence;
|
||||
using ERPCore.Repositories;
|
||||
using ERPCore.Services.Production;
|
||||
using ERPCore.System.Errors;
|
||||
using ERPCore.Tests.TestSupport;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace ERPCore.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Covers the 412 translation on the two production services, which save through
|
||||
/// <c>ExecuteInTransactionAsync</c> rather than a bare <c>SaveChangesAsync</c> and so guard
|
||||
/// concurrency differently from the inline pattern the other services use.
|
||||
/// </summary>
|
||||
public class ProductionConcurrencyTests
|
||||
{
|
||||
private static readonly InMemoryDatabaseRoot Root = new();
|
||||
|
||||
[Fact]
|
||||
public async Task TemplateUpdateAsync_MapsConcurrencyFailuresToDomainException()
|
||||
{
|
||||
await using var db = CreateContext();
|
||||
|
||||
// Validation runs before the transaction opens, so the referenced item has to exist.
|
||||
db.Items.Add(new Item
|
||||
{
|
||||
ItemId = 1,
|
||||
Sku = "FG-1",
|
||||
Name = "Finished good",
|
||||
CategoryId = 1,
|
||||
BaseUomId = 1,
|
||||
Status = EntityStatus.Active
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var service = CreateTemplateService(db);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<DomainException>(
|
||||
() => service.UpdateAsync(1, SingleStageTemplate(), expectedRowVersion: 1));
|
||||
|
||||
Assert.Equal(ErrorCodes.ConcurrencyConflict, ex.Code);
|
||||
Assert.Equal(412, ex.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunUpdateStageQuantitiesAsync_MapsConcurrencyFailuresToDomainException()
|
||||
{
|
||||
await using var db = CreateContext();
|
||||
var service = CreateRunService(db);
|
||||
|
||||
var ex = await Assert.ThrowsAsync<DomainException>(
|
||||
() => service.UpdateStageQuantitiesAsync(1, 1, new UpdateStageQuantitiesRequest()));
|
||||
|
||||
Assert.Equal(ErrorCodes.ConcurrencyConflict, ex.Code);
|
||||
Assert.Equal(412, ex.StatusCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The smallest graph the validator accepts: one stage (both entry and terminal) with a
|
||||
/// stock input and a single item-bearing output, and no edges.
|
||||
/// </summary>
|
||||
private static SaveTemplateRequest SingleStageTemplate() => new()
|
||||
{
|
||||
Code = "TPL-1",
|
||||
Name = "Template 1",
|
||||
Stages =
|
||||
[
|
||||
new SaveStageRequest
|
||||
{
|
||||
Key = "tmp-1",
|
||||
Name = "Mix",
|
||||
Inputs =
|
||||
[
|
||||
new SaveInputRequest
|
||||
{
|
||||
Source = StageInputSource.Stock,
|
||||
ItemId = 1,
|
||||
QtyUnit = StageQtyUnit.Pack,
|
||||
QtyPerBatch = 1m
|
||||
}
|
||||
],
|
||||
Outputs =
|
||||
[
|
||||
new SaveOutputRequest { Key = "tmp-o1", Name = "Output", ItemId = 1, QtyPerBatch = 1m }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
private static ErpDbContext CreateContext()
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ErpDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString("N"), Root)
|
||||
.Options;
|
||||
|
||||
// The real CurrentBranchContext over an unauthenticated user yields IsActive=false,
|
||||
// which leaves the branch query filters off: what a unit test wants.
|
||||
var currentUser = new TestCurrentUser();
|
||||
return new ErpDbContext(options, currentUser, new CurrentBranchContext(currentUser));
|
||||
}
|
||||
|
||||
private static ProductionTemplateService CreateTemplateService(ErpDbContext db)
|
||||
=> new(
|
||||
new Repository<ProductionTemplate>(db), new Repository<TemplateStage>(db),
|
||||
new Repository<StageInput>(db), new Repository<StageOutput>(db),
|
||||
new Repository<StageEdge>(db), new Repository<Item>(db),
|
||||
new Repository<Uom>(db), new Repository<ProductionRun>(db),
|
||||
new ThrowingTransactionUnitOfWork(), new TestCurrentUser());
|
||||
|
||||
// The costing, measurement and numbering collaborators are only ever touched inside the
|
||||
// transaction lambda, which ThrowingTransactionUnitOfWork never invokes.
|
||||
private static ProductionRunService CreateRunService(ErpDbContext db)
|
||||
=> new(
|
||||
new Repository<ProductionRun>(db), new Repository<ProductionTemplate>(db),
|
||||
new Repository<Warehouse>(db), new Repository<Bin>(db),
|
||||
new Repository<Item>(db), new Repository<ReasonCode>(db),
|
||||
fifo: null!, measure: null!, numbers: null!,
|
||||
new ThrowingTransactionUnitOfWork(), new TestCurrentUser());
|
||||
}
|
||||
@@ -11,6 +11,12 @@ internal sealed class TestCurrentUser : ICurrentUser
|
||||
public string UserId => "system";
|
||||
public int AuditUserId => User.SystemUserId;
|
||||
public bool IsAuthenticated => false;
|
||||
|
||||
// Unauthenticated, so no role claim and no branch scoping: the three members the
|
||||
// AuthHex integration added to ICurrentUser.
|
||||
public string? RoleCode => null;
|
||||
public bool IsAdmin => false;
|
||||
public int? BranchId => null;
|
||||
}
|
||||
|
||||
internal sealed class ThrowingUnitOfWork : ERPCore.Infra.UoW.IUnitOfWork
|
||||
@@ -25,6 +31,25 @@ internal sealed class ThrowingUnitOfWork : ERPCore.Infra.UoW.IUnitOfWork
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fails the transactional path the way a lost optimistic-concurrency race does, so the
|
||||
/// services that save through <c>ExecuteInTransactionAsync</c> can be covered too. The
|
||||
/// lambda is never invoked, which is the point: the real <c>UnitOfWork</c> issues its own
|
||||
/// <c>SaveChangesAsync</c> after the lambda returns, so the catch under test has to sit
|
||||
/// outside it.
|
||||
/// </summary>
|
||||
internal sealed class ThrowingTransactionUnitOfWork : ERPCore.Infra.UoW.IUnitOfWork
|
||||
{
|
||||
public Task<int> SaveChangesAsync(CancellationToken ct = default)
|
||||
=> throw new DbUpdateConcurrencyException("Test concurrency conflict.");
|
||||
|
||||
public Task ExecuteInTransactionAsync(Func<CancellationToken, Task> action, CancellationToken ct = default)
|
||||
=> throw new DbUpdateConcurrencyException("Test concurrency conflict.");
|
||||
|
||||
public Task<T> ExecuteInTransactionAsync<T>(Func<CancellationToken, Task<T>> action, CancellationToken ct = default)
|
||||
=> throw new DbUpdateConcurrencyException("Test concurrency conflict.");
|
||||
}
|
||||
|
||||
internal static class NotificationConfigTestSupport
|
||||
{
|
||||
private static readonly InMemoryDatabaseRoot Root = new();
|
||||
@@ -35,7 +60,10 @@ internal static class NotificationConfigTestSupport
|
||||
.UseInMemoryDatabase(name, Root)
|
||||
.Options;
|
||||
|
||||
return new ErpDbContext(options, new TestCurrentUser());
|
||||
// The real CurrentBranchContext over an unauthenticated user yields IsActive=false,
|
||||
// which leaves the branch query filters off: what a unit test wants.
|
||||
var currentUser = new TestCurrentUser();
|
||||
return new ErpDbContext(options, currentUser, new CurrentBranchContext(currentUser));
|
||||
}
|
||||
|
||||
public static async Task SeedAsync(ErpDbContext db, IEnumerable<NotificationConfigModule>? modules = null)
|
||||
@@ -52,7 +80,8 @@ internal static class NotificationConfigTestSupport
|
||||
NotificationConfigId = NotificationConfig.SingletonId,
|
||||
UpdatedBy = User.SystemUserId,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
RowVersion = 7,
|
||||
// No RowVersion here on purpose: ErpDbContext.BumpRowVersions stamps every
|
||||
// inserted row with 1, so any seeded value is overwritten on save.
|
||||
};
|
||||
|
||||
if (modules is not null)
|
||||
|
||||
Reference in New Issue
Block a user