Existing coverage was one integration test and zero e2e tests. Adds the thin, high-value integration/e2e layer unit tests can't reach (real Postgres wire contract, real browser flows) and wires it as a post-deploy smoke gate. - Seed a deterministic dev/QA admin user and fixed Development env key so HTTP-only tests can authenticate without per-run registration. - Expand the API integration suite: disabled-flag, unauthorized access, environment-document bootstrap, identity-override precedence, and auth login scenarios, reusing the existing black-box HTTP+Npgsql harness. - Add a Playwright suite for the admin SPA: login, nav, project/environment context selection, and feature create/toggle, against the real API. - Bind QA Postgres to 127.0.0.1 for direct seeding, add smoke-qa.sh and an ensure_node() bootstrap (the qa runner has no Node today), and wire a new smoke-qa CI job after deploy-qa.
10 KiB
Plan: Integration + Playwright smoke suite that doubles as post-deploy QA validation
Context
Today the repo has one integration test (FlagEnabledTests — seeds an enabled flag in Postgres, hits GET /api/v1/flags, asserts enabled) and zero browser/e2e tests. Unit coverage is already deep for the risky logic — flag/feature evaluation, segment evaluation (28 tests), webhooks, auth token/API-key/permission logic, audit. Per the 80–90% unit / 10–20% integration rule, we do not re-test that logic at integration. Instead we add a thin layer covering the most important, most visible, real-Postgres-backed flows that unit tests (all InMemory EF) cannot prove, and wire it to run after deploy-qa so every QA deploy is validated end-to-end.
Decisions locked with the user:
- E2E hits the real deployed stack (not mocked) —
http://localhost:${QA_ADMIN_PORT}in CI,http://localhost:5173local dev. - Add a fixed dev/QA seed admin user (QA DB is wiped
down -veach deploy and reseeded on startup; seeding is alreadyIsDevelopment()-guarded and QA runsASPNETCORE_ENVIRONMENT=Development). - Add a post-deploy
smoke-qaCI job running the API integration suite + Playwright against QA.
Key constraints discovered:
- Integration harness is black-box: HTTP to a running API + direct Npgsql seed/cleanup (
Common/TestDatabase.cs). It spins nothing up; needs a live API and direct DB reachability. - QA compose (
deploy/qa/docker-compose.qa.yml) publishes only the admin port; API + Postgres are network-internal. The smoke job runs on the same self-hostedqahost (localhost). So the DB port must be reachable from the host for direct seeding → publish Postgres bound to127.0.0.1on the QA host. /health+/alive+ Scalar/OpenAPI are Development-only (ServiceDefaults/Extensions.cs:113). QA is Development so/healthworks there, but smoke assertions should lean on real auth/flag reads, not/health.- Seeder (
Data/DatabaseSeeder.cs) currently creates Org → Project → 3 Environments with randomenv-{guid}API keys and no user. For HTTP-only smoke we make the admin user + one env key deterministic.
Implementation — 4 independently buildable/committable chunks
Chunk A — Deterministic dev/QA seed admin user + fixed env key
Files: src/api/MicCheck.Api/Data/DatabaseSeeder.cs, unit test in tests/api/MicCheck.Api.Tests.Unit/Data/.
- Inject
IPasswordHasher<User>intoDatabaseSeeder(mirrorAuthService.RegisterAsyncatCommon/Security/Authorization/AuthService.cs:54-82: createUser{ IsActive=true, PasswordHash=hasher.HashPassword(...) }, thenOrganizationUser{ Role=OrganizationRole.Admin }linking it to the seededDefaultorg).- Creds:
admin@miccheck.local/MicCheckQa!2026(login verify has no complexity rule, so any value works; keep it in one shared constants spot referenced by tests).
- Creds:
- Give the seeded Development environment a deterministic
ApiKey(e.g.env-qa-development) instead of a random guid; leave Staging/Production random. Lets HTTP-only smoke read/flagswith a known key without direct DB access. - Keep the existing
if (Organizations.AnyAsync) return;idempotency guard — user + env are created in the same first-run block. No prod risk: invocation is already gated byapp.Environment.IsDevelopment()(Program.cs:160-166). - Unit test: seed against InMemory EF twice → asserts admin user exists once, is Admin on Default org, password verifies, Development env key is the fixed value.
Chunk B — Expand API integration suite (tests/api/MicCheck.Api.Tests.Integration)
Reuse the existing harness verbatim: Common/FlagApiHttpClient.cs (drives ###-delimited .http files with {{var}} substitution), Common/TestDatabase.cs (Npgsql seed/cleanup + SnapshotRowsAsync for failure dumps), Common/IntegrationTestSettings.cs (env-var / .runsettings config), and the FlagSeed INSERT…RETURNING + cascade-delete-by-Organization pattern (Flags/FlagSeed.cs). Keep DTOs redefined locally (no ProjectReference to the API) per the current convention. New .http files under HttpFiles/ are auto-copied (csproj CopyToOutputDirectory=PreserveNewest).
Add these high-value scenarios (each the real Postgres wire contract, not re-covered logic):
- Flag disabled — seed
Enabled=false; assert/flagsreports the featureEnabled=false(complements the existing enabled test; cheap variant ofFlagSeed). - Environment-document bootstrap — new
HttpFiles/EnvironmentDocument.http(GET /api/v1/environment-document,X-Environment-Key); assert 200 and the seeded feature state + project segment are present (shape:Environments/EnvironmentDocumentResponse.cs). This is the edge/client-SDK bootstrap path — high blast radius, untested. - Identity override precedence — new seed adding an identity-override FeatureState (and/or a segment override) +
HttpFiles/Identity.http(POST /api/v1/identitywith identifier,X-Environment-Key); assert the evaluated value reflects identity > segment > env-default precedence (Features/FeatureEvaluationService.cs:43-146). Richest runtime logic against real data. - Auth liveness — new
HttpFiles/Auth.http(POST /api/v1/auth/login): bad creds → 401; the deterministic seed creds → 200 + tokens. Proves app + DB + auth are up without depending on/health. - Unauthorized guard —
GET /api/v1/flagswith no / wrongX-Environment-Key→ 401 (validatesEnvironmentKeyAuthenticationHandler).
Chunk C — Playwright admin e2e suite (new)
Location src/admin/e2e/ (distinct from Jest, which grabs *.spec.ts under tests/; name specs *.e2e.ts and set Jest roots/Playwright testDir so they never collide). Add dev deps @playwright/test, a playwright.config.ts (testDir: 'e2e', baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:5173', chromium project, globalSetup for auth). Add package.json scripts: "e2e": "playwright test", "e2e:install": "playwright install --with-deps chromium".
The app is already richly instrumented with data-testid — drive real UI, no selectors guesswork. Auth: globalSetup logs in once via the real /login form (login-form/email-input/password-input/login-submit) with the Chunk-A seed creds and saves storageState for reuse.
Journeys (ordered most→least important; the seed provides Default org, My Project, and Development/Staging/Production envs, so context is selectable without creating anything):
- Login — valid creds redirect off
/login; invalid creds surfacelogin-error(views/LoginView.vue). - App shell + nav — sidebar renders and routes (Dashboard/Features/Segments/Identities/Audit Logs/Settings) navigate (
layouts/components/NavItems.vue). - Select project + environment context —
project-selector/project-select+environment-tab-bar/env-select(components/nav/ProjectSelector.vue,EnvironmentTabBar.vue); required before feature screens work (context persists to localStorage viastores/context.ts). - View feature flags —
features-tablerenders for the selected project (views/FeaturesView.vue). - Create a feature flag —
create-feature-btn→FeatureDialog(feature-name-input,feature-type-select,feature-dialog-save) → row appears (components/features/FeatureDialog.vue, APIapi/features.ts). Exercises full write path to real DB. - Toggle a feature flag — row switch
toggle-${id}(orfeature-enabled-toggleinFeatureDetail.vue); reload → state persisted (validatesapi/featureStates.ts→ real Postgres).
Secondary/optional (add if cheap): create project, create environment via the sidebar dialogs.
Chunk D — CI post-deploy smoke job
Files: deploy/qa/docker-compose.qa.yml, scripts/ci/smoke-qa.sh (new), .github/workflows/ci.yml.
- DB reachability: add
ports: ["127.0.0.1:55432:5432"]to thedbservice in the QA compose (localhost-bound only — the runner host is the sole consumer; not exposed off-box). Lets the integration harness seed Postgres directly. - New
scripts/ci/smoke-qa.sh(matches the repo's "every CI step is a reproducible script" convention):- API integration:
dotnet test tests/api/MicCheck.Api.Tests.Integration -c Release --logger trxwith envMICCHECK_API_BASE_URL=http://localhost:${QA_ADMIN_PORT}andMICCHECK_DB_CONNECTION_STRING=Host=localhost;Port=55432;Database=miccheck;Username=miccheck;Password=password. - Playwright:
npm --prefix src/admin ci(or reuse install),npm --prefix src/admin run e2e:install, thenE2E_BASE_URL=http://localhost:${QA_ADMIN_PORT} npm --prefix src/admin run e2e.
- API integration:
.github/workflows/ci.yml: add jobsmoke-qa(needs: deploy-qa,runs-on: [self-hosted, qa], envQA_ADMIN_PORT: ${{ vars.QA_ADMIN_PORT }}) that checks out (plain-git step, matchingdeploy-qa) and runs./scripts/ci/smoke-qa.sh. Deploy is now gated by real end-to-end validation.
Files touched (summary)
- Modify:
src/api/MicCheck.Api/Data/DatabaseSeeder.cs,deploy/qa/docker-compose.qa.yml,.github/workflows/ci.yml,src/admin/package.json. - Add (API tests):
HttpFiles/EnvironmentDocument.http,HttpFiles/Identity.http,HttpFiles/Auth.http, new*Tests.cs+ seed helpers underFlags/(or a newIdentities/,Environments/,Auth/folder) intests/api/MicCheck.Api.Tests.Integration. - Add (unit): seeder test under
tests/api/MicCheck.Api.Tests.Unit/Data/. - Add (e2e):
src/admin/playwright.config.ts,src/admin/e2e/*.e2e.ts,src/admin/e2e/global-setup.ts. - Add (CI):
scripts/ci/smoke-qa.sh. - Per CLAUDE.md, also drop this plan at
docs/plans/api/integration-smoke_plan.mdand an_output.mdsummary after implementation.
Verification
- Chunk A:
dotnet test tests/api/MicCheck.Api.Tests.Unit(new seeder test green). Boot API locally (docker compose up), confirm login with seed creds returns tokens and the Development env key equals the fixed value. - Chunk B: bring up local stack (Postgres on
:5432, API on:5000),dotnet test tests/api/MicCheck.Api.Tests.Integration --settings local.runsettings— all new scenarios green; failure messages show live DB snapshots. - Chunk C:
npm --prefix src/admin run serve(API on :5000),E2E_BASE_URL=http://localhost:5173 npm --prefix src/admin run e2e— all journeys pass headed and headless. - Chunk D: push to
build-runner-fix; watch CI —smoke-qaruns afterdeploy-qa, bothdotnet testand Playwright green againsthttp://localhost:${QA_ADMIN_PORT}. Confirm a deliberately-broken deploy (e.g. bad DB creds) makessmoke-qafail.