[AZ-456] Test infrastructure: Vitest + MSW + Playwright + scripts

Scaffolds the Blackbox test project per AZ-456 / environment.md across
the three profiles:

- fast  : Vitest 3.x + jsdom + MSW 2.x + RTL/jest-dom; tests/setup.ts
          boots the MSW Node server with onUnhandledRequest:'error',
          afterEach resets handlers, clears bearer + navigate-to-login
          spy. Default handlers ship for every suite service plus OWM
          and tile stand-ins. Fixtures mirror seed_* in test-data.md.
- e2e   : Playwright ^1.49 with chromium + firefox projects against the
          suite docker-compose stack; owm-stub + tile-stub Bun servers,
          playwright-runner image, seeds.sql for the test-db.
- static: scripts/run-tests.sh extended — tsc --noEmit (test config),
          vite build, ripgrep checks (with grep -r fallback), CSV
          report at test-output/static-report.csv per AC-7 columns.

Smoke tests cover AC-3, AC-4 (fast, 5 tests, PASS) and AC-1, AC-2,
AC-5, AC-8 (e2e, gated by Risk 4 docker availability). Static profile
(13 checks) PASS — STC-SEC1 (no literal OWM key) lifted from
QUARANTINE per AZ-447 with a narrowed pattern.

Files:
  +24 tests/**, +10 e2e/**, +vitest.config.ts, +tsconfig.test.json
  ~package.json (test scripts + devDeps for vitest, @testing-library/*,
   msw, @playwright/test, jsdom, @types/node, @vitest/coverage-v8)
  ~scripts/run-tests.sh, scripts/run-performance-tests.sh — switched
   RESULTS_DIR to test-output/, compose path to project-local
  ~.gitignore — added /test-output/

Verification:
  bun run test:fast        → 11 / 11 PASS
  ./scripts/run-tests.sh   → static 13/13 + fast 11/11 PASS, exit 0

Tracker: AZ-456 → In Testing.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 02:57:04 +03:00
parent e5d9276b19
commit 38eb87fb08
45 changed files with 2377 additions and 157 deletions
+79
View File
@@ -0,0 +1,79 @@
import { test, expect } from '@playwright/test'
// Smoke tests for AZ-456 e2e infrastructure (AC-1, AC-2, AC-5, AC-8).
// Every other test file under e2e/tests/ is owned by AZ-457..AZ-482; those
// tasks add the production-shaped assertions per group. This file MUST stay
// minimal so any flake here is unambiguously an infrastructure regression.
const EXTERNAL_HOSTS = [
/api\.openweathermap\.org/,
/unpkg\.com/,
/\.tile\.openstreetmap\.org$/,
/^tile\.openstreetmap\.org$/,
]
test.describe('AZ-456 e2e infrastructure', () => {
test.beforeEach(async ({ context }, testInfo) => {
// AC-8: external-host firewall. The compose network is already isolated
// from the internet; this route guard catches code paths that try to
// reach an external host directly (and lets resilience tests flip it).
const externalHits: string[] = []
await context.route(/.*/, async (route) => {
const url = route.request().url()
if (EXTERNAL_HOSTS.some((re) => re.test(new URL(url).hostname))) {
externalHits.push(url)
await route.abort()
return
}
await route.continue()
})
testInfo.attachments.push({ name: 'externalHits', contentType: 'application/json', body: Buffer.from('[]') })
;(testInfo as unknown as { __externalHits: string[] }).__externalHits = externalHits
})
test.afterEach(async ({}, testInfo) => {
const externalHits = (testInfo as unknown as { __externalHits?: string[] }).__externalHits ?? []
expect(externalHits, 'leaked external requests detected').toEqual([])
})
test('AC-1: SPA HTML is served from azaion-ui', async ({ page }) => {
const response = await page.goto('/')
expect(response?.ok()).toBeTruthy()
const html = await page.content()
expect(html).toMatch(/<html[\s>]/i)
})
test('AC-2: owm-stub returns the canned wind shape', async ({ request }) => {
const res = await request.get('http://owm-stub:8081/data/2.5/weather?lat=0&lon=0&appid=test')
expect(res.status()).toBe(200)
const body = await res.json()
expect(body.wind).toEqual({ speed: 5.0, deg: 270 })
})
test('AC-2: tile-stub returns a 256x256 PNG', async ({ request }) => {
const res = await request.get('http://tile-stub:8082/1/0/0.png')
expect(res.status()).toBe(200)
expect(res.headers()['content-type']).toBe('image/png')
const body = await res.body()
expect(body.subarray(0, 8)).toEqual(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
})
test('AC-5: Playwright runs under the configured browser project', async ({ browserName }) => {
expect(['chromium', 'firefox']).toContain(browserName)
})
test('AC-8: external host is blocked by the route guard', async ({ page }) => {
const externalHits = (test.info() as unknown as { __externalHits?: string[] }).__externalHits ?? []
const beforeCount = externalHits.length
await page.goto('/')
await page.evaluate(() =>
fetch('https://api.openweathermap.org/data/2.5/weather?appid=leak').catch(() => null),
)
// The guard converts the request into an abort, so the leak is recorded
// but no real request escapes. The afterEach assertion will fire next.
expect(externalHits.length).toBeGreaterThan(beforeCount)
// Reset so afterEach doesn't fail this specific test (the guard already
// proved the assertion).
externalHits.length = 0
})
})