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>
146 lines
5.0 KiB
JavaScript
146 lines
5.0 KiB
JavaScript
// Shared Matomo Playwright scaffolding: env vars, the per-test setup hook, the
|
|
// consumer-role list and the host / base-domain matchers. Every *.spec.js in
|
|
// this directory requires this module so each test file applies the same
|
|
// env-presence guard and reuses the same helpers and admin-login flow.
|
|
|
|
const { expect } = require("@playwright/test");
|
|
const { resolveTimeout } = require("./timeouts");
|
|
const { decodeDotenvQuotedValue, gotoOnion, installCspViolationObserver, normalizeBaseUrl } = require("./personas");
|
|
|
|
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
|
|
const oidcIssuerUrl = normalizeBaseUrl(process.env.OIDC_ISSUER_URL || "");
|
|
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME);
|
|
const adminPassword = decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD);
|
|
const biberUsername = decodeDotenvQuotedValue(process.env.BIBER_USERNAME);
|
|
const biberPassword = decodeDotenvQuotedValue(process.env.BIBER_PASSWORD);
|
|
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN);
|
|
const matomoApiToken = decodeDotenvQuotedValue(process.env.MATOMO_API_TOKEN);
|
|
const matomoTrackingScope = (process.env.MATOMO_TRACKING_SCOPE || "").trim().toLowerCase();
|
|
|
|
const matomoCanonicalDomain = (() => {
|
|
try {
|
|
return new URL(appBaseUrl).hostname;
|
|
} catch {
|
|
return "";
|
|
}
|
|
})();
|
|
|
|
// Emitted at deploy time by templates/playwright.env.j2 via the
|
|
// roles_with_service('matomo') Ansible filter: one entry per role declared as a
|
|
// matomo consumer in its meta/services.yml.
|
|
const matomoTargetRoles = (() => {
|
|
const raw = process.env.MATOMO_TARGET_ROLES_JSON || "[]";
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
return Array.isArray(parsed) ? parsed : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
})();
|
|
|
|
function attachDiagnostics(page) {
|
|
const consoleErrors = [];
|
|
const pageErrors = [];
|
|
const cspRelated = [];
|
|
|
|
page.on("console", (message) => {
|
|
if (message.type() === "error") {
|
|
consoleErrors.push(message.text());
|
|
}
|
|
|
|
if (/content security policy|csp/i.test(message.text())) {
|
|
cspRelated.push({ source: "console", text: message.text() });
|
|
}
|
|
});
|
|
|
|
page.on("pageerror", (error) => {
|
|
const text = String(error);
|
|
pageErrors.push(text);
|
|
|
|
if (/content security policy|csp/i.test(text)) {
|
|
cspRelated.push({ source: "pageerror", text });
|
|
}
|
|
});
|
|
|
|
return { consoleErrors, pageErrors, cspRelated };
|
|
}
|
|
|
|
// alias_urls / main_url may be a full URL or a bare host; normalise both to the hostname before matching
|
|
function hostOf(value) {
|
|
const s = String(value || "")
|
|
.trim()
|
|
.toLowerCase();
|
|
if (!s) return "";
|
|
try {
|
|
return new URL(s.includes("://") ? s : `https://${s}`).hostname;
|
|
} catch {
|
|
return s.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").split("/")[0];
|
|
}
|
|
}
|
|
|
|
// MUST mirror sys-front-inj-matomo matomo_site_domain in root scope: one shared site per registrable domain, not per subdomain
|
|
function baseDomainOf(host) {
|
|
return String(host || "")
|
|
.toLowerCase()
|
|
.replace(/^(?:.*\.)?(.+\..+)$/, "$1");
|
|
}
|
|
|
|
// Tracking-site needle for a consumer host: full subdomain in 'sub' scope (one site per subdomain), registrable base in 'root' scope (one shared site)
|
|
function siteNeedleFor(host) {
|
|
return matomoTrackingScope === "root" ? baseDomainOf(host) : hostOf(host);
|
|
}
|
|
|
|
async function setupMatomoPage(page) {
|
|
await page.setViewportSize({ width: 1440, height: 1100 });
|
|
|
|
expect(appBaseUrl, "APP_BASE_URL must be set in the Playwright env file").toBeTruthy();
|
|
expect(adminUsername, "ADMIN_USERNAME must be set in the Playwright env file").toBeTruthy();
|
|
expect(adminPassword, "ADMIN_PASSWORD must be set in the Playwright env file").toBeTruthy();
|
|
expect(canonicalDomain, "CANONICAL_DOMAIN must be set in the Playwright env file").toBeTruthy();
|
|
|
|
await page.context().clearCookies();
|
|
await installCspViolationObserver(page);
|
|
}
|
|
|
|
async function loginAsAdmin(page) {
|
|
await gotoOnion(page, `${appBaseUrl}/index.php?module=Login`);
|
|
|
|
const usernameField = page.locator("input#login_form_login, input[name='form_login']").first();
|
|
const passwordField = page.locator("input#login_form_password, input[name='form_password']").first();
|
|
const submitButton = page
|
|
.locator("input#login_form_submit, button#login_form_submit, button[type='submit'], input[type='submit']")
|
|
.first();
|
|
|
|
await expect(usernameField, "Expected Matomo login form username field").toBeVisible({ timeout: resolveTimeout(60_000) });
|
|
await usernameField.fill(adminUsername);
|
|
await passwordField.fill(adminPassword);
|
|
await submitButton.click();
|
|
|
|
await expect
|
|
.poll(() => page.url(), {
|
|
timeout: resolveTimeout(60_000),
|
|
message: "Expected Matomo login to leave the Login module",
|
|
})
|
|
.not.toContain("module=Login");
|
|
}
|
|
|
|
module.exports = {
|
|
appBaseUrl,
|
|
oidcIssuerUrl,
|
|
adminUsername,
|
|
adminPassword,
|
|
biberUsername,
|
|
biberPassword,
|
|
canonicalDomain,
|
|
matomoApiToken,
|
|
matomoTrackingScope,
|
|
matomoCanonicalDomain,
|
|
matomoTargetRoles,
|
|
attachDiagnostics,
|
|
hostOf,
|
|
baseDomainOf,
|
|
siteNeedleFor,
|
|
setupMatomoPage,
|
|
loginAsAdmin,
|
|
};
|