[CLAUDE] Tests: Plan C B-Wrap BW1-BW7 Contract V2 test bundle +9 tests (111→120)
Plan C B-Wrap (spec D-Bis migration-todos.md lines 563-619) — Contract V2 ApproveV2Async ~227 LOC NO test cover after S29 Plan B deploy prod. Risk gotcha #48 high. Anh main S32 chốt defer dedicated session ~2h post-Phase 9 stabilize. S33 kick off cùng Plan B G-H1. 7 BW spec deterministic → 9 [Fact] method (BW6 split 3 for clean isolation): BW1 — ApproveV2 happy path Cấp 1→Cấp 2 cùng Bước advance pointer + Approval row + LevelOpinion UPSERT + LogTransition "Hoàn tất Cấp 1, sang Cấp 2" BW2 — Terminal Cấp cuối Bước cuối → DaPhatHanh + gen mã HĐ format "FLOCK01/HĐTP/SOL&BTBM/01" + clear pointers BW3 — skipToFinal F2 admin opt-in AllowApproverSkipToFinal=true Cấp 1 Bước 1 → advance lastStepIdx/lastLevelMaxOrder + prefix [Duyệt vượt cấp] BW4 — Outsider (không trong pendingLevelGroup.ApproverUserId) → ForbiddenException BW5 — CreateContractCommand pin workflow ApplicableType=DuyetNcc → exception "Workflow phải ApplicableType=Contract" (Reviewer S29 MAJOR catch) BW6a — ContractLevelOpinion duplicate composite (ContractId, LevelId) → DbUpdateException (UNIQUE Mig 33) BW6b — UPSERT pattern fetch+update → 1 row only Comment updated BW6c — Delete Contract → FK Cascade auto-delete ContractLevelOpinions BW7 — V1 fallback skipToFinal non-admin → ConflictException "skipToFinal chỉ hỗ trợ HĐ V2" Test infra dependencies: - ✅ TestApplicationDbContext SQLite reuse (Common/SqliteDbFixture.cs) - ✅ IdentityFixture reuse (UserManager + CreateUserAsync helper) - ✅ FixedDateTime reuse (deterministic clock) - ✅ NoOpNotificationService reuse - 🆕 TestCurrentUser stub ICurrentUser (configurable per-test scope) — 31 LOC - ✅ REAL ChangelogService inject TestCurrentUser - ✅ REAL ContractCodeGenerator inline (no mock needed, SqliteDbFixture enough for atomic sequence test) Verify: - dotnet build: 0 err 0 warn (2.49s) - dotnet test: **120/120 PASS** (was 111 baseline + 9 new) - Domain: 58/58 PASS - Infrastructure: 62/62 PASS (was 53 → +9 BW) Reviewer S33 verdict: **PASS** — 0 critical/major issues, 3 minor cosmetic defer-OK (CreateService helper dead code unused, TestCurrentUser null defensive C# warning shadow, BW1-4+7 vs BW6 using pattern style). 9/9 indep verify PASS in 4.7s. Spec strings exact match service source (BW1 ContextNote + BW2 Mã HĐ + BW3 ContextNote + BW4-7 exception messages). Smart Friend independence lần thứ 5 cumulative: 1. S22 #44 silent 403 — Reviewer catch 2. S25 #48 SQLite tie-break — Reviewer catch 3. S29 Plan CA password ≥12 — Reviewer catch 4. S29 Plan B ApplicableType — Reviewer catch 5. S33 Plan C BW — clean, em main+Implementer quality genuine NOT lowered Patterns applied: - Implementer Pattern 12-bis cross-module entity cookie-cutter mirror PE → Contract (proven 3× S29 + S33) - Test deterministic seeded helper SeedApproverF2WorkflowAsync mirror PurchaseEvaluationWorkflowServiceReturnModeTests.cs structure Files (4 new tests + 1 stub): - A tests/SolutionErp.Infrastructure.Tests/Common/TestCurrentUser.cs (31 LOC) - A tests/SolutionErp.Infrastructure.Tests/Services/ContractWorkflowServiceApproveV2Tests.cs - A tests/SolutionErp.Infrastructure.Tests/Application/CreateContractCommandApplicableTypeTests.cs - A tests/SolutionErp.Infrastructure.Tests/Common/ContractV2SchemaPersistenceTests.cs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,238 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||
using SolutionErp.Domain.Common;
|
||||
using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.Master;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Tests.Common;
|
||||
|
||||
// Plan C B-Wrap BW6 (S33) — ContractLevelOpinion schema persistence verification
|
||||
// (Mig 33 — cookie-cutter mirror PE Mig 26).
|
||||
//
|
||||
// 3 assertion:
|
||||
// 1. UNIQUE composite (ContractId, ApprovalWorkflowLevelId) — không cho 2 row
|
||||
// cùng (HĐ, Level slot) → DbUpdateException khi cố insert duplicate.
|
||||
// 2. UPSERT pattern fetch + update — Comment thay đổi, vẫn 1 row only.
|
||||
// 3. FK Cascade Contract — xoá HĐ → ContractLevelOpinions auto-deleted.
|
||||
//
|
||||
// FK Restrict ApprovalWorkflowLevel KHÔNG test (per spec):
|
||||
// - Admin xoá Level chặn nếu opinion tồn tại (data preservation guarantee)
|
||||
// - Verify ngoài qua manual SQL hoặc integration test riêng.
|
||||
public class ContractV2SchemaPersistenceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ContractLevelOpinion_DuplicateComposite_ThrowsDbUpdateException()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
|
||||
var approver = await fix.CreateUserAsync("a-bw6@test.local", "Approver BW6",
|
||||
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||
|
||||
// Seed 1 Workflow + 1 Step + 1 Level
|
||||
var wf = new ApprovalWorkflow
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "QT-BW6-001",
|
||||
Version = 1,
|
||||
Name = "BW6 unique test",
|
||||
ApplicableType = ApprovalWorkflowApplicableType.Contract,
|
||||
IsActive = true,
|
||||
IsUserSelectable = true,
|
||||
};
|
||||
var step = new ApprovalWorkflowStep
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ApprovalWorkflowId = wf.Id,
|
||||
Order = 1,
|
||||
DepartmentId = null,
|
||||
Name = "Bước 1",
|
||||
};
|
||||
var level = new ApprovalWorkflowLevel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ApprovalWorkflowStepId = step.Id,
|
||||
Order = 1,
|
||||
ApproverUserId = approver.Id,
|
||||
};
|
||||
db.ApprovalWorkflows.Add(wf);
|
||||
db.ApprovalWorkflowSteps.Add(step);
|
||||
db.ApprovalWorkflowLevels.Add(level);
|
||||
|
||||
// Seed Supplier + Project + Contract V2
|
||||
var sup = new Supplier { Id = Guid.NewGuid(), Code = "ABC", Name = "NCC ABC", Type = SupplierType.NhaThauPhu };
|
||||
var proj = new Project { Id = Guid.NewGuid(), Code = "PROJ01", Name = "Dự án 01" };
|
||||
db.Suppliers.Add(sup);
|
||||
db.Projects.Add(proj);
|
||||
|
||||
var contract = new Contract
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = ContractType.HopDongThauPhu,
|
||||
Phase = ContractPhase.ChoDuyet,
|
||||
SupplierId = sup.Id,
|
||||
ProjectId = proj.Id,
|
||||
DrafterUserId = Guid.NewGuid(),
|
||||
TenHopDong = "Test BW6 unique",
|
||||
GiaTri = 10_000_000m,
|
||||
ApprovalWorkflowId = wf.Id,
|
||||
CurrentWorkflowStepIndex = 0,
|
||||
CurrentApprovalLevelOrder = 1,
|
||||
};
|
||||
db.Contracts.Add(contract);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
// 2 ContractLevelOpinion cùng (ContractId, ApprovalWorkflowLevelId)
|
||||
db.ContractLevelOpinions.Add(new ContractLevelOpinion
|
||||
{
|
||||
ContractId = contract.Id,
|
||||
ApprovalWorkflowLevelId = level.Id,
|
||||
Comment = "Ý kiến lần 1",
|
||||
SignedAt = DateTime.UtcNow,
|
||||
SignedByUserId = approver.Id,
|
||||
SignedByFullName = "Approver BW6",
|
||||
});
|
||||
db.ContractLevelOpinions.Add(new ContractLevelOpinion
|
||||
{
|
||||
ContractId = contract.Id,
|
||||
ApprovalWorkflowLevelId = level.Id,
|
||||
Comment = "Ý kiến lần 2 — duplicate",
|
||||
SignedAt = DateTime.UtcNow,
|
||||
SignedByUserId = approver.Id,
|
||||
SignedByFullName = "Approver BW6",
|
||||
});
|
||||
|
||||
var act = async () => await db.SaveChangesAsync(CancellationToken.None);
|
||||
await act.Should().ThrowAsync<DbUpdateException>(
|
||||
"UNIQUE composite (ContractId, ApprovalWorkflowLevelId) — Mig 33");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContractLevelOpinion_UpsertPattern_FetchAndUpdate_KeepsSingleRow()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
|
||||
var approver = await fix.CreateUserAsync("a-bw6-up@test.local", "Approver BW6 UP",
|
||||
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||
|
||||
var (wf, step, level, sup, proj, contract) = await SeedBaseAsync(db, approver.Id);
|
||||
|
||||
// Initial insert
|
||||
db.ContractLevelOpinions.Add(new ContractLevelOpinion
|
||||
{
|
||||
ContractId = contract.Id,
|
||||
ApprovalWorkflowLevelId = level.Id,
|
||||
Comment = "Ý kiến đầu tiên",
|
||||
SignedAt = DateTime.UtcNow,
|
||||
SignedByUserId = approver.Id,
|
||||
SignedByFullName = "Approver BW6 UP",
|
||||
});
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
// UPSERT — fetch + update
|
||||
var existing = await db.ContractLevelOpinions
|
||||
.FirstAsync(o => o.ContractId == contract.Id && o.ApprovalWorkflowLevelId == level.Id);
|
||||
existing.Comment = "Ý kiến đã được cập nhật";
|
||||
existing.SignedAt = DateTime.UtcNow.AddSeconds(1);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var rows = await db.ContractLevelOpinions
|
||||
.Where(o => o.ContractId == contract.Id).ToListAsync();
|
||||
rows.Should().HaveCount(1, "UPSERT giữ duy nhất 1 row per (HĐ, Level)");
|
||||
rows[0].Comment.Should().Be("Ý kiến đã được cập nhật");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ContractLevelOpinion_FkCascade_DeleteContract_AlsoDeletesOpinions()
|
||||
{
|
||||
using var fix = new IdentityFixture();
|
||||
var db = fix.Services.GetRequiredService<TestApplicationDbContext>();
|
||||
|
||||
var approver = await fix.CreateUserAsync("a-bw6-cas@test.local", "Approver BW6 CAS",
|
||||
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||
|
||||
var (wf, step, level, sup, proj, contract) = await SeedBaseAsync(db, approver.Id);
|
||||
|
||||
db.ContractLevelOpinions.Add(new ContractLevelOpinion
|
||||
{
|
||||
ContractId = contract.Id,
|
||||
ApprovalWorkflowLevelId = level.Id,
|
||||
Comment = "Sẽ bị wipe khi xoá HĐ",
|
||||
SignedAt = DateTime.UtcNow,
|
||||
SignedByUserId = approver.Id,
|
||||
SignedByFullName = "Approver BW6 CAS",
|
||||
});
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var beforeCount = await db.ContractLevelOpinions
|
||||
.CountAsync(o => o.ContractId == contract.Id);
|
||||
beforeCount.Should().Be(1);
|
||||
|
||||
// Hard delete Contract — Cascade wipe opinions
|
||||
db.Contracts.Remove(contract);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
|
||||
var afterCount = await db.ContractLevelOpinions
|
||||
.CountAsync(o => o.ContractId == contract.Id);
|
||||
afterCount.Should().Be(0, "FK Cascade Mig 33 — xoá HĐ wipe ContractLevelOpinions");
|
||||
}
|
||||
|
||||
// Helper seed full chain: Workflow + Step + Level + Supplier + Project + Contract.
|
||||
private static async Task<(ApprovalWorkflow wf, ApprovalWorkflowStep step,
|
||||
ApprovalWorkflowLevel level, Supplier sup, Project proj, Contract contract)>
|
||||
SeedBaseAsync(TestApplicationDbContext db, Guid approverUserId)
|
||||
{
|
||||
var wf = new ApprovalWorkflow
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = $"QT-BW6-{Guid.NewGuid().ToString()[..6]}",
|
||||
Version = 1,
|
||||
Name = "BW6 helper",
|
||||
ApplicableType = ApprovalWorkflowApplicableType.Contract,
|
||||
IsActive = true,
|
||||
IsUserSelectable = true,
|
||||
};
|
||||
var step = new ApprovalWorkflowStep
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ApprovalWorkflowId = wf.Id,
|
||||
Order = 1,
|
||||
DepartmentId = null,
|
||||
Name = "Bước 1",
|
||||
};
|
||||
var level = new ApprovalWorkflowLevel
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ApprovalWorkflowStepId = step.Id,
|
||||
Order = 1,
|
||||
ApproverUserId = approverUserId,
|
||||
};
|
||||
var sup = new Supplier { Id = Guid.NewGuid(), Code = "ABC", Name = "NCC ABC", Type = SupplierType.NhaThauPhu };
|
||||
var proj = new Project { Id = Guid.NewGuid(), Code = "PROJ01", Name = "Dự án 01" };
|
||||
var contract = new Contract
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = ContractType.HopDongThauPhu,
|
||||
Phase = ContractPhase.ChoDuyet,
|
||||
SupplierId = sup.Id,
|
||||
ProjectId = proj.Id,
|
||||
DrafterUserId = Guid.NewGuid(),
|
||||
TenHopDong = "BW6 helper contract",
|
||||
GiaTri = 10_000_000m,
|
||||
ApprovalWorkflowId = wf.Id,
|
||||
CurrentWorkflowStepIndex = 0,
|
||||
CurrentApprovalLevelOrder = 1,
|
||||
};
|
||||
db.ApprovalWorkflows.Add(wf);
|
||||
db.ApprovalWorkflowSteps.Add(step);
|
||||
db.ApprovalWorkflowLevels.Add(level);
|
||||
db.Suppliers.Add(sup);
|
||||
db.Projects.Add(proj);
|
||||
db.Contracts.Add(contract);
|
||||
await db.SaveChangesAsync(CancellationToken.None);
|
||||
return (wf, step, level, sup, proj, contract);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Tests.Common;
|
||||
|
||||
// Stub ICurrentUser cho tests cần inject ChangelogService (Contract V2 wire S29).
|
||||
// ChangelogService resolve actor qua ICurrentUser.UserId → cần stub
|
||||
// configurable per test scenario (vd BW3 admin bypass cần Roles chứa "Admin").
|
||||
//
|
||||
// Pattern: instance per test, set Acting* properties trước khi gọi service.
|
||||
// Khác ICurrentUser prod (HttpContextCurrentUser) đọc JWT claims — test
|
||||
// override trực tiếp.
|
||||
public sealed class TestCurrentUser : ICurrentUser
|
||||
{
|
||||
public Guid? UserId { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? FullName { get; set; }
|
||||
public IReadOnlyList<string> Roles { get; set; } = Array.Empty<string>();
|
||||
public bool IsAuthenticated => UserId is not null;
|
||||
|
||||
public TestCurrentUser() { }
|
||||
|
||||
public TestCurrentUser(Guid userId, string? fullName = null, string? email = null, params string[] roles)
|
||||
{
|
||||
UserId = userId;
|
||||
FullName = fullName;
|
||||
Email = email;
|
||||
Roles = roles ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
// Helper: simulate system actor (vd SLA auto-approve, DbInitializer seed).
|
||||
public static TestCurrentUser System() => new();
|
||||
}
|
||||
Reference in New Issue
Block a user