@brunosps00/dev-workflow 0.7.0 → 0.8.1

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 (50) hide show
  1. package/README.md +20 -4
  2. package/lib/constants.js +8 -0
  3. package/lib/install-deps.js +13 -0
  4. package/package.json +1 -1
  5. package/scaffold/en/commands/dw-deps-audit.md +326 -0
  6. package/scaffold/en/commands/dw-dockerize.md +321 -0
  7. package/scaffold/en/commands/dw-find-skills.md +158 -0
  8. package/scaffold/en/commands/dw-fix-qa.md +34 -13
  9. package/scaffold/en/commands/dw-help.md +4 -0
  10. package/scaffold/en/commands/dw-new-project.md +350 -0
  11. package/scaffold/en/commands/dw-run-qa.md +124 -23
  12. package/scaffold/en/templates/project-onepager.md +129 -0
  13. package/scaffold/pt-br/commands/dw-deps-audit.md +326 -0
  14. package/scaffold/pt-br/commands/dw-dockerize.md +321 -0
  15. package/scaffold/pt-br/commands/dw-find-skills.md +158 -0
  16. package/scaffold/pt-br/commands/dw-fix-qa.md +34 -13
  17. package/scaffold/pt-br/commands/dw-help.md +4 -0
  18. package/scaffold/pt-br/commands/dw-new-project.md +350 -0
  19. package/scaffold/pt-br/commands/dw-run-qa.md +124 -23
  20. package/scaffold/pt-br/templates/project-onepager.md +129 -0
  21. package/scaffold/skills/api-testing-recipes/SKILL.md +104 -0
  22. package/scaffold/skills/api-testing-recipes/recipes/dotnet-webapp-factory.md +168 -0
  23. package/scaffold/skills/api-testing-recipes/recipes/http-rest-client.md +130 -0
  24. package/scaffold/skills/api-testing-recipes/recipes/pytest-httpx.md +157 -0
  25. package/scaffold/skills/api-testing-recipes/recipes/rust-reqwest.md +173 -0
  26. package/scaffold/skills/api-testing-recipes/recipes/supertest-node.md +153 -0
  27. package/scaffold/skills/api-testing-recipes/references/auth-patterns.md +138 -0
  28. package/scaffold/skills/api-testing-recipes/references/log-conventions.md +117 -0
  29. package/scaffold/skills/api-testing-recipes/references/matrix-conventions.md +68 -0
  30. package/scaffold/skills/api-testing-recipes/references/openapi-driven.md +97 -0
  31. package/scaffold/skills/docker-compose-recipes/SKILL.md +84 -0
  32. package/scaffold/skills/docker-compose-recipes/references/compose-composition.md +91 -0
  33. package/scaffold/skills/docker-compose-recipes/references/env-conventions.md +51 -0
  34. package/scaffold/skills/docker-compose-recipes/references/healthcheck-patterns.md +54 -0
  35. package/scaffold/skills/docker-compose-recipes/references/prod-vs-dev.md +85 -0
  36. package/scaffold/skills/docker-compose-recipes/services/elasticsearch.yml +34 -0
  37. package/scaffold/skills/docker-compose-recipes/services/jaeger.yml +24 -0
  38. package/scaffold/skills/docker-compose-recipes/services/localstack.yml +30 -0
  39. package/scaffold/skills/docker-compose-recipes/services/mailhog.yml +23 -0
  40. package/scaffold/skills/docker-compose-recipes/services/mailpit.yml +27 -0
  41. package/scaffold/skills/docker-compose-recipes/services/meilisearch.yml +28 -0
  42. package/scaffold/skills/docker-compose-recipes/services/memcached.yml +19 -0
  43. package/scaffold/skills/docker-compose-recipes/services/minio.yml +30 -0
  44. package/scaffold/skills/docker-compose-recipes/services/mysql.yml +30 -0
  45. package/scaffold/skills/docker-compose-recipes/services/postgres.yml +30 -0
  46. package/scaffold/skills/docker-compose-recipes/services/rabbitmq.yml +29 -0
  47. package/scaffold/skills/docker-compose-recipes/services/redis.yml +25 -0
  48. package/scaffold/skills/docker-compose-recipes/services/smtp4dev.yml +27 -0
  49. package/scaffold/skills/docker-compose-recipes/services/traefik.yml +42 -0
  50. package/scaffold/skills/docker-compose-recipes/services/typesense.yml +25 -0
@@ -0,0 +1,129 @@
1
+ ---
2
+ type: project-onepager
3
+ schema_version: "1.0"
4
+ status: draft
5
+ date: YYYY-MM-DD
6
+ shape: frontend | backend | fullstack
7
+ languages: []
8
+ frameworks: { web: '', api: '' }
9
+ package_manager: ''
10
+ monorepo: ''
11
+ services: []
12
+ ---
13
+
14
+ # Projeto: [Nome do projeto]
15
+
16
+ ## Proposito
17
+
18
+ [Um paragrafo em linguagem de produto. Quem vai usar, qual problema resolve, como o sucesso se parece em 6-12 semanas. Evite linguagem de implementacao.]
19
+
20
+ ## Stack Selecionado
21
+
22
+ | Camada | Escolha | Justificativa |
23
+ |--------|---------|---------------|
24
+ | Forma | frontend / backend / fullstack | [por que essa forma — superficie unica, API para parceiros, etc.] |
25
+ | Frontend | Next.js / Vite+React / n/a | [por que esse framework — SSR, simplicidade SPA, etc.] |
26
+ | Backend | FastAPI / ASP.NET Core minimal / Axum / Fastify / n/a | [por que — expertise do time, ecosystem, alvo de latencia] |
27
+ | Database | Postgres / MySQL / SQLite / MongoDB / nenhum | [por que esse DB — relacional, JSON-heavy, transacional, etc.] |
28
+ | Cache | Redis / Memcached / nenhum | [por que ou por que nao] |
29
+ | Fila | BullMQ / Celery / RabbitMQ / LocalStack SQS / nenhum | [por que ou nao + sync vs async workers] |
30
+ | Email — dev | MailHog (default) / Mailpit / smtp4dev / nenhum | [normalmente MailHog — captura only, nunca envia real] |
31
+ | Email — prod | SMTP / SendGrid / Resend / Postmark / SES / nenhum | [por que esse provider — volume, deliverability, custo] |
32
+ | Object storage | S3 / MinIO (dev) / GCS / nenhum | [por que ou nao] |
33
+ | Search | Meilisearch / Typesense / Elasticsearch / nenhum | [por que esse engine — features, escala, simplicidade] |
34
+ | Observability | Sentry / OTel + Jaeger / nenhum | [por que essa abordagem — so error tracking, tracing completo, etc.] |
35
+ | Reverse proxy | Traefik / Caddy / nenhum | [normalmente so multi-host dev ou prod] |
36
+ | Auth | NextAuth / Lucia / Clerk / fastapi-users / dotnet Identity / JWT custom / nenhum | [por que — social login, B2B, etc.] |
37
+ | Linter / formatter | Biome / ESLint+Prettier / Ruff+Black / dotnet format / cargo fmt+clippy | [preferencia do time] |
38
+ | Package manager | pnpm / npm / yarn / poetry / uv / cargo / dotnet | [preferencia do time] |
39
+ | Monorepo orchestrator | pnpm workspaces / npm workspaces / Turborepo / Nx / n/a | [so para fullstack — caching/build] |
40
+ | CI | GitHub Actions / nenhum | [normalmente GitHub Actions; pular so para repos nao-publicos] |
41
+
42
+ ## Servicos & Infra
43
+
44
+ [Servicos gerados a partir da skill docker-compose-recipes. Preenchido pelo /dw-new-project.]
45
+
46
+ | Servico | Porta (host) | UI | Credenciais default |
47
+ |---------|--------------|----|--------------------|
48
+ | postgres | 5432 | — | POSTGRES_USER=app, POSTGRES_PASSWORD=app, POSTGRES_DB=app |
49
+ | redis | 6379 | — | (sem auth em dev) |
50
+ | mailhog | 1025 (smtp), 8025 (UI) | http://localhost:8025 | (sem auth) |
51
+ | ... | ... | ... | ... |
52
+
53
+ ## Diagrama da Arquitetura
54
+
55
+ ```
56
+ [Diagrama ASCII da forma escolhida. Exemplos:]
57
+
58
+ # So frontend
59
+ [ Browser ] -> [ Next.js (apps/web) ]
60
+
61
+ # Fullstack
62
+ [ Browser ] -> [ Next.js (apps/web) ] -> [ FastAPI (apps/api) ] -> [ Postgres ]
63
+ |-> [ Redis ]
64
+ |-> [ MailHog ]
65
+
66
+ # Com observability
67
+ ... -> [ FastAPI ] -> { OTLP } -> [ Jaeger ]
68
+ ```
69
+
70
+ ## Arquivos Gerados
71
+
72
+ [Preenchido pelo /dw-new-project apos Fase 3 — lista de arquivos criados com origem.]
73
+
74
+ ```
75
+ {{TARGET_DIR}}/
76
+ ├── apps/
77
+ │ ├── web/ (criado por `pnpm create next-app`)
78
+ │ └── api/ (scaffold inline — FastAPI)
79
+ ├── packages/
80
+ │ └── shared/ (criado pelo /dw-new-project)
81
+ ├── docker-compose.dev.yml (composto a partir de .agents/skills/docker-compose-recipes/)
82
+ ├── .env.example (consolidado dos servicos selecionados)
83
+ ├── .gitignore (por stack)
84
+ ├── .dockerignore (por stack)
85
+ ├── .github/workflows/ci.yml (CI com matrix por app)
86
+ ├── package.json (scripts raiz: dev:up/down/logs/reset)
87
+ ├── pnpm-workspace.yaml (se pnpm workspaces)
88
+ ├── turbo.json (se Turborepo)
89
+ ├── README.md (Quick Start + tabela de portas Local Dev)
90
+ └── .dw/
91
+ ├── rules/index.md (seed — enriquecer depois via /dw-analyze-project)
92
+ └── spec/projects/<nome>.md (este arquivo)
93
+ ```
94
+
95
+ ## Escopo MVP
96
+
97
+ [A primeira feature menor que voce vai entregar. Pensada como user stories — vai dirigir a primeira rodada de /dw-create-prd.]
98
+
99
+ - Como [persona], eu posso [acao] para que [beneficio]
100
+ - Como [persona], eu posso [acao] para que [beneficio]
101
+
102
+ Se voce ainda nao tem a primeira feature em mente, tudo bem — deixa placeholder e roda o /dw-create-prd quando tiver.
103
+
104
+ ## Nao Estou Fazendo (explicito)
105
+
106
+ [Itens tentadores adiados. Forca disciplina de escopo.]
107
+
108
+ - **[item 1]** — motivo: [fora do v1 porque...]
109
+ - **[item 2]** — motivo: [pode virar v2 se a hipotese X validar]
110
+
111
+ ## Premissas-Chave
112
+
113
+ - **[premissa sobre usuarios / mercado / escala]** — teste: [como validar]
114
+ - **[premissa sobre latencia / volume / SLAs]** — teste: [load profile, metrica alvo]
115
+
116
+ ## Perguntas em Aberto
117
+
118
+ [O que este one-pager nao consegue responder sozinho. Resolva antes do /dw-create-prd ou escale para um stakeholder.]
119
+
120
+ - [pergunta 1]
121
+ - [pergunta 2]
122
+
123
+ ## Proximo Passo
124
+
125
+ Escolha UM:
126
+
127
+ - **`/dw-create-prd`** — quando voce tem a primeira feature em mente e quer rascunhar o PRD em cima deste stack
128
+ - **`/dw-analyze-project`** — apos primeiro commit substancial, para enriquecer `.dw/rules/` com convencoes por modulo
129
+ - **`/dw-deps-audit --scan-only`** — para confirmar que nenhuma dep vulneravel veio dos templates `create-*`
@@ -0,0 +1,104 @@
1
+ ---
2
+ name: api-testing-recipes
3
+ description: Validated API-testing snippets (.http, pytest+httpx, supertest, WebApplicationFactory, reqwest) used by /dw-run-qa and /dw-fix-qa when the project has no UI. Default format is .http (REST Client) for IDE portability.
4
+ allowed-tools:
5
+ - Read
6
+ - Write
7
+ - Grep
8
+ - Glob
9
+ ---
10
+
11
+ # api-testing-recipes
12
+
13
+ Curated library of **API-testing snippets** that `/dw-run-qa` and `/dw-fix-qa` use when a project is API-only (no Playwright). Each recipe is a ready-to-customize block per stack; the default is `.http` (REST Client) for maximum portability across IDEs.
14
+
15
+ ## Why a skill (not inline)
16
+
17
+ - Each recipe is independently maintainable. Bumping `pytest` or `supertest` patterns is a one-file change.
18
+ - Discoverable by AI agents in any project the user installs dev-workflow into.
19
+ - Reusable by future commands (e.g., `dw-bench-api`, `dw-contract-test`) without duplication.
20
+
21
+ ## When to Use
22
+
23
+ Read this skill when:
24
+
25
+ - `/dw-run-qa` detected API mode (no UI deps in the manifest) or was invoked with `--api`.
26
+ - `/dw-fix-qa` is retesting a bug whose `evidence_type` is `api-log`.
27
+ - Generating a baseline test suite from an OpenAPI spec.
28
+ - Authoring contract checks against a backend.
29
+
30
+ Do NOT use when:
31
+
32
+ - The project has a UI and `/dw-run-qa` is in UI mode → use Playwright MCP instead.
33
+ - The user wants browser-level acceptance (forms, navigation, accessibility) — that's Playwright territory.
34
+
35
+ ## Available Recipes
36
+
37
+ | Format | When to use | Recipe path |
38
+ |--------|-------------|-------------|
39
+ | `.http` (REST Client) — DEFAULT | Universal. Reads in VSCode (REST Client), JetBrains (HTTP Client), Neovim (rest.nvim, kulala), Zed. Stack-agnostic. Best for projects without an existing test runner, or when devs read tests in their IDE. | `recipes/http-rest-client.md` |
40
+ | `pytest + httpx` | Python project (FastAPI, Starlette, Flask). Already runs `pytest` in CI. Async client matches FastAPI's async-first design. | `recipes/pytest-httpx.md` |
41
+ | `supertest` (Node/TS) | Node/TS project (Fastify, Express, NestJS). Already runs `vitest`/`jest`. Integrates with the app's test setup. | `recipes/supertest-node.md` |
42
+ | `WebApplicationFactory<T>` (.NET) | C# project (ASP.NET Core minimal API or MVC). Built-in support for in-process testing without HTTP overhead. | `recipes/dotnet-webapp-factory.md` |
43
+ | `reqwest + tokio::test` (Rust) | Rust project (Axum, Actix-web, Rocket). Async client matches Axum's tower-based design. | `recipes/rust-reqwest.md` |
44
+
45
+ Picking order:
46
+ 1. Default to `.http` unless the project already has an established test runner.
47
+ 2. If the project has a test runner (`pytest`, `vitest`, `dotnet test`, `cargo test`), prefer the stack-specific recipe so QA tests live alongside unit tests.
48
+ 3. The user can override during the interview/run with `--format=http|pytest|supertest|dotnet|rust`.
49
+
50
+ ## How to Compose
51
+
52
+ The composing command (`/dw-run-qa` API mode) follows this loop:
53
+
54
+ 1. **Pick the recipe** based on the rules above.
55
+ 2. **Read the recipe file** (`recipes/<name>.md`) for the variable conventions, test-matrix shape, and an example block.
56
+ 3. **For each requirement (RF-XX) in the PRD**, derive a test matrix per `references/matrix-conventions.md`:
57
+ - 200 happy path
58
+ - 4xx — validation, auth, not found, conflict
59
+ - 5xx — server error (synthetic)
60
+ - Contract drift — response shape vs OpenAPI / TS types
61
+ - Authorization cross-tenant
62
+ 4. **Generate** one file per RF in `{{PRD_PATH}}/QA/scripts/api/RF-XX-[slug].<ext>` using the recipe's structure. Wire credentials via the patterns in `references/auth-patterns.md` (NEVER hardcode tokens).
63
+ 5. **Execute** each request:
64
+ - `.http` → `curl` (Bash) or the in-IDE runner during interactive review.
65
+ - Stack-specific → the project's test runner (`pytest <files>`, `vitest run <files>`, `dotnet test --filter`, `cargo test`).
66
+ 6. **Log** every request/response per `references/log-conventions.md` to `{{PRD_PATH}}/QA/logs/api/RF-XX-[slug].log` (one JSONL line per request).
67
+ 7. **Assert** per matrix expectation: status code, response shape (use `jq` for `.http`, framework matchers per stack), headers.
68
+ 8. **Mark** PASS/FAIL per RF, citing the log path as evidence.
69
+
70
+ ## OpenAPI-Driven Mode
71
+
72
+ If the project exposes OpenAPI (`openapi.yaml`/`openapi.json` static, or `/openapi.json` in runtime for FastAPI), follow `references/openapi-driven.md` to:
73
+ - Generate a baseline of 200/4xx tests per endpoint automatically.
74
+ - Detect contract drift by diffing live responses against the spec.
75
+ - Skip endpoints marked `x-internal: true` or those without examples.
76
+
77
+ ## Variable Conventions
78
+
79
+ Every recipe uses three variable layers:
80
+
81
+ - **`@base`** — base URL (`http://localhost:3000` in dev). Set once per file.
82
+ - **`@token_admin` / `@token_user` / `@token_guest`** — credential tokens, captured from a login response or read from `.env` / `QA/test-credentials.md`.
83
+ - **`@<resource>_id`** — IDs created during a multi-step flow (e.g., create → fetch → update → delete on the same RF).
84
+
85
+ Per-recipe details in `references/auth-patterns.md`.
86
+
87
+ ## References
88
+
89
+ - `references/matrix-conventions.md` — how to derive the {200, 4xx, 5xx, contract drift, authz cross-tenant} matrix from a PRD requirement.
90
+ - `references/auth-patterns.md` — how to capture and reuse JWT / cookie / API-key credentials in scripts; refresh-token patterns; scoped credentials per role.
91
+ - `references/openapi-driven.md` — generating a baseline test suite from an OpenAPI spec; detecting contract drift.
92
+ - `references/log-conventions.md` — JSONL log shape (one line per request: timestamp, method, url, status, request_headers, request_body, response_headers, response_body, ms).
93
+
94
+ ## Rules
95
+
96
+ - **Default to `.http`** unless the project already has a test runner.
97
+ - **Never hardcode credentials**. Always use `@variable` references that resolve to env vars or files outside git.
98
+ - **Always log request + response** so the bug evidence is reproducible without re-running.
99
+ - **One file per RF**. Don't pile every requirement into one giant test file.
100
+ - **PASS/FAIL per RF, never per request**. A request that returns 401 when the matrix says it should is a PASS for that case.
101
+
102
+ ## Inspired by
103
+
104
+ Hand-curated by dev-workflow. `.http` syntax follows the JetBrains HTTP Client / VSCode REST Client conventions. Per-stack recipes adapt patterns from each ecosystem's official testing docs (FastAPI testing tutorial, NestJS testing recipes, Microsoft.AspNetCore.Mvc.Testing docs, Axum testing examples).
@@ -0,0 +1,168 @@
1
+ # Recipe: `WebApplicationFactory<T>` + xUnit (.NET)
2
+
3
+ Use for ASP.NET Core minimal API or MVC. Microsoft's official integration-testing pattern. Runs the full pipeline (DI, middleware, filters) in-process — no Kestrel port, no flake.
4
+
5
+ ## File shape
6
+
7
+ `{{PRD_PATH}}/QA/scripts/api/RF_XX_[Slug]Tests.cs`
8
+
9
+ ```csharp
10
+ using System.Net;
11
+ using System.Net.Http.Headers;
12
+ using System.Net.Http.Json;
13
+ using Microsoft.AspNetCore.Mvc.Testing;
14
+ using Xunit;
15
+
16
+ namespace YourProject.QA.Api;
17
+
18
+ public class RF_XX_CreateUserTests : IClassFixture<WebApplicationFactory<Program>>
19
+ {
20
+ private readonly WebApplicationFactory<Program> _factory;
21
+ private readonly string _tokenAdmin = Environment.GetEnvironmentVariable("QA_TOKEN_ADMIN") ?? "";
22
+ private readonly string _tokenOtherOrg = Environment.GetEnvironmentVariable("QA_TOKEN_OTHER_ORG") ?? "";
23
+
24
+ public RF_XX_CreateUserTests(WebApplicationFactory<Program> factory) => _factory = factory;
25
+
26
+ private HttpClient Client(string? token = null)
27
+ {
28
+ var c = _factory.CreateClient();
29
+ if (!string.IsNullOrEmpty(token))
30
+ c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
31
+ return c;
32
+ }
33
+
34
+ private record CreateUserDto(string Email, string Name);
35
+ private record UserResponse(string Id, string Email, string Name, DateTime CreatedAt);
36
+
37
+ [Fact]
38
+ public async Task HappyPath_Returns201()
39
+ {
40
+ var r = await Client(_tokenAdmin).PostAsJsonAsync("/users",
41
+ new CreateUserDto($"qa-{Guid.NewGuid():N}@example.com", "QA"));
42
+ Assert.Equal(HttpStatusCode.Created, r.StatusCode);
43
+ var body = await r.Content.ReadFromJsonAsync<UserResponse>();
44
+ Assert.NotNull(body);
45
+ Assert.NotNull(body!.Id);
46
+ }
47
+
48
+ [Theory]
49
+ [InlineData("{\"name\":\"No email\"}", "email")]
50
+ [InlineData("{\"email\":\"no-name@x.com\"}", "name")]
51
+ [InlineData("{\"email\":\"not-an-email\",\"name\":\"X\"}", "email")]
52
+ public async Task Validation_Returns422_AndMentionsField(string payload, string field)
53
+ {
54
+ var content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json");
55
+ var r = await Client(_tokenAdmin).PostAsync("/users", content);
56
+ Assert.Equal(HttpStatusCode.UnprocessableEntity, r.StatusCode);
57
+ var msg = await r.Content.ReadAsStringAsync();
58
+ Assert.Contains(field, msg.ToLower());
59
+ }
60
+
61
+ [Fact]
62
+ public async Task NoToken_Returns401()
63
+ {
64
+ var r = await Client().PostAsJsonAsync("/users", new CreateUserDto("x@y.com", "x"));
65
+ Assert.Equal(HttpStatusCode.Unauthorized, r.StatusCode);
66
+ }
67
+
68
+ [Fact]
69
+ public async Task CrossTenant_Returns403Or404()
70
+ {
71
+ if (string.IsNullOrEmpty(_tokenOtherOrg)) return;
72
+ // assume a known id from another tenant; in a real suite, create one in setup
73
+ var r = await Client(_tokenOtherOrg).GetAsync("/users/00000000-0000-0000-0000-000000000001");
74
+ Assert.True(r.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.NotFound);
75
+ }
76
+
77
+ [Fact]
78
+ public async Task Contract_HasRequiredFields_NoLeaks()
79
+ {
80
+ var create = await Client(_tokenAdmin).PostAsJsonAsync("/users",
81
+ new CreateUserDto($"contract-{Guid.NewGuid():N}@example.com", "Contract"));
82
+ var created = await create.Content.ReadFromJsonAsync<UserResponse>();
83
+ var get = await Client(_tokenAdmin).GetAsync($"/users/{created!.Id}");
84
+ Assert.Equal(HttpStatusCode.OK, get.StatusCode);
85
+
86
+ var raw = await get.Content.ReadAsStringAsync();
87
+ foreach (var field in new[] { "id", "email", "name", "created_at" })
88
+ Assert.Contains(field, raw, StringComparison.OrdinalIgnoreCase);
89
+ foreach (var leak in new[] { "password_hash", "internal_id", "_raw" })
90
+ Assert.DoesNotContain(leak, raw, StringComparison.OrdinalIgnoreCase);
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## Configuration
96
+
97
+ Project file (`*.QA.csproj` or extend the existing test project):
98
+
99
+ ```xml
100
+ <ItemGroup>
101
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.*" />
102
+ <PackageReference Include="xunit" Version="2.9.*" />
103
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.8.*" />
104
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.*" />
105
+ </ItemGroup>
106
+ <ItemGroup>
107
+ <InternalsVisibleTo Include="$(AssemblyName)" />
108
+ </ItemGroup>
109
+ ```
110
+
111
+ The `Program` class must be public (for `WebApplicationFactory<Program>`). For minimal APIs, add at the bottom of `Program.cs`:
112
+
113
+ ```csharp
114
+ public partial class Program { }
115
+ ```
116
+
117
+ ## Running
118
+
119
+ ```bash
120
+ # all RF tests
121
+ dotnet test --filter FullyQualifiedName~YourProject.QA.Api
122
+
123
+ # one RF
124
+ dotnet test --filter FullyQualifiedName~RF_XX_CreateUserTests
125
+
126
+ # log to QA/logs/api/
127
+ dotnet test --filter FullyQualifiedName~YourProject.QA.Api \
128
+ --logger "console;verbosity=detailed" 2>&1 \
129
+ | tee "QA/logs/api/run-$(date +%F).log"
130
+ ```
131
+
132
+ ## Logging request/response
133
+
134
+ Use a custom `DelegatingHandler` registered on the factory's client:
135
+
136
+ ```csharp
137
+ public class LoggingHandler : DelegatingHandler
138
+ {
139
+ private static readonly string LogPath = "QA/logs/api/RF-XX-create-user.log";
140
+
141
+ protected override async Task<HttpResponseMessage> SendAsync(
142
+ HttpRequestMessage req, CancellationToken ct)
143
+ {
144
+ var sw = Stopwatch.StartNew();
145
+ var res = await base.SendAsync(req, ct);
146
+ sw.Stop();
147
+ Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
148
+ var entry = new {
149
+ ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
150
+ method = req.Method.Method,
151
+ url = req.RequestUri?.ToString(),
152
+ status = (int)res.StatusCode,
153
+ ms = sw.ElapsedMilliseconds,
154
+ };
155
+ await File.AppendAllTextAsync(LogPath,
156
+ System.Text.Json.JsonSerializer.Serialize(entry) + "\n", ct);
157
+ return res;
158
+ }
159
+ }
160
+ ```
161
+
162
+ ## Pros / cons
163
+
164
+ - **Pro**: in-process — full DI graph, no port, deterministic.
165
+ - **Pro**: `[Theory]` + `[InlineData]` covers the 4xx matrix.
166
+ - **Pro**: same project as unit tests; `dotnet test` runs both.
167
+ - **Con**: requires `Program` to be partial and public.
168
+ - **Con**: tied to `Microsoft.AspNetCore.Mvc.Testing` package versions.
@@ -0,0 +1,130 @@
1
+ # Recipe: `.http` (REST Client) — DEFAULT
2
+
3
+ Universal API-testing format. One file per RF. Read by VSCode REST Client, JetBrains HTTP Client, Neovim rest.nvim/kulala, Zed Assistant. No test runner needed.
4
+
5
+ ## File shape
6
+
7
+ `{{PRD_PATH}}/QA/scripts/api/RF-XX-[slug].http`
8
+
9
+ ```http
10
+ ### RF-XX [slug] — happy path
11
+ # @name create_user
12
+ POST {{base}}/users
13
+ Authorization: Bearer {{token_admin}}
14
+ Content-Type: application/json
15
+
16
+ {
17
+ "email": "qa-{{$randomInt 1 999999}}@example.com",
18
+ "name": "QA User"
19
+ }
20
+
21
+ > {%
22
+ client.test("status is 201", () => client.assert(response.status === 201));
23
+ client.test("response has id", () => client.assert(response.body.id != null));
24
+ client.global.set("created_user_id", response.body.id);
25
+ %}
26
+
27
+ ### RF-XX — 4xx validation: missing email
28
+ POST {{base}}/users
29
+ Authorization: Bearer {{token_admin}}
30
+ Content-Type: application/json
31
+
32
+ { "name": "No email" }
33
+
34
+ > {%
35
+ client.test("status is 422", () => client.assert(response.status === 422));
36
+ client.test("error mentions email", () => client.assert(response.body.error.message.toLowerCase().includes("email")));
37
+ %}
38
+
39
+ ### RF-XX — 4xx auth: missing token
40
+ POST {{base}}/users
41
+ Content-Type: application/json
42
+
43
+ { "email": "x@y.com", "name": "x" }
44
+
45
+ > {%
46
+ client.test("status is 401", () => client.assert(response.status === 401));
47
+ %}
48
+
49
+ ### RF-XX — 4xx authz: cross-tenant access
50
+ GET {{base}}/users/{{created_user_id}}
51
+ Authorization: Bearer {{token_other_org_admin}}
52
+
53
+ > {%
54
+ client.test("status is 403 or 404", () =>
55
+ client.assert(response.status === 403 || response.status === 404));
56
+ %}
57
+
58
+ ### RF-XX — contract drift: response shape vs OpenAPI
59
+ GET {{base}}/users/{{created_user_id}}
60
+ Authorization: Bearer {{token_admin}}
61
+
62
+ > {%
63
+ client.test("has required fields", () => {
64
+ ["id", "email", "name", "created_at"].forEach(f =>
65
+ client.assert(response.body[f] != null, `missing ${f}`));
66
+ });
67
+ client.test("no leaked internal fields", () => {
68
+ ["password_hash", "internal_id", "_raw"].forEach(f =>
69
+ client.assert(response.body[f] === undefined, `leaked ${f}`));
70
+ });
71
+ %}
72
+ ```
73
+
74
+ ## Variables
75
+
76
+ Set once at the top of the file (or in a `http-client.env.json` next to it):
77
+
78
+ ```http
79
+ @base = {{$dotenv API_BASE_URL}}
80
+ @token_admin = {{$dotenv QA_TOKEN_ADMIN}}
81
+ @token_user = {{$dotenv QA_TOKEN_USER}}
82
+ @token_other_org_admin = {{$dotenv QA_TOKEN_OTHER_ORG}}
83
+ ```
84
+
85
+ Or, if the project uses login-based auth, capture the token in a setup request and reference it in subsequent requests:
86
+
87
+ ```http
88
+ ### Setup — login as admin
89
+ # @name login_admin
90
+ POST {{base}}/auth/login
91
+ Content-Type: application/json
92
+
93
+ { "email": "{{$dotenv QA_ADMIN_EMAIL}}", "password": "{{$dotenv QA_ADMIN_PASSWORD}}" }
94
+
95
+ > {% client.global.set("token_admin", response.body.access_token); %}
96
+ ```
97
+
98
+ ## Execution from `dw-run-qa` (CLI fallback)
99
+
100
+ When running outside an IDE (e.g., from the agent in headless mode), parse and execute via `curl`:
101
+
102
+ ```bash
103
+ # For each ### block, extract method/url/headers/body and execute:
104
+ curl -sS -X POST "$BASE/users" \
105
+ -H "Authorization: Bearer $TOKEN_ADMIN" \
106
+ -H "Content-Type: application/json" \
107
+ -d '{"email":"qa-1@example.com","name":"QA"}' \
108
+ -w '\n%{http_code} %{time_total}s\n' \
109
+ | tee -a "QA/logs/api/RF-XX-create-user.log"
110
+ ```
111
+
112
+ The `dw-run-qa` agent does this loop automatically and writes to the JSONL log per `references/log-conventions.md`.
113
+
114
+ ## Assertions
115
+
116
+ Use the inline `> {% ... %}` post-response handler when running in an IDE. For headless `curl` execution, use `jq`:
117
+
118
+ ```bash
119
+ RESP=$(curl -sS ...)
120
+ STATUS=$(echo "$RESP" | head -1 | awk '{print $2}')
121
+ [ "$STATUS" = "201" ] || { echo "FAIL: expected 201, got $STATUS"; exit 1; }
122
+ echo "$RESP" | jq -e '.id != null' >/dev/null || { echo "FAIL: missing id"; exit 1; }
123
+ ```
124
+
125
+ ## Pros / cons
126
+
127
+ - **Pro**: zero install, opens in any IDE, devs read it without running a test runner.
128
+ - **Pro**: each request is a single block, easy to copy-paste into incident tickets.
129
+ - **Con**: no native fixture/teardown — multi-request flows rely on `client.global.set` for state.
130
+ - **Con**: parallel execution requires per-block uniqueness in resource names (use `{{$randomInt}}` or `{{$timestamp}}`).
@@ -0,0 +1,157 @@
1
+ # Recipe: `pytest + httpx` (Python)
2
+
3
+ Use when the project already runs `pytest` (FastAPI, Starlette, Flask + pytest-flask). The async client matches FastAPI's design and gives you fixtures + parametrize for free.
4
+
5
+ ## File shape
6
+
7
+ `{{PRD_PATH}}/QA/scripts/api/test_RF_XX_[slug].py`
8
+
9
+ ```python
10
+ """RF-XX [slug] — API QA suite."""
11
+ import os
12
+ import pytest
13
+ import httpx
14
+
15
+ BASE = os.environ["API_BASE_URL"]
16
+ TOKEN_ADMIN = os.environ["QA_TOKEN_ADMIN"]
17
+ TOKEN_USER = os.environ["QA_TOKEN_USER"]
18
+ TOKEN_OTHER_ORG = os.environ.get("QA_TOKEN_OTHER_ORG", "")
19
+
20
+
21
+ @pytest.fixture
22
+ async def client():
23
+ async with httpx.AsyncClient(base_url=BASE, timeout=10.0) as c:
24
+ yield c
25
+
26
+
27
+ def auth(token: str) -> dict:
28
+ return {"Authorization": f"Bearer {token}"}
29
+
30
+
31
+ # ---------- happy path ----------
32
+
33
+ @pytest.mark.asyncio
34
+ async def test_create_user_happy_path(client):
35
+ r = await client.post("/users", headers=auth(TOKEN_ADMIN),
36
+ json={"email": "qa@example.com", "name": "QA"})
37
+ assert r.status_code == 201, r.text
38
+ body = r.json()
39
+ assert body["id"]
40
+ assert body["email"] == "qa@example.com"
41
+ pytest.created_user_id = body["id"] # share via module attr or use a fixture
42
+
43
+
44
+ # ---------- 4xx validation ----------
45
+
46
+ @pytest.mark.asyncio
47
+ @pytest.mark.parametrize("payload, missing_field", [
48
+ ({"name": "No email"}, "email"),
49
+ ({"email": "no-name@x.com"}, "name"),
50
+ ({"email": "not-an-email", "name": "X"}, "email"),
51
+ ])
52
+ async def test_create_user_validation(client, payload, missing_field):
53
+ r = await client.post("/users", headers=auth(TOKEN_ADMIN), json=payload)
54
+ assert r.status_code == 422, r.text
55
+ assert missing_field in r.json()["error"]["message"].lower()
56
+
57
+
58
+ # ---------- 4xx auth ----------
59
+
60
+ @pytest.mark.asyncio
61
+ async def test_create_user_no_token(client):
62
+ r = await client.post("/users", json={"email": "x@y.com", "name": "x"})
63
+ assert r.status_code == 401
64
+
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_create_user_expired_token(client):
68
+ r = await client.post("/users",
69
+ headers={"Authorization": "Bearer expired.token.here"},
70
+ json={"email": "x@y.com", "name": "x"})
71
+ assert r.status_code == 401
72
+
73
+
74
+ # ---------- 4xx authz cross-tenant ----------
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_get_user_other_org_denied(client):
78
+ if not TOKEN_OTHER_ORG:
79
+ pytest.skip("QA_TOKEN_OTHER_ORG not set")
80
+ r = await client.get(f"/users/{pytest.created_user_id}",
81
+ headers=auth(TOKEN_OTHER_ORG))
82
+ assert r.status_code in (403, 404)
83
+
84
+
85
+ # ---------- contract drift ----------
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_get_user_contract(client):
89
+ r = await client.get(f"/users/{pytest.created_user_id}",
90
+ headers=auth(TOKEN_ADMIN))
91
+ assert r.status_code == 200
92
+ body = r.json()
93
+ for field in ("id", "email", "name", "created_at"):
94
+ assert field in body, f"missing {field}"
95
+ for leaked in ("password_hash", "internal_id", "_raw"):
96
+ assert leaked not in body, f"leaked {leaked}"
97
+ ```
98
+
99
+ ## Configuration
100
+
101
+ `pyproject.toml` (or `pytest.ini`):
102
+
103
+ ```toml
104
+ [tool.pytest.ini_options]
105
+ asyncio_mode = "auto"
106
+ testpaths = ["QA/scripts/api"]
107
+ ```
108
+
109
+ ## Running
110
+
111
+ ```bash
112
+ # all RF tests
113
+ pytest QA/scripts/api/
114
+
115
+ # one RF
116
+ pytest QA/scripts/api/test_RF_01_create_user.py -v
117
+
118
+ # capture log to QA/logs/api/
119
+ pytest QA/scripts/api/ -v --tb=short 2>&1 | tee QA/logs/api/run-$(date +%F).log
120
+ ```
121
+
122
+ ## Logging request/response (for QA evidence)
123
+
124
+ Wrap `client` to log every call:
125
+
126
+ ```python
127
+ import json, time
128
+ from pathlib import Path
129
+
130
+ LOG = Path("QA/logs/api/RF-XX-create-user.log")
131
+
132
+ class LoggingClient(httpx.AsyncClient):
133
+ async def request(self, method, url, **kw):
134
+ start = time.time()
135
+ r = await super().request(method, url, **kw)
136
+ ms = int((time.time() - start) * 1000)
137
+ LOG.parent.mkdir(parents=True, exist_ok=True)
138
+ with LOG.open("a") as f:
139
+ f.write(json.dumps({
140
+ "ts": time.time(),
141
+ "method": method,
142
+ "url": str(r.request.url),
143
+ "status": r.status_code,
144
+ "ms": ms,
145
+ "request_body": kw.get("json"),
146
+ "response_body": r.json() if r.headers.get("content-type", "").startswith("application/json") else None,
147
+ }) + "\n")
148
+ return r
149
+ ```
150
+
151
+ ## Pros / cons
152
+
153
+ - **Pro**: parametrize covers the 4xx matrix in one block.
154
+ - **Pro**: fixtures handle auth + setup/teardown cleanly.
155
+ - **Pro**: integrates with existing `pytest` config + CI.
156
+ - **Con**: not portable to non-Python projects.
157
+ - **Con**: requires `httpx` and `pytest-asyncio` in dev deps.