Files
redefined-designs/scripts/run-tests.ps1
T
bermudalambandClaude Opus 5 4e2357356b
Linting / lint (pull_request) Successful in 7m20s
SonarQube Analysis / sonarqube (pull_request) Successful in 32m55s
fix(scripts): make the database the e2e suite reads a recorded fact (#273)
The end-to-end helper opens its own connection to read a password-reset token, and nothing guaranteed it pointed at the database the application was actually using. With -E2eDb the app runs on redefined_e2e at 55501 while the helper kept its redefined_local default at 55500, so the app wrote to one database and the suite read another — and the specs failed for a reason that had nothing to do with them.

start-local.ps1 now records the coordinates it chose in .local/database.json, and run-tests.ps1 reads them into TEST_PGHOST, TEST_PGPORT, TEST_PGUSER, TEST_PGPASSWORD and TEST_PGDATABASE before Playwright starts. The answer now comes from one place, written by the thing that made the decision at the moment it made it.

It is written after Start-Database rather than before, so the file never names a database that failed to come up, and removed by -Stop, so a stopped stack does not leave a record pointing at a container that is gone.

Setting all five closes the second fault in the same change. Invoke-IntegrationSuite sets TEST_PGPORT and PowerShell keeps it for the rest of the session, so a -Suite all run leaked the integration port into the e2e run that followed — with none of the matching credentials, leaving the helper offering redefined_local's password to the integration database. Overwriting every one of them is what makes that leak harmless.

A missing record throws rather than falling back. A default is what produced both faults in the first place: always plausible, silently wrong, and it fails in ways that look like application bugs rather than configuration.

The guard test is the point of the change as much as the fix is. This is the third instance today of two files having to agree with nothing comparing them — #107 and #118 were envValidation against a compose file, #287 was the workflow against start-local.ps1, and this is three files rather than two. The test pins the whole chain: that the writer records the five settings, that the runner reads each of them from the record rather than a default, and that the helper reads no connection variable the runner does not set. Removing a single line from the runner fails four of its assertions, which was checked rather than assumed.

What it cannot do is run PowerShell, so these are text assertions against the scripts. That is weaker than executing them and still catches the drift that actually happened.

Closes #273

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 16:57:53 -05:00

254 lines
9.6 KiB
PowerShell

#Requires -Version 7
<#
.SYNOPSIS
Runs the project's test suites.
.DESCRIPTION
One script rather than three, because the Node version switch, the test
database bring-up and the TEST_PGPORT handling are shared by more than one
suite and would otherwise be copied around and drift apart.
Switches Node to the pinned version in scripts/NodeVersion.ps1 for the run and puts the
machine default back afterwards, the same way start-local.ps1 does — and for
the same reason, since ts-jest and Playwright are subject to the same Node
20 floor as the migrations.
.PARAMETER Suite
unit Backend Jest unit tests. Needs nothing running.
integration Backend Jest integration tests against a disposable Postgres.
Brings the container up itself.
e2e Frontend Playwright tests. Needs the app stack up; start it
with .\scripts\start-local.ps1 -E2eDb first.
-E2eDb matters. Without it the suite runs against the
development database, which nothing truncates: every run seeds
more fixtures and leaves them, the unfiltered storefront grows
monotonically, and eventually rendering it outruns the
assertions' timeout. It then fails locally, passes in CI where
the database is fresh, and gets worse (#186). With it, the
stack runs against a throwaway database that starts empty.
all All three, in that order — cheapest and most isolated first,
so a failure that a later suite would also show up in is
reported by the suite that localises it best.
.PARAMETER TestDbPort
Host port for the integration suite's Postgres. Change it if something else
holds the default, or if Hyper-V has reserved it.
.PARAMETER KeepTestDb
Leave the integration database running afterwards. Useful when re-running
the same suite repeatedly; it starts faster.
.PARAMETER Filter
Passed through to the test runner to select tests by file or name.
.EXAMPLE
.\scripts\run-tests.ps1 -Suite unit
.\scripts\run-tests.ps1 -Suite integration -TestDbPort 55600
.\scripts\run-tests.ps1 -Suite e2e -Filter email-templates
.\scripts\run-tests.ps1 -Suite all
#>
[CmdletBinding()]
param(
[ValidateSet('unit', 'integration', 'e2e', 'all')]
[string]$Suite = 'all',
[int]$TestDbPort = 55432,
[switch]$KeepTestDb,
[string]$Filter,
# Resolved after NodeVersion.ps1 is dot-sourced below, not here. A param
# block runs before anything else in the script, so $script:DEFAULT_NODE_VERSION
# is still $null at this point and using it as the default would silently
# restore nothing — leaving the machine on the pinned version, which is the
# exact failure the restore exists to prevent. See #208.
[string]$DefaultNodeVersion = ''
)
$ErrorActionPreference = 'Stop'
$RepoRoot = Split-Path -Parent $PSScriptRoot
$Backend = Join-Path $RepoRoot 'backend'
$Frontend = Join-Path $RepoRoot 'frontend'
$WebPort = 5173
function Write-Step { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan }
function Write-Note { param([string]$Message) Write-Host " $Message" -ForegroundColor DarkGray }
function Write-Good { param([string]$Message) Write-Host " $Message" -ForegroundColor Green }
# $ErrorActionPreference = 'Stop' does not stop the script when a native
# executable exits non-zero, only when a cmdlet throws — and every runner here
# is npm. Without this a failing suite prints red and the script reports success.
function Invoke-Checked {
param([scriptblock]$Command, [string]$What)
& $Command
if ($LASTEXITCODE -ne 0) {
throw "$What failed (exit code $LASTEXITCODE)."
}
}
. (Join-Path $PSScriptRoot 'NodeVersion.ps1')
# The one home for this value is NodeVersion.ps1, beside NODE_VERSION. It cannot
# be a param default (see the note there), so it is filled in here instead, and
# an explicit -DefaultNodeVersion still wins.
if (-not $DefaultNodeVersion) { $DefaultNodeVersion = $script:DEFAULT_NODE_VERSION }
# Bound once so the shared switcher reports in this script's output style.
$NodeOut = @{ Step = ${function:Write-Step}; Note = ${function:Write-Note} }
function Assert-Docker {
docker info *>$null
if ($LASTEXITCODE -ne 0) {
throw 'Docker is not running. Start Docker Desktop and try again.'
}
}
function Invoke-UnitSuite {
Write-Step 'Backend unit tests'
Push-Location $Backend
try {
if ($Filter) { Invoke-Checked { npm run test:unit -- $Filter } 'Unit tests' }
else { Invoke-Checked { npm run test:unit } 'Unit tests' }
}
finally { Pop-Location }
}
function Invoke-IntegrationSuite {
Assert-Docker
# The suite's globalSetup connects on TEST_PGPORT and fails with a clear
# message if nothing answers, so the container has to be up first. Reusing a
# running one is safe: every test truncates in beforeEach.
Write-Step "Starting the test database on port $TestDbPort"
$env:TEST_PGPORT = "$TestDbPort"
Push-Location $Backend
try {
Invoke-Checked { npm run db:test:up } 'Test database'
Write-Step 'Backend integration tests'
try {
if ($Filter) { Invoke-Checked { npm run test:integration -- $Filter } 'Integration tests' }
else { Invoke-Checked { npm run test:integration } 'Integration tests' }
}
finally {
if ($KeepTestDb) {
Write-Note 'leaving the test database up (-KeepTestDb)'
}
else {
# In a finally so a failing suite still tidies up. The data is on
# tmpfs, so there is nothing to preserve for a post-mortem.
Write-Step 'Stopping the test database'
npm run db:test:down *>$null
}
}
}
finally { Pop-Location }
}
<#
.SYNOPSIS
Points the end-to-end helper at the database the running stack actually uses.
.DESCRIPTION
Closes both halves of #273.
The helper in frontend/tests/e2e/support/db.ts opens its own connection to read
a password-reset token, and it defaults to redefined_local on 55500. With
start-local.ps1 -E2eDb the application is on redefined_e2e on 55501 instead, so
the app wrote to one database and the suite read another.
Separately, Invoke-IntegrationSuite sets TEST_PGPORT to the integration
database and PowerShell keeps it for the rest of the session, so a -Suite all
run leaked that port into the e2e run that followed — with none of the matching
credentials, which left the helper offering redefined_local's password to the
integration database. Setting all five here overrides that leak as well, which
is why one change closes both faults.
Throws rather than falling back to a default. A default is what produced both
faults: it is always plausible and silently wrong, and a suite that reads the
wrong database fails in ways that look like application bugs.
#>
function Set-E2eDatabaseEnv {
$databaseFile = Join-Path $RepoRoot '.local/database.json'
if (-not (Test-Path $databaseFile)) {
throw @"
No .local/database.json, so there is no way to know which database the stack is
using — and guessing is what #273 was about.
It is written by start-local.ps1 when it brings the database up. Start the stack
first:
.\scripts\start-local.ps1 (development database)
.\scripts\start-local.ps1 -E2eDb (throwaway end-to-end database)
"@
}
$db = Get-Content $databaseFile -Raw | ConvertFrom-Json
$env:TEST_PGHOST = $db.host
$env:TEST_PGPORT = "$($db.port)"
$env:TEST_PGUSER = $db.user
$env:TEST_PGPASSWORD = $db.password
$env:TEST_PGDATABASE = $db.database
Write-Good "end-to-end helper pointed at $($db.database) on $($db.port)"
}
function Invoke-E2eSuite {
# Playwright's own webServer starts the dev server, but the backend it talks
# to is not its job. Checked here rather than left to twenty-five specs
# failing on a connection refused, which does not say what is missing.
$backendUp = $false
try {
$null = Invoke-WebRequest 'http://localhost:3000/api/config' -TimeoutSec 2 -UseBasicParsing
$backendUp = $true
}
catch { $backendUp = $false }
if (-not $backendUp) {
throw @"
No backend answering on http://localhost:3000/api/config.
The end-to-end suite drives real registration, cart and checkout flows against a
running stack. Start it first:
.\scripts\start-local.ps1
"@
}
Write-Good 'backend is up'
# Before Playwright starts, so every spec inherits it — and after the
# backend check, so a stack that is not running is reported as that rather
# than as a missing state file.
Set-E2eDatabaseEnv
Write-Note "dev server on $WebPort is started by Playwright if it is not already running"
Write-Step 'Frontend end-to-end tests'
Push-Location $Frontend
try {
if ($Filter) { Invoke-Checked { npx playwright test $Filter --project=chromium } 'End-to-end tests' }
else { Invoke-Checked { npx playwright test --project=chromium } 'End-to-end tests' }
}
finally { Pop-Location }
}
Use-PinnedNode @NodeOut
try {
switch ($Suite) {
'unit' { Invoke-UnitSuite }
'integration' { Invoke-IntegrationSuite }
'e2e' { Invoke-E2eSuite }
'all' {
# Cheapest and most isolated first, so a break that several suites
# would show is reported by the one that localises it best.
Invoke-UnitSuite
Invoke-IntegrationSuite
Invoke-E2eSuite
}
}
Write-Host ''
Write-Host "Passed ($Suite)." -ForegroundColor Green
}
finally {
Restore-Node -Version $DefaultNodeVersion @NodeOut
}