@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.
- package/.claude-plugin/marketplace.json +22 -0
- package/.claude-plugin/plugin.json +38 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/agents/dknet-architect.md +49 -0
- package/agents/dknet-bdd-engineer.md +62 -0
- package/agents/dknet-implementer.md +75 -0
- package/package.json +52 -0
- package/plugin.json +26 -0
- package/skills/README.md +47 -0
- package/skills/dknet-auth-and-ownership/SKILL.md +418 -0
- package/skills/dknet-bdd-tests/SKILL.md +355 -0
- package/skills/dknet-bdd-tests/checklist.md +39 -0
- package/skills/dknet-crud/SKILL.md +483 -0
- package/skills/dknet-ddd-principles/SKILL.md +87 -0
- package/skills/dknet-docs/SKILL.md +296 -0
- package/skills/dknet-docs/checklist.md +58 -0
- package/skills/dknet-docs/templates/README-template.md +68 -0
- package/skills/dknet-docs/templates/api-reference-template.md +275 -0
- package/skills/dknet-docs/templates/architecture-template.md +166 -0
- package/skills/dknet-docs/templates/data-model-template.md +99 -0
- package/skills/dknet-docs/templates/events-template.md +155 -0
- package/skills/dknet-dto-mapping/SKILL.md +278 -0
- package/skills/dknet-efcore-config/SKILL.md +379 -0
- package/skills/dknet-endpoint/SKILL.md +458 -0
- package/skills/dknet-entity/SKILL.md +483 -0
- package/skills/dknet-feature/SKILL.md +139 -0
- package/skills/dknet-feature-lifecycle/SKILL.md +144 -0
- package/skills/dknet-feature-remove/SKILL.md +131 -0
- package/skills/dknet-messaging-events/SKILL.md +395 -0
- package/skills/dknet-package-adoption/SKILL.md +252 -0
- package/skills/dknet-platform-config/SKILL.md +342 -0
- package/skills/dknet-project-structure/SKILL.md +148 -0
- package/skills/dknet-queries-specs/SKILL.md +330 -0
- package/skills/dknet-scaffold/SKILL.md +209 -0
- package/skills/dknet-unit-tests/SKILL.md +382 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dknet-bdd-tests
|
|
3
|
+
description: Create and maintain Reqnroll + NUnit BDD .feature scenarios and step bindings in Minimal.App.BDDTests — request/status/response-body scenarios and domain-event side effects observed via log capture, for the hand-written PurchaseOrder and generator-driven Product samples. Use when adding or updating HTTP-facing scenarios for a DKNet.Templates feature. Result-level Result-object assertions, architecture rules and pure functional tests belong in the `dknet-unit-tests` skill instead — do not duplicate a behavior here that xUnit already covers. Invoke as `/dknet-bdd-tests <Feature>` to scaffold it for a feature.
|
|
4
|
+
metadata:
|
|
5
|
+
kind: workflow
|
|
6
|
+
arguments: "<Feature> e.g. Orders"
|
|
7
|
+
allowed-tools: Read, Grep, Glob, Edit, Write, Bash
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Usage: `/dknet-bdd-tests <Feature> e.g. Orders`
|
|
11
|
+
|
|
12
|
+
# BDD tests (Minimal.App.BDDTests)
|
|
13
|
+
|
|
14
|
+
## What BDD owns vs xUnit
|
|
15
|
+
|
|
16
|
+
BDD owns user-facing HTTP behavior: request → status code → response body, and domain-event side effects
|
|
17
|
+
observed through captured log lines. xUnit (`dknet-unit-tests`) owns architecture/convention rules, pure
|
|
18
|
+
functional tests (entity methods, validators, specs), and result-level integration assertions on the
|
|
19
|
+
handler's `IResult`/`IResultBase` object. Schema/model/migration assertions never belong in BDD. Don't
|
|
20
|
+
write a BDD scenario for a rule already proven at the `Result` level in xUnit unless it's the one place
|
|
21
|
+
that rule is reachable over HTTP — and don't add a duplicate xUnit HTTP test for a rule a BDD scenario
|
|
22
|
+
already proves.
|
|
23
|
+
|
|
24
|
+
Both shipped suites are teaching material: every scenario must be about the `PurchaseOrder` (manual) or
|
|
25
|
+
`Product` (automated) sample's business behavior, never about logging, health checks, CORS, security
|
|
26
|
+
headers, rate limits, or config binding.
|
|
27
|
+
|
|
28
|
+
## Infrastructure
|
|
29
|
+
|
|
30
|
+
`Support/BddApiFactory : TestApiFactoryBase("bdd-tests")` (built on the same base as the xUnit suite's
|
|
31
|
+
`ApiFixture` — see `dknet-unit-tests` for what the base class provides) overrides two things:
|
|
32
|
+
|
|
33
|
+
- `AddFeatureOverrides` sets `FeatureManagement:RequireAuthorization = "false"` unconditionally, and
|
|
34
|
+
`ConnectionStrings:Redis` only for the one `@redis`-tagged scenario.
|
|
35
|
+
- `ConfigureTestServices` swaps in `UnexpectedErrorTriggerMapper` (an `IMapper` that throws when mapping a
|
|
36
|
+
`PurchaseOrder` named `"__drk1515-unexpected-error-trigger__"`, the suite's only reachable seam for a
|
|
37
|
+
genuine unhandled exception through a real route) and, for the `@redis` scenario, swaps the idempotency
|
|
38
|
+
key store for the Redis-backed one.
|
|
39
|
+
|
|
40
|
+
`Support/ApiHooks` is a static `[Binding]` class:
|
|
41
|
+
|
|
42
|
+
```csharp
|
|
43
|
+
[BeforeTestRun]
|
|
44
|
+
public static void BeforeTestRun()
|
|
45
|
+
{
|
|
46
|
+
_factory = new BddApiFactory();
|
|
47
|
+
_client = _factory.CreateClient();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[BeforeScenario(Order = 0)]
|
|
51
|
+
public async Task BeforeScenarioAsync()
|
|
52
|
+
{
|
|
53
|
+
await _factory.ResetDatabaseAsync();
|
|
54
|
+
_factory.LogCapture.Clear();
|
|
55
|
+
objectContainer.RegisterInstanceAs<HttpClient>(_client);
|
|
56
|
+
objectContainer.RegisterInstanceAs(_factory);
|
|
57
|
+
objectContainer.RegisterInstanceAs(new ScenarioState());
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The host boots once for the whole run; every scenario gets a reset database, a cleared `LogCapture`, and
|
|
62
|
+
fresh `ScenarioState`. **Never add a second `[BeforeTestRun]`** — Reqnroll runs it at assembly level, and a
|
|
63
|
+
second one races the shared host's startup/teardown.
|
|
64
|
+
|
|
65
|
+
`Support/ScenarioState` is a plain bag (`Response`, `ResponseBody`) a step class writes to and later steps
|
|
66
|
+
read from — inject it by constructor, same as `HttpClient` and `BddApiFactory`.
|
|
67
|
+
|
|
68
|
+
`Support/CommonSteps` holds steps shared by more than one feature so exact wording isn't duplicated (which
|
|
69
|
+
Reqnroll would flag as ambiguous): `the request is rejected`, `the response status is (\d+)`, `the response
|
|
70
|
+
is (\d+)` (a second wording for the same assertion — the acceptance criteria used frozen text, not
|
|
71
|
+
paraphrased to match house style), `catalogue-ops holds the scope "..."` /
|
|
72
|
+
`holds every scope the operation needs` / `is signed in` (no-ops — the BDD host runs with
|
|
73
|
+
`RequireAuthorization = false`, so no policy is ever evaluated; scope-gated behavior is xUnit's
|
|
74
|
+
`AuthOnApiFixture` territory), `the response body carries a trace identifier`, `... carries a code naming
|
|
75
|
+
the rule that refused` (asserts the `precondition.` prefix plus a rule segment, not just a non-empty
|
|
76
|
+
string), and `the response names the field it refused`. Reuse these before writing a new one with the same
|
|
77
|
+
meaning.
|
|
78
|
+
|
|
79
|
+
## Layout
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
Features/<Domain>/<Name>.feature
|
|
83
|
+
Features/<Domain>/Steps/<Name>Steps.cs
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each step class is `[Binding]`, constructor-injects `HttpClient client, ScenarioState state, BddApiFactory
|
|
87
|
+
factory` (and any other step class it needs data from — `ProductSteps` injects `PurchaseOrderSteps` to look
|
|
88
|
+
up a purchase order id created cross-feature). Step regex must match feature text exactly; a phrase
|
|
89
|
+
typo'd between the `.feature` and the `[Given]/[When]/[Then]` attribute leaves the step undefined at run
|
|
90
|
+
time, not a compile error. Tag scenarios by domain (`@PurchaseOrder`, `@Product`) and, for cross-cutting
|
|
91
|
+
integration scenarios, `@integration`, filterable with `--filter "TestCategory=PurchaseOrder"`. Use
|
|
92
|
+
`Scenario Outline` + `Examples` for a validation table (several inputs, one shared assertion shape) instead
|
|
93
|
+
of repeating near-identical scenarios.
|
|
94
|
+
|
|
95
|
+
## Worked example — PurchaseOrder (manual mode, full feature)
|
|
96
|
+
|
|
97
|
+
`Features/PurchaseOrders/PurchaseOrder.feature`:
|
|
98
|
+
|
|
99
|
+
```gherkin
|
|
100
|
+
@PurchaseOrder
|
|
101
|
+
Feature: Purchase order lifecycle (manual sample)
|
|
102
|
+
Background:
|
|
103
|
+
Given the service is running with no Redis connection configured
|
|
104
|
+
|
|
105
|
+
Scenario: Creating a purchase order persists it and returns its details
|
|
106
|
+
When I create a purchase order for customer "Acme Pte Ltd" with amount 250.00
|
|
107
|
+
Then the response status is 201
|
|
108
|
+
And the purchase order response has customer name "Acme Pte Ltd" and amount 250.00
|
|
109
|
+
And the purchase order response status is "placed"
|
|
110
|
+
|
|
111
|
+
Scenario: Replaying the same idempotency key on create does not create a second order
|
|
112
|
+
When I create a purchase order for customer "Acme Pte Ltd" with amount 250.00 using idempotency key "11111111-1111-1111-1111-111111111111"
|
|
113
|
+
And I replay the same create request with idempotency key "11111111-1111-1111-1111-111111111111"
|
|
114
|
+
Then both responses report the same purchase order id
|
|
115
|
+
|
|
116
|
+
Scenario: Cancelling a purchase order succeeds once and fails the second time
|
|
117
|
+
Given a purchase order exists for customer "Initech LLC" with amount 50.00
|
|
118
|
+
When I cancel that purchase order
|
|
119
|
+
Then the response status is 200
|
|
120
|
+
When I cancel that purchase order again
|
|
121
|
+
Then the response status is 409
|
|
122
|
+
|
|
123
|
+
Scenario Outline: Creating a purchase order rejects invalid input
|
|
124
|
+
When I create a purchase order for customer "<customerName>" with amount <amount>
|
|
125
|
+
Then the response status is 400
|
|
126
|
+
Examples:
|
|
127
|
+
| customerName | amount |
|
|
128
|
+
| | 100.00 |
|
|
129
|
+
| Acme Pte Ltd | 0 |
|
|
130
|
+
|
|
131
|
+
Scenario: Creating a purchase order without an idempotency key is rejected
|
|
132
|
+
When I create a purchase order for customer "Acme Pte Ltd" with amount 100.00 without an idempotency key
|
|
133
|
+
Then the request is rejected
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Key step bodies from `Steps/PurchaseOrderSteps.cs` — a fresh `Guid.NewGuid()` per plain create, a caller
|
|
137
|
+
supplied key for the idempotency scenarios, and the header on every create:
|
|
138
|
+
|
|
139
|
+
```csharp
|
|
140
|
+
[When(@"I create a purchase order for customer ""(.*)"" with amount (.*)")]
|
|
141
|
+
public Task WhenICreateAPurchaseOrder(string customerName, decimal amount) =>
|
|
142
|
+
CreateAsync(customerName, amount, Guid.NewGuid().ToString());
|
|
143
|
+
|
|
144
|
+
private async Task CreateAsync(string customerName, decimal amount, string idempotencyKey)
|
|
145
|
+
{
|
|
146
|
+
using var request = new HttpRequestMessage(HttpMethod.Post, "/v1/purchase-orders")
|
|
147
|
+
{
|
|
148
|
+
Content = JsonContent.Create(new { customerName, amount })
|
|
149
|
+
};
|
|
150
|
+
request.Headers.Add("X-Idempotency-Key", idempotencyKey);
|
|
151
|
+
state.Response = await client.SendAsync(request);
|
|
152
|
+
state.ResponseBody = await state.Response.Content.ReadAsStringAsync();
|
|
153
|
+
if (state.Response.IsSuccessStatusCode)
|
|
154
|
+
{
|
|
155
|
+
var dto = JsonSerializer.Deserialize<PurchaseOrderDto>(state.ResponseBody, SharedConsts.JsonSerializerOptions);
|
|
156
|
+
_lastId = dto!.Id;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Update/cancel/delete/get reuse `_lastId` from the last create; list deserializes the response as a bare
|
|
162
|
+
JSON array (`List<PurchaseOrderDto>`, the hand-mapped route's own shape) via
|
|
163
|
+
`SharedConsts.JsonSerializerOptions` — always deserialize through that options instance, not a fresh
|
|
164
|
+
default one, so casing matches what the API actually emits.
|
|
165
|
+
|
|
166
|
+
## Worked example — Product (automated mode: event log + precondition 409)
|
|
167
|
+
|
|
168
|
+
`Features/Products/Product.feature` proves the generator-driven CRUD slice. Two scenarios worth reading in
|
|
169
|
+
full:
|
|
170
|
+
|
|
171
|
+
```gherkin
|
|
172
|
+
Scenario: Creating a product raises the internal ProductCreatedEvent
|
|
173
|
+
When I create a product named "Widget" with price 9.99
|
|
174
|
+
Then a log line reports the automated sample product was created
|
|
175
|
+
|
|
176
|
+
@integration
|
|
177
|
+
Scenario: A product name that is already taken is refused
|
|
178
|
+
Given catalogue-ops holds the scope "products.write"
|
|
179
|
+
And the product "Widget" priced 9.99 SGD exists
|
|
180
|
+
When catalogue-ops creates the product "Widget" priced 12.00 SGD
|
|
181
|
+
Then the response is 409
|
|
182
|
+
And exactly 1 product is named "Widget"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The log-line step just asserts on `LogCapture.Messages` — no polling needed here because the response body
|
|
186
|
+
already round-trips through the handler that logs synchronously before returning; if a future scenario
|
|
187
|
+
asserts on an *async consumer's* log line instead (a domain event's own subscriber, not the handler that
|
|
188
|
+
raised it), poll with `Eventually.IsTrueAsync(...)` from `Minimal.App.TestSupport` rather than asserting
|
|
189
|
+
immediately — the in-memory bus publishes non-blocking:
|
|
190
|
+
|
|
191
|
+
```csharp
|
|
192
|
+
[Then("a log line reports the automated sample product was created")]
|
|
193
|
+
public void ThenALogLineReportsTheAutomatedSampleProductWasCreated() =>
|
|
194
|
+
factory.LogCapture.Messages.ShouldContain(m => m.Contains("AutomatedSample product created", StringComparison.Ordinal));
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Product create sends **no** `X-Idempotency-Key` header at all — the generated create route has no
|
|
198
|
+
`.RequiredIdempotentKey()` call, so a replayed request is a fresh create, not a replay. Never assert
|
|
199
|
+
idempotent-replay behavior on a generated create route.
|
|
200
|
+
|
|
201
|
+
Another scenario in the same file documents a real, permanent gap rather than a bug to fix:
|
|
202
|
+
|
|
203
|
+
```gherkin
|
|
204
|
+
Scenario: A negative price is still accepted — a known and accepted limitation of the generated path
|
|
205
|
+
When I create a product named "Broken Widget" with price -1
|
|
206
|
+
Then the response status is 201
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`[Range(0.01, double.MaxValue)]` is forwarded onto the generated request but never evaluated, because the
|
|
210
|
+
.NET 10 minimal-API validation source generator can't see through `DKNet.AspCore.Extensions`'s generic
|
|
211
|
+
`MapPost<TRequest,TResponse>` wrapper. **Never assert 400 for a DataAnnotations violation on a generated
|
|
212
|
+
create/update route** — write the scenario the way this one is written, asserting the (surprising) 201.
|
|
213
|
+
|
|
214
|
+
`ProductList.feature` is a regression fence around the generic `MapGetList<Product, Guid, ProductDto>()`
|
|
215
|
+
list route: paging envelope (`totalItemCount`/`hasNextPage`/`hasPreviousPage`), `pageSize` clamped (not
|
|
216
|
+
rejected) above the max, `orderBy`/`desc`, `filter=field:op:value` (including the `In` operator and
|
|
217
|
+
multiple filters ANDed), `search` (substring, 2-char minimum), and two security-boundary scenarios: an
|
|
218
|
+
unknown filter field 400s, and filtering by an excluded DTO column (`ownedBy`, excluded via
|
|
219
|
+
`[GenerateDto(... Exclude ...)]`) 400s rather than silently matching against the underlying entity. Summarize
|
|
220
|
+
new list-contract scenarios the same way when a feature's list route gains its own filters or computed DTO
|
|
221
|
+
fields.
|
|
222
|
+
|
|
223
|
+
## Assertion depth
|
|
224
|
+
|
|
225
|
+
Assert status code, then the fields the scenario is actually about — not just "success". The manual
|
|
226
|
+
sample's error body shape: `errors[]` (each with `message`, optional `code`/`field`), `code`, `traceId`
|
|
227
|
+
(see `CommonSteps`'s trace-identifier and precondition-code steps above). Prefer a structured JSON property
|
|
228
|
+
check (`JsonDocument.Parse(...).RootElement.GetProperty(...)`) over a raw substring match on the response
|
|
229
|
+
body when the contract has a real shape to check against; a substring check is fine for a fixed literal
|
|
230
|
+
like `"status":"placed"` where the surrounding shape isn't in question.
|
|
231
|
+
|
|
232
|
+
## Mode differences
|
|
233
|
+
|
|
234
|
+
- **manual** (PurchaseOrder): create requires `X-Idempotency-Key`; a missing key is rejected, a replayed
|
|
235
|
+
key returns the same order id. Acting user comes from `[FromClaim(ClaimTypes.Name)] ByUser`.
|
|
236
|
+
- **auto** (Product): create has no idempotency key and does not enforce DataAnnotations — never write a
|
|
237
|
+
scenario expecting either. The acting user for audit stamps comes from the demo identity: the BDD host
|
|
238
|
+
runs with `EnableDemoAuthentication = true` (`appsettings.Testing.json`) and
|
|
239
|
+
`RequireAuthorization = false` (`BddApiFactory`), so every request is the built-in demonstration caller,
|
|
240
|
+
not an anonymous one — `catalogue-ops holds the scope "..."` steps are no-ops for exactly that reason.
|
|
241
|
+
|
|
242
|
+
## Commands
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
dotnet test ApiEndpoints/Minimal.App.BDDTests/Minimal.App.BDDTests.csproj --filter "TestCategory=PurchaseOrder"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Step-by-step
|
|
249
|
+
|
|
250
|
+
1. Confirm the behavior is HTTP-shaped (request → status → body) or an event side-effect via log capture —
|
|
251
|
+
otherwise it belongs in `dknet-unit-tests`.
|
|
252
|
+
2. Add `Features/<Domain>/<Name>.feature` under the right tag; reuse `CommonSteps` wording before coining
|
|
253
|
+
new phrasing for something already covered (rejected request, status code, trace id, precondition code).
|
|
254
|
+
3. Add `Features/<Domain>/Steps/<Name>Steps.cs`, `[Binding]`, constructor-inject `HttpClient`,
|
|
255
|
+
`ScenarioState`, `BddApiFactory`, and any other step class whose state you need.
|
|
256
|
+
4. Manual-mode create: generate a fresh `Guid.NewGuid()` for `X-Idempotency-Key` in each `[When]` step that
|
|
257
|
+
creates a resource, unless the scenario is specifically about idempotency (use a fixed key there).
|
|
258
|
+
5. Cover: happy path, not-found, a guarded transition refused on retry, and (auto mode) the event's log
|
|
259
|
+
line and the generated route's un-enforced-DataAnnotations behavior where relevant.
|
|
260
|
+
6. Run the filtered test for the tag before the full BDD project.
|
|
261
|
+
|
|
262
|
+
## Common mistakes
|
|
263
|
+
|
|
264
|
+
- **What you might expect**: a generated Product create route rejects an idempotency replay like
|
|
265
|
+
PurchaseOrder does. **What actually happens**: it creates a second product. **Why**: the generated route
|
|
266
|
+
has no `.RequiredIdempotentKey()` call — there is nothing to replay against.
|
|
267
|
+
- **What you might expect**: a negative price on `Product` create returns 400. **What actually happens**:
|
|
268
|
+
201. **Why**: the forwarded `[Range]` DataAnnotation is never evaluated on a generic-wrapper route — this
|
|
269
|
+
is documented, accepted behavior, not a bug to "fix" with a failing scenario.
|
|
270
|
+
- **What you might expect**: asserting a log line right after the response comes back is safe. **What
|
|
271
|
+
actually happens**: it's sometimes missing for an async consumer. **Why**: the in-memory bus publishes
|
|
272
|
+
non-blocking; poll instead of asserting immediately when the log line comes from a separate event
|
|
273
|
+
consumer rather than the handling code itself.
|
|
274
|
+
- **What you might expect**: adding a second `[BeforeTestRun]` hook in a new feature's step file to set up
|
|
275
|
+
feature-specific state. **What actually happens**: it races `ApiHooks`'s host bootstrap. **Why**: Reqnroll
|
|
276
|
+
runs every `[BeforeTestRun]` at assembly level with no defined ordering guarantee between classes; put
|
|
277
|
+
one-time setup in `ApiHooks` or in `[BeforeScenario]` instead.
|
|
278
|
+
- **What you might expect**: forgetting `X-Idempotency-Key` on a manual-sample create just needs a retry.
|
|
279
|
+
**What actually happens**: the request is rejected outright (400/`the request is rejected`) — the header
|
|
280
|
+
is required, not merely deduplicating.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
# Workflow: `/dknet-bdd-tests`
|
|
285
|
+
|
|
286
|
+
The procedure an agent follows when invoked with arguments. The reference sections above are the rules it applies.
|
|
287
|
+
|
|
288
|
+
You are DKNet BDD Test Engineer.
|
|
289
|
+
|
|
290
|
+
Your job is to create, update, and validate BDD scenarios for this repository with contract-first assertions and deterministic step bindings.
|
|
291
|
+
|
|
292
|
+
### User Input
|
|
293
|
+
|
|
294
|
+
$ARGUMENTS
|
|
295
|
+
|
|
296
|
+
### Required Skill Loading
|
|
297
|
+
|
|
298
|
+
Before any BDD design or edits:
|
|
299
|
+
1. Load and follow the BDD skill at the reference sections above.
|
|
300
|
+
2. Use this skill's `checklist.md` as the completion gate.
|
|
301
|
+
|
|
302
|
+
### Scope
|
|
303
|
+
|
|
304
|
+
Work only on BDD test artifacts and closely related support wiring:
|
|
305
|
+
- `ApiEndpoints/Minimal.App.BDDTests/Features/**/*.feature`
|
|
306
|
+
- `ApiEndpoints/Minimal.App.BDDTests/Features/**/Steps/*.cs`
|
|
307
|
+
- `ApiEndpoints/Minimal.App.BDDTests/Support/*.cs`
|
|
308
|
+
- `ApiEndpoints/Minimal.App.BDDTests/*.csproj`
|
|
309
|
+
|
|
310
|
+
### Constraints
|
|
311
|
+
|
|
312
|
+
- Use `specs/<feature>/contracts/*` as the assertion source of truth.
|
|
313
|
+
- Treat `docs/features/**` and `specs/**` as reference context for scenario coverage and wording.
|
|
314
|
+
- Keep step phrases and `[Given]/[When]/[Then]` attributes exactly matched.
|
|
315
|
+
- Validate response at three levels whenever applicable:
|
|
316
|
+
- HTTP status code
|
|
317
|
+
- response structure (`isSuccess`, `value`, `errors`, required objects/arrays)
|
|
318
|
+
- key data fields and expected values
|
|
319
|
+
- Use `SharedConsts.JsonSerializerOptions` for request serialization.
|
|
320
|
+
- Include required request headers when contracts require them. A **manual-flow** create route
|
|
321
|
+
requires a fresh `Guid.NewGuid()` `X-Idempotency-Key` per `[When]` step; an **automated-flow**
|
|
322
|
+
generated create route has no idempotency filter, so do not assert replay behavior against it.
|
|
323
|
+
- Assert only behavior the endpoint actually has. An automated-flow route does not enforce its
|
|
324
|
+
forwarded DataAnnotations — a scenario expecting `400` from an out-of-range value will fail against
|
|
325
|
+
a `201`. Cover that gap by asserting what happens, or leave it to the manual flow.
|
|
326
|
+
- Do not implement unrelated domain/business logic outside BDD test scope.
|
|
327
|
+
|
|
328
|
+
### Workflow
|
|
329
|
+
|
|
330
|
+
1. Build context map from:
|
|
331
|
+
- `docs/features/<feature>/`
|
|
332
|
+
- `specs/<feature>/spec.md`
|
|
333
|
+
- `specs/<feature>/contracts/*`
|
|
334
|
+
2. Produce or update `.feature` scenarios:
|
|
335
|
+
- Happy path
|
|
336
|
+
- Business-rule failure
|
|
337
|
+
- Validation failure
|
|
338
|
+
3. Implement/adjust step bindings in `Steps/*.cs`.
|
|
339
|
+
4. Run validation:
|
|
340
|
+
- `dotnet build -c Release`
|
|
341
|
+
- `dotnet test ApiEndpoints/Minimal.App.BDDTests`
|
|
342
|
+
5. Report:
|
|
343
|
+
- changed files
|
|
344
|
+
- scenario count
|
|
345
|
+
- pass/fail results
|
|
346
|
+
- unresolved contract gaps (if any)
|
|
347
|
+
|
|
348
|
+
### Output Format
|
|
349
|
+
|
|
350
|
+
Always provide:
|
|
351
|
+
1. BDD phase status
|
|
352
|
+
2. Artifacts changed
|
|
353
|
+
3. Assertion coverage summary (status + shape + key fields)
|
|
354
|
+
4. Test results summary
|
|
355
|
+
5. Remaining risks or blockers
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# BDD Scenario Skill Checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist before considering BDD scenario work complete.
|
|
4
|
+
|
|
5
|
+
## Context Coverage
|
|
6
|
+
|
|
7
|
+
- [ ] Confirmed the behavior is HTTP-shaped (request → status → body) or an event side effect via log
|
|
8
|
+
capture — otherwise it belongs in the `dknet-unit-tests` skill instead
|
|
9
|
+
- [ ] Reviewed the feature's AppServices request/handler code as the assertion source of truth (status
|
|
10
|
+
codes, error codes, response fields)
|
|
11
|
+
- [ ] Checked `CommonSteps` for an existing step with the same meaning before writing a new one
|
|
12
|
+
- [ ] Captured at least one edge case (not-found, a guarded transition refused on retry, invalid input)
|
|
13
|
+
|
|
14
|
+
## Scenario Quality
|
|
15
|
+
|
|
16
|
+
- [ ] `.feature` file has clear business title and purpose
|
|
17
|
+
- [ ] Includes happy path scenario
|
|
18
|
+
- [ ] Includes business-rule failure scenario
|
|
19
|
+
- [ ] Includes validation failure scenario
|
|
20
|
+
- [ ] Uses stable domain language (no implementation jargon)
|
|
21
|
+
|
|
22
|
+
## Binding Quality
|
|
23
|
+
|
|
24
|
+
- [ ] Every step has exactly one matching `[Given]/[When]/[Then]` binding
|
|
25
|
+
- [ ] Constructor injection uses scenario-registered dependencies
|
|
26
|
+
- [ ] Request serialization uses `SharedConsts.JsonSerializerOptions`
|
|
27
|
+
- [ ] Required headers (for example idempotency) are present
|
|
28
|
+
- [ ] Assertions verify status code, contract-defined JSON structure, and key data fields
|
|
29
|
+
- [ ] For generated response DTO contracts, assertions include representative entity-derived fields
|
|
30
|
+
- [ ] Success assertions validate required `value` fields (not only success flag)
|
|
31
|
+
- [ ] Failure assertions validate `errors` array/object shape and expected messages/codes
|
|
32
|
+
- [ ] No assertion relies only on substring matching when structured contract fields exist
|
|
33
|
+
|
|
34
|
+
## Validation
|
|
35
|
+
|
|
36
|
+
- [ ] `dotnet build -c Release` succeeds
|
|
37
|
+
- [ ] `dotnet test ApiEndpoints/Minimal.App.BDDTests/Minimal.App.BDDTests.csproj` passes
|
|
38
|
+
- [ ] No undefined or pending Reqnroll steps
|
|
39
|
+
- [ ] Scenario names are readable in test output
|