Files
computer-playbook/roles/web-app-moodle/files/playwright/test-keycloak-scope.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

54 lines
2.3 KiB
JavaScript

const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, gotoOnion } = require("./personas");
const moodleScopeName = decodeDotenvQuotedValue(process.env.MOODLE_OIDC_SCOPE_NAME);
exports.register = function (shared) {
test.describe("moodle keycloak scope wiring (variant 0)", () => {
test.skip(!shared.env.ssoEnabled, "SSO shared service disabled");
test("Keycloak realm discovery advertises the moodle OIDC scope", async ({ request }) => {
expect(shared.env.oidcIssuerUrl, "OIDC_ISSUER_URL must be set in env").toBeTruthy();
const r = await request.get(`${shared.env.oidcIssuerUrl}/.well-known/openid-configuration`, { timeout: resolveTimeout(30_000) });
expect(r.ok(), `discovery doc must be reachable at ${shared.env.oidcIssuerUrl}`).toBeTruthy();
const cfg = await r.json();
expect(Array.isArray(cfg.scopes_supported), "scopes_supported must be an array").toBe(true);
expect(
cfg.scopes_supported.includes(moodleScopeName),
`realm scopes_supported must contain "${moodleScopeName}"`
).toBe(true);
});
test("biber can edit middleName via Keycloak Account REST API and value persists", async ({ page }) => {
expect(shared.env.oidcIssuerUrl, "OIDC_ISSUER_URL must be set in env").toBeTruthy();
expect(shared.env.oidcClientId, "OIDC_CLIENT_ID must be set in env").toBeTruthy();
await gotoOnion(page, `${shared.env.oidcIssuerUrl}/.well-known/openid-configuration`);
const probe = `MN-${Date.now()}`;
const result = await page.evaluate(shared.setMiddleNameViaAccountRest, {
issuer: shared.env.oidcIssuerUrl,
clientId: shared.env.oidcClientId,
username: shared.env.biberUsername,
password: shared.env.biberPassword,
middleName: probe,
withRestore: true,
});
expect(
result.stage,
`flow must reach ok stage; got stage=${result.stage} status=${result.status} body=${(result.body || "").slice(0, 200)}`
).toBe("ok");
expect(
result.attrNames,
"Account user-profile metadata must include the Moodle 'middleName' attribute"
).toContain("middleName");
expect(
result.verifiedMiddleName,
"middleName value must round-trip after Account REST update"
).toBe(probe);
});
});
};