@drunkcoding/dknet-implementation-skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.claude-plugin/marketplace.json +22 -0
  2. package/.claude-plugin/plugin.json +38 -0
  3. package/LICENSE +21 -0
  4. package/README.md +261 -0
  5. package/agents/dknet-architect.md +49 -0
  6. package/agents/dknet-bdd-engineer.md +62 -0
  7. package/agents/dknet-implementer.md +75 -0
  8. package/package.json +52 -0
  9. package/plugin.json +26 -0
  10. package/skills/README.md +47 -0
  11. package/skills/dknet-auth-and-ownership/SKILL.md +418 -0
  12. package/skills/dknet-bdd-tests/SKILL.md +355 -0
  13. package/skills/dknet-bdd-tests/checklist.md +39 -0
  14. package/skills/dknet-crud/SKILL.md +483 -0
  15. package/skills/dknet-ddd-principles/SKILL.md +87 -0
  16. package/skills/dknet-docs/SKILL.md +296 -0
  17. package/skills/dknet-docs/checklist.md +58 -0
  18. package/skills/dknet-docs/templates/README-template.md +68 -0
  19. package/skills/dknet-docs/templates/api-reference-template.md +275 -0
  20. package/skills/dknet-docs/templates/architecture-template.md +166 -0
  21. package/skills/dknet-docs/templates/data-model-template.md +99 -0
  22. package/skills/dknet-docs/templates/events-template.md +155 -0
  23. package/skills/dknet-dto-mapping/SKILL.md +278 -0
  24. package/skills/dknet-efcore-config/SKILL.md +379 -0
  25. package/skills/dknet-endpoint/SKILL.md +458 -0
  26. package/skills/dknet-entity/SKILL.md +483 -0
  27. package/skills/dknet-feature/SKILL.md +139 -0
  28. package/skills/dknet-feature-lifecycle/SKILL.md +144 -0
  29. package/skills/dknet-feature-remove/SKILL.md +131 -0
  30. package/skills/dknet-messaging-events/SKILL.md +395 -0
  31. package/skills/dknet-package-adoption/SKILL.md +252 -0
  32. package/skills/dknet-platform-config/SKILL.md +342 -0
  33. package/skills/dknet-project-structure/SKILL.md +148 -0
  34. package/skills/dknet-queries-specs/SKILL.md +330 -0
  35. package/skills/dknet-scaffold/SKILL.md +209 -0
  36. package/skills/dknet-unit-tests/SKILL.md +382 -0
@@ -0,0 +1,382 @@
1
+ ---
2
+ name: dknet-unit-tests
3
+ description: Write xUnit + Shouldly tests for a DKNet.Templates feature in Minimal.App.Tests — architecture/convention rules, pure functional tests (entity methods, validators, specs, mapping), and result-level integration tests against ApiFixture + IMessageBus (handler Result failures, EF persistence, domain events). Use after AppServices actions and endpoint config are ready. Covers only what xUnit owns; HTTP request/response and event-log scenarios belong in the `dknet-bdd-tests` skill. Invoke as `/dknet-unit-tests <Feature> <Entity> [mode=manual|auto]` to scaffold it for a feature.
4
+ metadata:
5
+ kind: workflow
6
+ arguments: "<Feature> <Entity> [mode=manual|auto]"
7
+ allowed-tools: Read, Grep, Glob, Edit, Write, Bash, Agent
8
+ ---
9
+
10
+ Usage: `/dknet-unit-tests <Feature> <Entity> [mode=manual|auto]`
11
+
12
+ # xUnit tests (Minimal.App.Tests)
13
+
14
+ ## Both shipped suites are teaching material — business tests only
15
+
16
+ `Minimal.App.Tests` and `Minimal.App.BDDTests` ship inside every generated solution. A team reads them
17
+ to learn how tests are written here, so every test must be about the business domain: the `PurchaseOrder`
18
+ (manual) and `Product` (automated) samples — entity invariants, validators, specs, handler results, CRUD
19
+ over HTTP, domain events.
20
+
21
+ Never add a test for platform plumbing to either suite: logging/telemetry, host/startup wiring, security
22
+ middleware (CORS, HSTS, rate limits, JWT config), health/Swagger endpoints, `FeatureOptions`↔appsettings
23
+ key binding, or template/scaffolding shape. Those belong in this repository's own hygiene tests, not in a
24
+ consumer's shipped example — if you find yourself testing framework or ASP.NET behavior instead of the
25
+ sample's business rules, stop and delete the test.
26
+
27
+ ## Test layering — where a test belongs
28
+
29
+ xUnit owns three things; BDD must not re-cover them:
30
+
31
+ 1. **Architecture/convention** — `Architecture/*`: NetArchTest + reflection over the compiled assembly
32
+ (handler/validator classes are `internal sealed`, repo interfaces inherit `IRepository<T>`, DTOs never
33
+ expose a domain entity type). Cannot be expressed as an HTTP scenario; never port to BDD.
34
+ 2. **Pure functional** — `Unit/*`: entity methods, validators, spec predicate filters, static data. No
35
+ host, no DB, no HTTP.
36
+ 3. **Result-level integration** — `Integration/<Feature>/V1/*`: handler failures asserted on the
37
+ `IResult`/`IResultBase` object (not-found, missing acting user, "already cancelled", 409 precondition
38
+ codes) and EF model/schema shape. BDD's HTTP-status/response-body assertions are coarser and would lose
39
+ this intent — keep the `Result`-level assertion here even though the same rule is also proven over HTTP
40
+ in BDD.
41
+
42
+ BDD owns user-facing HTTP behavior (request → status → response body) and domain-event side effects
43
+ observed via log capture. Do not duplicate the same behavior in both suites — an HTTP-status check for a
44
+ rule already covered here belongs in BDD instead, not copied into both.
45
+
46
+ ## Fixtures (`Minimal.App.TestSupport` + `Integration/Support`)
47
+
48
+ `TestApiFactoryBase(string? dbName) : WebApplicationFactory<Minimal.Api.Program>` (in
49
+ `Minimal.App.TestSupport`) is the shared host substitution both xUnit and BDD build on. It:
50
+
51
+ - Sets `FeatureManagement:RunDbMigrationWhenAppStart/EnableSwagger/EnableAzureAppConfig = false` and
52
+ `ConnectionStrings:AppDb = UseInMemory`.
53
+ - Swaps `CoreDbContext` for EF Core InMemory via `AddDbContextWithHook` (plain `AddDbContext` would drop
54
+ the DKNet events hook — `AddEvent`/`[RaisesEvent]` domain events would never publish).
55
+ - Replaces `IMembershipService` with `TestMembershipService`.
56
+ - Exposes `LogCapture` (a `TestLogCapture : ILoggerProvider` — captures every log line for assertions),
57
+ `CreateScope()`, and `ResetDatabaseAsync()` (drops + recreates the InMemory DB and clears `LogCapture`).
58
+
59
+ `Integration/Support/ApiFixture : TestApiFactoryBase, IAsyncLifetime` is the plain fixture — no auth, no
60
+ extra overrides:
61
+
62
+ ```csharp
63
+ public sealed class ApiFixture : TestApiFactoryBase, IAsyncLifetime
64
+ {
65
+ public async Task InitializeAsync()
66
+ {
67
+ _ = CreateClient();
68
+ await ResetDatabaseAsync();
69
+ }
70
+
71
+ Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask;
72
+ }
73
+ ```
74
+
75
+ Use `IClassFixture<ApiFixture>` and call `fixture.ResetDatabaseAsync()` at the top of every `[Fact]` —
76
+ xUnit shares one fixture instance across all tests in the class, so each test resets state itself rather
77
+ than relying on fixture disposal.
78
+
79
+ **Two ways to exercise a feature:** through `IMessageBus` from `fixture.CreateScope()` — resolve
80
+ `IMessageBus` and (for seeding/assertion) `IRepositorySpec`, call `bus.Send(request)`, assert on the
81
+ returned `IResult<TDto>`/`IResultBase` — this is the default for result-level integration tests; or
82
+ through `HttpClient` (`fixture.CreateClient()`), only for a precondition branch with no BDD scenario of
83
+ its own (see `ProductPreconditionTests` below). Prefer BDD for anything HTTP-shaped.
84
+
85
+ **Auth fixture variants** (`Integration/Support/`), used only when a test needs the authenticated/authorized
86
+ path: `AuthOnApiFixture` flips `FeatureManagement:RequireAuthorization` on and swaps the JWT bearer scheme
87
+ for `TestAuthHandler` (fixed caller identity, no live token needed); `AuthOnFixedTenantApiFixture`,
88
+ `AuthOnMultiSubjectApiFixture`, `AuthOnNoNameClaimApiFixture`, `DemoAuthenticationOffApiFixture`,
89
+ `RequireAuthorizationPlusDemoApiFixture`, `VersioningOffApiFixture` cover narrower combinations — read the
90
+ closest one before writing a new fixture. `AuthOnApiFixture` sets its flags via
91
+ `Environment.SetEnvironmentVariable` in the constructor, not `AddFeatureOverrides` — `Program.cs` binds
92
+ `FeatureOptions` before the fixture's `ConfigureAppConfiguration` override is merged in, so only an
93
+ environment variable set before `WebApplication.CreateBuilder(args)` runs takes effect. Safe only because
94
+ `AssemblyInfo.cs` disables assembly-wide test parallelization.
95
+
96
+ Use `TestLogCapture` (via `fixture.LogCapture.Messages`) together with `Eventually.IsTrueAsync(...)` when
97
+ asserting a domain-event handler's side effect — the in-memory bus publishes with
98
+ `EnableBlockingPublish = false`, so a consumer's log line lands on a background task, not before the
99
+ command's response returns. Polling avoids a flaky race.
100
+
101
+ ## Worked examples
102
+
103
+ **Result-level integration — happy path + not-found**
104
+ (`Integration/ManualSample/V1/PurchaseOrderActionsIntegrationTests.cs`):
105
+
106
+ ```csharp
107
+ public sealed class PurchaseOrderActionsIntegrationTests(ApiFixture fixture) : IClassFixture<ApiFixture>
108
+ {
109
+ [Fact]
110
+ public async Task Create_ShouldPersistOrder_AndReturnMatchingDto()
111
+ {
112
+ await fixture.ResetDatabaseAsync();
113
+ using var scope = fixture.CreateScope();
114
+ var bus = scope.ServiceProvider.GetRequiredService<IMessageBus>();
115
+ var repository = scope.ServiceProvider.GetRequiredService<IRepositorySpec>();
116
+
117
+ var result = await bus.Send(new CreatePurchaseOrderRequest
118
+ {
119
+ CustomerName = "Acme Pte Ltd", Amount = 250.00m, ByUser = "integration-test"
120
+ });
121
+
122
+ result.IsSuccess.ShouldBeTrue();
123
+ result.Value!.CustomerName.ShouldBe("Acme Pte Ltd");
124
+
125
+ var created = await repository.FirstOrDefaultAsync(
126
+ new SpecGetPurchaseOrder(byCustomerName: "Acme Pte Ltd"), CancellationToken.None);
127
+ created!.CreatedBy.ShouldBe("integration-test");
128
+ }
129
+
130
+ [Fact]
131
+ public async Task Update_ShouldFail_WhenOrderNotFound()
132
+ {
133
+ await fixture.ResetDatabaseAsync();
134
+ using var scope = fixture.CreateScope();
135
+ var bus = scope.ServiceProvider.GetRequiredService<IMessageBus>();
136
+
137
+ var result = await bus.Send(new UpdatePurchaseOrderRequest { Id = Guid.NewGuid(), Amount = 50m, ByUser = "integration-test" });
138
+
139
+ result.IsFailed.ShouldBeTrue();
140
+ }
141
+ }
142
+ ```
143
+
144
+ The same class also proves a guarded state transition by calling the handler twice — `Cancel` once
145
+ succeeds, a second `Cancel` on the same order fails with the `precondition.purchase-order-already-cancelled`
146
+ message (`PreconditionCodes`, `Minimal.AppServices/Share/PreconditionCodes.cs`), and every mutating action
147
+ fails when `ByUser` is left empty (the acting-user rule the manual mode enforces in the handler itself).
148
+
149
+ **Precondition branch reachable only over HTTP from xUnit**
150
+ (`Integration/AutomatedSample/V1/ProductPreconditionTests.cs`):
151
+
152
+ ```csharp
153
+ public sealed class ProductPreconditionTests(AuthOnApiFixture fixture) : IClassFixture<AuthOnApiFixture>
154
+ {
155
+ [Fact]
156
+ public async Task DeletingAnUnknownProduct_PassesThePreconditionAndStillAnswers404()
157
+ {
158
+ await fixture.ResetDatabaseAsync();
159
+ var client = fixture.CreateClient();
160
+
161
+ using var request = new HttpRequestMessage(HttpMethod.Delete, $"/v1/products/{Guid.NewGuid()}");
162
+ request.Headers.Add("X-Test-Scopes", "products.write");
163
+ using var response = await client.SendAsync(request);
164
+
165
+ response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
166
+ }
167
+ }
168
+ ```
169
+
170
+ The comment on that class explains why: `DeleteProductRequestValidator` must let an unknown id pass its
171
+ precondition check, or the route's own 404 would be hidden behind a 409. BDD covers the "still for sale"
172
+ (409) and "discontinued" (204) branches of the same rule end to end; this is the one branch — the target
173
+ not existing at all — with no user-facing scenario of its own. Reach for `HttpClient` in xUnit only for a
174
+ gap shaped like this one, not as a default way to test a route.
175
+
176
+ **Pure functional — validators** (`Unit/ManualSample/PurchaseOrderValidatorsTests.cs`): construct the
177
+ `AbstractValidator<TRequest>` directly and call `.Validate(request)`, no host:
178
+
179
+ ```csharp
180
+ private readonly CreatePurchaseOrderCommandValidator _createValidator = new();
181
+
182
+ [Fact]
183
+ public void CreateValidator_ShouldFail_WhenCustomerNameIsBlank()
184
+ {
185
+ var result = _createValidator.Validate(new CreatePurchaseOrderRequest { CustomerName = "", Amount = 10m });
186
+ result.IsValid.ShouldBeFalse();
187
+ }
188
+ ```
189
+
190
+ That file also guards a real regression (DRK-714): `UpdateValidator_ShouldNotRejectEmptyId` — `Id` comes
191
+ from the route, not the body, so the validator must not fail on `Guid.Empty`; an unknown/empty id 404s
192
+ from the handler's spec lookup instead. Write the equivalent guard whenever a validator could accidentally
193
+ reject a route-bound id.
194
+
195
+ **Pure functional — specs** (`Unit/ManualSample/SpecGetPurchaseOrderTests.cs`): compile the spec's
196
+ `FilterQuery` and run it against in-memory instances, no DB:
197
+
198
+ ```csharp
199
+ [Fact]
200
+ public void NoFilter_ShouldMatchEveryOrder()
201
+ {
202
+ // Regression guard: an unstarted predicate builder compiles to WHERE FALSE (empty list page).
203
+ var predicate = new SpecGetPurchaseOrder().FilterQuery!.Compile();
204
+ predicate(MakeOrder("Acme")).ShouldBeTrue();
205
+ }
206
+ ```
207
+
208
+ **Pure functional — entity methods, mode=auto** (`Unit/AutomatedSample/ProductTests.cs`):
209
+
210
+ ```csharp
211
+ [Fact]
212
+ public void ChangePrice_ShouldUpdatePrice()
213
+ {
214
+ var product = new Product("Widget", 9.99m);
215
+ product.ChangePrice(12.50m);
216
+ product.Price.ShouldBe(12.50m);
217
+ }
218
+
219
+ [Fact]
220
+ public void Discontinue_CalledTwice_ShouldStayDiscontinued_NotThrow()
221
+ {
222
+ // The entity method itself stays idempotent — the refusal on a second discontinue is a business
223
+ // rule enforced by DiscontinueProductCommandHandler, one layer up, not by this method.
224
+ var product = new Product("Widget", 9.99m);
225
+ product.Discontinue();
226
+ product.Discontinue();
227
+ product.IsDiscontinued.ShouldBeTrue();
228
+ }
229
+ ```
230
+
231
+ The same file reflects over the `[CrudCreate]` constructor's parameters to prove the `DataAnnotations`
232
+ (`[Required]`, `[StringLength]`, `[Range]`) that the generator forwards onto the generated request are
233
+ actually present on the source the generator reads — grounding the claim without asserting on generated
234
+ code that has no committed source file.
235
+
236
+ **Static data seeder** (`Unit/ManualSample/PurchaseOrderStaticDataTests.cs`): a `DataSeedingConfiguration<T>`
237
+ seeder's `GetDataAsync` is `protected`, invoked only by DKNet's `UseAutoDataSeeding` pipeline — neither test
238
+ fixture wires that pipeline into its InMemory `DbContextOptions`, so it's otherwise never exercised. Invoke
239
+ it via reflection to prove the actual seed data instead of leaving the seeder at 0% coverage:
240
+
241
+ ```csharp
242
+ var method = typeof(PurchaseOrderStaticData).GetMethod("GetDataAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
243
+ var orders = await (ValueTask<ICollection<PurchaseOrder>>)method.Invoke(new PurchaseOrderStaticData(), [CancellationToken.None])!;
244
+ orders.ShouldAllBe(o => o.CreatedBy == SharedConsts.SystemAccount);
245
+ ```
246
+
247
+ **Architecture rule** (`Architecture/AppServiceTests.cs`), using NetArchTest against the compiled
248
+ `Minimal.AppServices` assembly:
249
+
250
+ ```csharp
251
+ var result = Types.InAssembly(typeof(AppSetup).Assembly)
252
+ .That().AreClasses().And().AreNotAbstract().And().ImplementInterface(typeof(IRequestHandler<,>))
253
+ .Should().NotBePublic().And().BeSealed()
254
+ .GetResult();
255
+ result.IsSuccessful.ShouldBeTrue();
256
+ ```
257
+
258
+ ## Mode differences
259
+
260
+ - **manual** (`ManualSample/PurchaseOrder`): test the hand-written `CreatePurchaseOrderCommandValidator`
261
+ etc. directly; cover duplicate checks and rejected state transitions (`Cancel` twice) through the
262
+ handler's `Result`; the acting-user rule (`ByUser` empty → fail) is enforced by the handler itself, so
263
+ assert it there.
264
+ - **auto** (`AutomatedSample/Product`): no hand-written validator class exists for `[CrudCreate]`/
265
+ `[CrudUpdate]` members — test the entity method directly (`ChangePrice`, `Discontinue`, `Approve`)
266
+ instead. `DataAnnotations` on a `[CrudCreate]` parameter are forwarded onto the generated request but
267
+ enforced only on a literal `Map*(string, Delegate)` route — **never assert 400 from a DataAnnotations
268
+ violation on a generated CRUD route**; it returns 201/200 (`DKNet.AspCore.Extensions`'s generic
269
+ `Map*<TRequest,TDto>` wrapper isn't visible to the validation source generator). A FluentValidation
270
+ validator still runs on every route. Verify a declared event's composed name against the compiled
271
+ assembly before asserting on it — `strings bin/**/Minimal.Domains.dll | grep <Entity>`.
272
+
273
+ ## Conventions
274
+
275
+ - xUnit + Shouldly (`result.IsSuccess.ShouldBeTrue()`, not `Assert.True`). `Minimal.App.Tests.csproj`
276
+ disables analyzers and warnings-as-errors — production code style rules do not apply here.
277
+ - Implicit usings from `GlobalUsings.cs`: `AutoBogus`, `Shouldly`, `System.Text.Json`, `MapsterMapper`,
278
+ plus csproj-level `System.Net`, `Microsoft.Extensions.DependencyInjection`, `Xunit`. Still add explicit
279
+ `using`s for `SlimMessageBus`, `DKNet.EfCore.Specifications*`, and your feature's `AppServices`/`Domains`
280
+ namespaces.
281
+ - Reset the database at the top of every test (`await fixture.ResetDatabaseAsync()`), never in a shared
282
+ constructor — the fixture instance is shared across the whole test class.
283
+ - Don't hand-roll a mock of `IRepositorySpec` or `IMapper`; resolve the real ones from
284
+ `fixture.CreateScope()` against the InMemory provider — that is what proves DI wiring, not a substitute.
285
+ - Test classes/methods: `{Entity}{Concern}Tests`, `[Fact]` methods named `Method_ShouldOutcome_WhenCondition`.
286
+
287
+ ## Commands
288
+
289
+ ```bash
290
+ dotnet test ApiEndpoints/Minimal.App.Tests/Minimal.App.Tests.csproj --filter "FullyQualifiedName~PurchaseOrder"
291
+ dotnet test --settings coverage.runsettings --collect:"XPlat Code Coverage"
292
+ ```
293
+
294
+ `coverage.runsettings` includes `[DKNet*]*` and `[Minimal*]*`, excludes `*.Tests`/`*Tests` assemblies and
295
+ `**/bin/**, **/obj/**, **/*Tests.cs, **/GlobalUsings.cs, **/*.g.cs` by file — don't put real logic in an
296
+ excluded path expecting it to be measured.
297
+
298
+ ## Step-by-step
299
+
300
+ 1. Decide the layer: architecture rule, pure functional (entity/validator/spec), or result-level
301
+ integration. Don't write an HTTP test here unless it fills a genuine result-only/precondition gap BDD
302
+ can't reach.
303
+ 2. Pure functional: construct the validator/entity/spec directly, no fixture.
304
+ 3. Result-level integration: pick `ApiFixture` (or an auth variant only if the scenario needs
305
+ authentication/authorization), reset the DB, resolve `IMessageBus`/`IRepositorySpec` from
306
+ `CreateScope()`, send the request, assert on `IResult`/`IResultBase`.
307
+ 4. Cover: happy path, not-found, missing/invalid acting user (manual mode), a guarded state transition
308
+ failing on retry, and (auto mode) the entity method's own behavior plus its forwarded
309
+ `DataAnnotations`.
310
+ 5. Run the filtered test, then the full suite with coverage before moving on.
311
+
312
+ ## Common mistakes
313
+
314
+ - **What you might expect**: asserting `400` when a `[Range]` attribute on a `[CrudCreate]` parameter is
315
+ violated on a generated route. **What actually happens**: it succeeds (201). **Why**: DataAnnotations are
316
+ forwarded onto the generated request but only enforced when the route is a literal `Map*` call the .NET
317
+ validation source generator can see in the compiling project; generated CRUD routes go through
318
+ `DKNet.AspCore.Extensions`'s generic wrapper instead.
319
+ - **What you might expect**: a domain event handler's log line is present immediately after
320
+ `bus.Send(...)` returns. **What actually happens**: it's sometimes missing. **Why**: the in-memory bus
321
+ publishes with `EnableBlockingPublish = false`; poll with `Eventually.IsTrueAsync(...)` against
322
+ `fixture.LogCapture.Messages` instead.
323
+ - **What you might expect**: an HTTP-status assertion in xUnit for a rule already covered by a BDD
324
+ scenario is harmless extra coverage. **What actually happens**: duplicated, drifting coverage. **Why**:
325
+ xUnit owns architecture/pure-functional/result-level assertions; BDD owns request→status→body.
326
+ - **What you might expect**: a new fixture mutating `Environment.SetEnvironmentVariable` for its own flag
327
+ works standalone. **What actually happens**: it races other tests intermittently. **Why**: only
328
+ `AssemblyInfo.cs`'s `DisableTestParallelization = true` makes that trick safe assembly-wide.
329
+
330
+ ---
331
+
332
+ # Workflow: `/dknet-unit-tests`
333
+
334
+ The procedure an agent follows when invoked with arguments. The reference sections above are the rules it applies.
335
+
336
+ You are adding integration tests in `Minimal.App.Tests` that exercise the AppServices and Domains layers through the real DI container.
337
+
338
+ ### Inputs
339
+
340
+ `$ARGUMENTS` — feature folder, entity, optional `mode=manual|auto`. If not supplied, detect it: a
341
+ `[CrudCreate]` on the entity means `auto`. The mode decides which of the cases below apply.
342
+
343
+ ### Required reading
344
+
345
+ 1. The reference sections above
346
+ 2. `ApiEndpoints/Minimal.App.Tests/` — existing fixtures and test patterns (`Architecture/`, `Integration/`, `Unit/`).
347
+
348
+ ### Steps
349
+
350
+ 1. Use the `dknet-implementer` subagent (or follow the skill directly) to write tests covering.
351
+
352
+ Both modes:
353
+ - Happy-path Create / Update / Delete via `IMessageBus.Send(...)`.
354
+ - Not-found in Update + Delete.
355
+ - Domain event firing — assert the in-memory consumer ran. In `auto`, send to the **composed**
356
+ event name (`<Entity><NarrowingProps><Operation>Event`, e.g. `ProductPriceUpdatedEvent`), which
357
+ you verify against the compiled assembly, not by guessing.
358
+ - Mapster smoke test (entity → DTO field-for-field).
359
+
360
+ `mode=manual` only:
361
+ - FluentValidation failures (empty / too-long / invalid format) on Create + Update.
362
+ - Duplicate detection in Create handler.
363
+ - Rejected state transitions (`Result.Fail`) on any business action.
364
+
365
+ `mode=auto` only:
366
+ - Entity mutation methods tested directly — that is where the behavior lives.
367
+ - **Do NOT** write a test asserting a `400`/validation failure from a forwarded DataAnnotations
368
+ attribute on a generated request. It is never enforced under this template's endpoint
369
+ convention, so such a test either fails or, worse, gets "fixed" by relaxing it into asserting
370
+ the gap is correct. Note the gap in the report instead.
371
+ 2. Run only the affected tests:
372
+ ```
373
+ dotnet test ApiEndpoints/Minimal.App.Tests/Minimal.App.Tests.csproj --filter "FullyQualifiedName~<Entity>"
374
+ ```
375
+ 3. If any test fails, fix the test or product code (per skill guidance) — do not relax assertions.
376
+ 4. Report: test file path, count, pass/fail, coverage areas hit.
377
+
378
+ ### Constraints
379
+
380
+ - Tests use the real `ApiFixture` + DI container — no hand-rolled mocks for `IRepositorySpec` or `IMapper`.
381
+ - xUnit + Shouldly. Assertions: `result.IsSuccess.ShouldBeTrue()`, `result.Value.X.ShouldBe(...)`, `result.Errors.ShouldContain(e => ...)`.
382
+ - Reset DB state between tests (per the skill's fixture pattern). Don't leak state between cases.