Files
computer-playbook/roles/web-svc-simpleicons/files/playwright/playwright.spec.js
kevinveenbirkenbachandClaude Opus 5 4ce588fea1 test(lint): forbid a JavaScript default for an unset environment variable
`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>
2026-09-12 12:07:57 +02:00

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) },
);
}
},
});
});