also looking at this
test: raise instead of skipping when a provided live service is missing
#1187
4 files
+194
-9
| @@ -0,0 +1,89 @@ | ||
| 1 | /** | |
| 2 | * The live-service gate raises where the service is provided, skips where it | |
| 3 | * is optional. | |
| 4 | * | |
| 5 | * `probePostgres()` used to answer `false` for every possible failure, so the | |
| 6 | * 14 files gated on it vanished into "skip" whenever Postgres was unreachable | |
| 7 | * for any reason — and a run with all of them gone still exits 0. That is how | |
| 8 | * this repository once shipped a release whose two most severe guards never | |
| 9 | * executed: the probe built its URL from `process.env.USER`, auth failed on | |
| 10 | * every runner, and CI was green throughout. Fixing the URL fixed that cause. | |
| 11 | * This pins the mechanism. | |
| 12 | * | |
| 13 | * Both directions are controlled, because only the pair is evidence: a gate | |
| 14 | * that always raises would pass the first case here while making the suite | |
| 15 | * unrunnable for anyone without Postgres, which is the behaviour the skip | |
| 16 | * exists to provide. | |
| 17 | * | |
| 18 | * Each case runs the real gate in a child `bun test`, against a port nothing | |
| 19 | * answers on, and reads the exit status — the thing CI actually keys on. | |
| 20 | */ | |
| 21 | ||
| 22 | import { describe, expect, it } from 'bun:test' | |
| 23 | import process from 'node:process' | |
| 24 | import { resolve } from 'node:path' | |
| 25 | ||
| 26 | // Any file whose only precondition is a live Postgres. Its own assertions are | |
| 27 | // irrelevant here; what matters is whether the gate let the run pass. | |
| 28 | const GATED = 'packages/bun-query-builder/test/client.json-contains.pg.test.ts' | |
| 29 | const REPO = resolve(import.meta.dir, '../../..') | |
| 30 | const UNREACHABLE = 'postgresql://postgres:postgres@127.0.0.1:59999/test_db' | |
| 31 | ||
| 32 | function runGated(env: Record<string, string | undefined>): { code: number | null, output: string } { | |
| 33 | const child = Bun.spawnSync([process.execPath, 'test', GATED], { | |
| 34 | cwd: REPO, | |
| 35 | env: { | |
| 36 | ...process.env, | |
| 37 | // Cleared so the parent environment cannot decide the case under test: | |
| 38 | // a developer machine has no CI, a runner has it set, and this file has | |
| 39 | // to produce both regardless of where it runs. | |
| 40 | CI: undefined, | |
| 41 | QB_REQUIRE_LIVE: undefined, | |
| 42 | DATABASE_URL: UNREACHABLE, | |
| 43 | POSTGRES_URL: UNREACHABLE, | |
| 44 | ...env, | |
| 45 | } as Record<string, string>, | |
| 46 | stdout: 'pipe', | |
| 47 | stderr: 'pipe', | |
| 48 | }) | |
| 49 | ||
| 50 | return { | |
| 51 | code: child.exitCode, | |
| 52 | output: `${child.stdout.toString()}${child.stderr.toString()}`, | |
| 53 | } | |
| 54 | } | |
| 55 | ||
| 56 | describe('the live-Postgres gate', () => { | |
| 57 | it('fails the run when a provided Postgres is unreachable', () => { | |
| 58 | const { code, output } = runGated({ CI: '1' }) | |
| 59 | ||
| 60 | // The assertion that matters is the status, not the text: a skipped suite | |
| 61 | // exits 0, and 0 is what CI reads as "the guards ran and passed". | |
| 62 | expect(code).not.toBe(0) | |
| 63 | // And it has to say what was missing, or the red run sends someone hunting | |
| 64 | // through 14 files for a failure that is really a dead service. | |
| 65 | expect(output).toContain('live Postgres') | |
| 66 | expect(output).toContain('59999') | |
| 67 | }, 30_000) | |
| 68 | ||
| 69 | it('skips, and passes, where Postgres is merely optional', () => { | |
| 70 | const { code, output } = runGated({}) | |
| 71 | ||
| 72 | expect(code).toBe(0) | |
| 73 | expect(output).toContain('skipping live Postgres tests') | |
| 74 | }, 30_000) | |
| 75 | ||
| 76 | it('lets a CI-like environment without services opt out', () => { | |
| 77 | // A fork running the suite with no service containers: CI is set, but | |
| 78 | // nothing provides Postgres, so the gate must stay a skip. | |
| 79 | const { code } = runGated({ CI: '1', QB_REQUIRE_LIVE: '0' }) | |
| 80 | ||
| 81 | expect(code).toBe(0) | |
| 82 | }, 30_000) | |
| 83 | ||
| 84 | it('lets a developer opt in to CI strictness', () => { | |
| 85 | const { code } = runGated({ QB_REQUIRE_LIVE: '1' }) | |
| 86 | ||
| 87 | expect(code).not.toBe(0) | |
| 88 | }, 30_000) | |
| 89 | }) | |