mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
`process.env.X || <value>` is a second source for something the deployment
already renders, and the two drift without a sound. playwright.config.js read
`process.env.PLAYWRIGHT_ACTION_TIMEOUT || 30_000` while nothing ever set that
variable, so every run used the unscaled 30 s and no failure said so.
Normalising an absent variable stays allowed, because it substitutes nothing:
`|| ""`, `|| '[]'`, `|| false`, `|| 0` and falling through to another
`process.env` entry only let the caller test absence itself.
The six sites the rule found:
n8n and flowise built a Postgres port from `|| '5432'` while env.j2 renders it
from `lookup('database', application_id, 'port')`. Node throws
ERR_SOCKET_BAD_PORT on the resulting NaN, so an unfed port now names itself
instead of connecting somewhere plausible.
The bluesky broker did the same with BROKER_PORT, which compose.yml.j2 renders
unconditionally. Its INSECURE_TLS default was `"false"`, which is what `""`
already evaluates to.
matomo defaulted MATOMO_TRACKING_SCOPE to "sub"; the only comparison is against
"root", so `""` takes the identical branch.
moodle and simpleicons defaulted a name their playwright.env.j2 always renders.
simpleicons already asserts the value is truthy; moodle now fails on the scope
it cannot find rather than on a name nobody configured.
Four values the code itself owns keep a default under `// nocheck: env-default`:
PDS_INTERNAL_URL (which falls through to requireEnv, not to a literal),
HANDOFF_COOKIE_NAME, HANDOFF_COOKIE_MAX_AGE and PASSWORD_SALT_HASH_ROUNDS. No
role renders any of them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
121 lines
5.0 KiB
JavaScript
121 lines
5.0 KiB
JavaScript
const { test, expect } = require("@playwright/test");
|
|
const { resolveTimeout } = require("./timeouts");
|
|
|
|
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
|
|
test.use({
|
|
ignoreHTTPSErrors: true
|
|
});
|
|
|
|
function contentTypeOf(response) {
|
|
return response.headers()["content-type"] || "";
|
|
}
|
|
|
|
function expectOrigin(url, expectedOrigin, label) {
|
|
expect(new URL(url).origin, label).toBe(expectedOrigin);
|
|
}
|
|
|
|
function expectLandingPageText(body) {
|
|
const normalized = body.toLowerCase();
|
|
|
|
expect(
|
|
normalized,
|
|
"Expected the Simple Icons landing page to expose README or provider content on the Simple Icons domain"
|
|
).toMatch(/simple icons|simpleicons\.org|provided by/);
|
|
}
|
|
|
|
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
|
|
const knownIconSlug = decodeDotenvQuotedValue(process.env.KNOWN_ICON_SLUG);
|
|
|
|
test("simpleicons serves keycloak assets directly on its own domain", async ({ request }) => {
|
|
expect(appBaseUrl, "APP_BASE_URL must be set in the Playwright env file").toBeTruthy();
|
|
expect(knownIconSlug, "KNOWN_ICON_SLUG must be set in the Playwright env file").toBeTruthy();
|
|
|
|
const expectedOrigin = new URL(appBaseUrl).origin;
|
|
|
|
const landingResponse = await request.get(appBaseUrl, { maxRedirects: 0, timeout: resolveTimeout(30_000) });
|
|
const landingStatus = landingResponse.status();
|
|
|
|
expect(
|
|
landingStatus === 200 || (landingStatus >= 301 && landingStatus <= 302),
|
|
`Expected the Simple Icons landing page to return HTTP 200 or a redirect (301/302), got ${landingStatus}`
|
|
).toBe(true);
|
|
|
|
if (landingStatus === 200) {
|
|
expectOrigin(
|
|
landingResponse.url(),
|
|
expectedOrigin,
|
|
`Expected the Simple Icons landing page to stay on ${expectedOrigin} when no redirect is configured`
|
|
);
|
|
const landingBody = await landingResponse.text();
|
|
expect(contentTypeOf(landingResponse)).toMatch(/text\/html|text\/plain/);
|
|
expectLandingPageText(landingBody);
|
|
} else {
|
|
const location = landingResponse.headers()["location"];
|
|
expect(location, "Expected the redirect to include a Location header").toBeTruthy();
|
|
}
|
|
|
|
const svgResponse = await request.get(`${appBaseUrl}/${knownIconSlug}.svg`, { timeout: resolveTimeout(30_000) });
|
|
expect(svgResponse.status(), `Expected ${knownIconSlug}.svg to return HTTP 200`).toBe(200);
|
|
expectOrigin(
|
|
svgResponse.url(),
|
|
expectedOrigin,
|
|
`Expected ${knownIconSlug}.svg to stay on the Simple Icons domain instead of redirecting elsewhere`
|
|
);
|
|
expect(contentTypeOf(svgResponse)).toContain("image/svg+xml");
|
|
const svgBody = await svgResponse.text();
|
|
expect(svgBody).toContain("<svg");
|
|
expect(svgBody).toContain("<path");
|
|
|
|
const pngResponse = await request.get(`${appBaseUrl}/${knownIconSlug}.png?size=64`, { timeout: resolveTimeout(30_000) });
|
|
expect(pngResponse.status(), `Expected ${knownIconSlug}.png to return HTTP 200`).toBe(200);
|
|
expectOrigin(
|
|
pngResponse.url(),
|
|
expectedOrigin,
|
|
`Expected ${knownIconSlug}.png to stay on the Simple Icons domain instead of redirecting elsewhere`
|
|
);
|
|
expect(contentTypeOf(pngResponse)).toContain("image/png");
|
|
const pngBody = await pngResponse.body();
|
|
expect(Buffer.compare(pngBody.subarray(0, 8), Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))).toBe(0);
|
|
expect(pngBody.length, "Expected the PNG route to return a non-empty image").toBeGreaterThan(100);
|
|
|
|
const missingIconResponse = await request.get(`${appBaseUrl}/definitely-not-a-real-icon.svg`, { failOnStatusCode: false, timeout: resolveTimeout(30_000) });
|
|
expect(missingIconResponse.status(), "Expected unknown icons to return HTTP 404").toBe(404);
|
|
expectOrigin(
|
|
missingIconResponse.url(),
|
|
expectedOrigin,
|
|
"Expected missing icons to stay on the Simple Icons domain instead of redirecting elsewhere"
|
|
);
|
|
expect(await missingIconResponse.text()).toContain("Icon not found");
|
|
});
|
|
|
|
// Persona scenarios.
|
|
// Bodies live in the shared helper roles/test-e2e-playwright/files/personas.js
|
|
// so every role's persona flow stays consistent.
|
|
|
|
test("guest: public-landing → auth chain → never authenticated", async ({ page }) => {
|
|
await runGuestFlow(page);
|
|
});
|
|
|
|
test("biber: app → universal logout", async ({ page }) => {
|
|
await runBiberFlow(page);
|
|
});
|
|
|
|
test("administrator: app → universal logout", async ({ page }) => {
|
|
await runAdminFlow(page, {
|
|
adminInteraction: async (interactivePage) => {
|
|
// web-svc-simpleicons admin-only interaction: open a management surface.
|
|
const link = interactivePage
|
|
.getByRole("link", { name: /^(admin|index|status)$/i })
|
|
.first();
|
|
if (await link.isVisible().catch(() => false)) {
|
|
await link.click({ timeout: resolveTimeout(30_000) }).catch(() => {});
|
|
await interactivePage.waitForLoadState("domcontentloaded", { timeout: resolveTimeout(30_000) }).catch(() => {});
|
|
await expect(interactivePage.locator("body")).toContainText(
|
|
/admin|status|index|simpleicons/i,
|
|
{ timeout: resolveTimeout(30_000) },
|
|
);
|
|
}
|
|
},
|
|
});
|
|
});
|