test(playwright): refuse an env value defaulted to "" as a match argument

`"".endsWith(x)`, `x.includes("")` and `x.startsWith("")` are true for every
string. A spec that binds

    const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");

and later asserts `url.includes(canonicalDomain)` therefore stops testing
anything the moment the deploy does not write that key: the assertion passes,
the job is green, and the missing value never surfaces.

The repository already refuses the list-shaped half of this. `decodeDotenvJsonList`
throws rather than return `[]` because an empty list "would pass every assertion
that iterates it". `requireDotenvValue` is the scalar counterpart and throws
while the module loads.

43 bindings across 43 files fed a matcher this way, in exactly two shapes:
`decodeDotenvQuotedValue(ENV || "")` forty times and `normalizeBaseUrl(ENV || "")`
three times. The wrapper is kept where it does work, so the URL normalisation
survives and only the core is replaced. 38 `decodeDotenvQuotedValue` imports fell
out of use and are removed; eslint reporting no unused binding is what confirms
that.

`|| ""` itself is not the defect and is not banned: it appears 400 times, and
most of those values are compared with `===`, where empty simply is not equal,
or flow into `normalizeBaseUrl`, which tolerates it. The lint pairs the empty
default with the matcher and reports only that combination, which is why the set
is 43 and not 400.

xwiki carries the instance this started from. Its OIDC spec asserted that the
IdP host ends with the last two labels of the XWiki host, so under a three-label
domain it checked `infinito.test` and would have accepted any foreign host in
the parent zone. It now checks `DOMAIN_PRIMARY`, exported for the purpose.

Verified by running it: node --check on all 48 files, eslint 0 errors,
`make lint-playwright` parses 89 specs. That last one is the load-bearing
evidence, because `requireDotenvValue` throws at module load and ce1a19d3c2 hit
exactly that wall on seaweedfs this morning; it holds here because the lint
harness stubs an unmatched key as the non-empty string "stub". The rule was also
watched failing: restoring one binding reports
`test-baseline.js:21: .includes(canonicalDomain)`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-30 22:14:50 +02:00
1 parent f84e85c8c2
commit 48b119a381
47 files changed
+528 -96

No files matched your search

@@ -61,8 +61,20 @@ function decodeDotenvJsonList(raw, name) {
return parsed;
}
function requireDotenvValue(raw, name) {
const decoded = decodeDotenvQuotedValue(raw || "");
if (typeof decoded !== "string" || decoded === "") {
throw new Error(
`${name} is empty — a substring or suffix check against an empty string ` +
"matches everything, so the assertion would pass without testing anything."
);
}
return decoded;
}
module.exports = {
decodeDotenvQuotedValue,
decodeDotenvJsonList,
requireDotenvValue,
normalizeBaseUrl,
};
@@ -1,10 +1,14 @@
const { test, expect } = require("@playwright/test");
const { decodeDotenvQuotedValue, normalizeBaseUrl, gotoOnion } = require("./personas");
const {
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const baseUrl = normalizeBaseUrl(process.env.AKAUNTING_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test("baseline: Akaunting responds on the canonical domain", async ({ page }) => {
expect(baseUrl, "AKAUNTING_BASE_URL must be set").toBeTruthy();
@@ -1,8 +1,12 @@
const { test, expect } = require("@playwright/test");
const { normalizeBaseUrl, decodeDotenvQuotedValue, gotoOnion } = require("./personas");
const {
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
} = require("./personas");
const baseUrl = normalizeBaseUrl(process.env.BASEROW_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.use({ ignoreHTTPSErrors: true });
@@ -1,8 +1,12 @@
const { test, expect } = require("@playwright/test");
const { normalizeBaseUrl, decodeDotenvQuotedValue, gotoOnion } = require("./personas");
const {
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
} = require("./personas");
const baseUrl = normalizeBaseUrl(process.env.BOOKWYRM_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.use({ ignoreHTTPSErrors: true });
@@ -1,11 +1,18 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, gotoOnion, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
const {
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,14 @@
const { expect } = require("./fixtures/onion-test");
const { resolveTimeout } = require("./timeouts");
const { normalizeBaseUrl } = require("./personas");
const {
normalizeBaseUrl,
requireDotenvValue,
} = require("./personas");
const { isServiceEnabled, skipUnlessServiceEnabled } = require("./service-gating");
const dashboardJsBaseUrl = normalizeBaseUrl(process.env.DASHBOARD_JS_BASE_URL || "");
const matomoBaseUrl = normalizeBaseUrl(process.env.MATOMO_BASE_URL || "");
const matomoBaseUrl = normalizeBaseUrl(requireDotenvValue(process.env.MATOMO_BASE_URL, "MATOMO_BASE_URL"));
async function beforeEach({ page }) {
await page.setViewportSize({ width: 1440, height: 1100 });
@@ -1,9 +1,15 @@
const { test, expect } = require("./fixtures/onion-test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, isVisible, normalizeBaseUrl, gotoOnion } = require("./personas");
const {
decodeDotenvQuotedValue,
gotoOnion,
isVisible,
normalizeBaseUrl,
requireDotenvValue,
} = require("./personas");
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const appBaseUrl = normalizeBaseUrl(requireDotenvValue(process.env.APP_BASE_URL, "APP_BASE_URL"));
const oidcIssuerUrl = normalizeBaseUrl(process.env.OIDC_ISSUER_URL || "");
const loginUsername = decodeDotenvQuotedValue(process.env.LOGIN_USERNAME);
const loginPassword = decodeDotenvQuotedValue(process.env.LOGIN_PASSWORD);
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,8 +1,12 @@
const { test, expect } = require("@playwright/test");
const { decodeDotenvQuotedValue, normalizeBaseUrl, gotoOnion } = require("./personas");
const {
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
} = require("./personas");
const baseUrl = normalizeBaseUrl(process.env.FLOWISE_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.use({ ignoreHTTPSErrors: true });
@@ -1,13 +1,21 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
require("./test-native-login");
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -2,12 +2,21 @@ const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, performKeycloakLoginForm, runAdminFlow, runBiberFlow, runGuestFlow, gotoOnion } = require("./personas");
const {
decodeDotenvQuotedValue,
gotoOnion,
normalizeBaseUrl,
performKeycloakLoginForm,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const baseUrl = normalizeBaseUrl(process.env.FUSIONDIRECTORY_BASE_URL || "");
const oidcIssuerUrl = normalizeBaseUrl(process.env.OIDC_ISSUER_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME);
const adminPassword = decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD);
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,13 +1,21 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
require("./test-mcp-guest").register();
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,7 +1,15 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
require("./test-seaweedfs");
@@ -9,7 +17,7 @@ require("./test-seaweedfs");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -9,11 +9,12 @@ const {
installCspViolationObserver,
normalizeBaseUrl,
performKeycloakLoginForm,
requireDotenvValue,
runGuestFlow,
} = require("./personas");
const oidcIssuerUrl = normalizeBaseUrl(process.env.OIDC_ISSUER_URL || "");
const elementBaseUrl = normalizeBaseUrl(process.env.ELEMENT_BASE_URL || "");
const elementBaseUrl = normalizeBaseUrl(requireDotenvValue(process.env.ELEMENT_BASE_URL, "ELEMENT_BASE_URL"));
const matrixBaseUrl = normalizeBaseUrl(process.env.MATRIX_BASE_URL || "");
const matrixServerName = decodeDotenvQuotedValue(process.env.MATRIX_SERVER_NAME);
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME);
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -3,11 +3,20 @@ const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { assertCspResponseHeader, decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
assertCspResponseHeader,
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
const apiBaseUrl = normalizeBaseUrl(process.env.API_BASE_URL || "");
test.beforeEach(async ({ page }) => {
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,9 +1,13 @@
const { test, expect } = require("@playwright/test");
const { normalizeBaseUrl, decodeDotenvQuotedValue, gotoOnion } = require("./personas");
const {
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
} = require("./personas");
const { resolveTimeout } = require("./timeouts");
const baseUrl = normalizeBaseUrl(process.env.OPENBAO_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.use({ ignoreHTTPSErrors: true });
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,7 +1,11 @@
// @ts-check
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
requireDotenvValue,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
@@ -14,7 +18,7 @@ function decodeDotenvQuotedValue(value) {
}
const piholeBaseUrl = decodeDotenvQuotedValue(process.env.PIHOLE_BASE_URL);
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(piholeBaseUrl, "PIHOLE_BASE_URL must be set").toBeTruthy();
@@ -1,8 +1,12 @@
const { test, expect } = require("@playwright/test");
const { normalizeBaseUrl, decodeDotenvQuotedValue, gotoOnion } = require("./personas");
const {
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
} = require("./personas");
const baseUrl = normalizeBaseUrl(process.env.POSTMARKS_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.use({ ignoreHTTPSErrors: true });
@@ -1,13 +1,21 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
require("./test-mcp-guest").register();
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,13 +1,21 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
require("./test-admin-native");
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,9 +1,14 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { normalizeBaseUrl, decodeDotenvQuotedValue , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
} = require("./personas");
const baseUrl = normalizeBaseUrl(process.env.SNIPE_IT_BASE_URL || process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.use({ ignoreHTTPSErrors: true });
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -4,15 +4,15 @@ const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const { skipUnlessServiceEnabled } = require("../service-gating");
const {
decodeDotenvQuotedValue,
normalizeBaseUrl,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
} = require("../personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
const ssoEnabled = (process.env.SSO_SERVICE_ENABLED || "").toLowerCase() === "true";
test("ldap-authenticator: XWiki enforces its LDAP-backed native credential form on protected pages", async ({ page }) => {
@@ -5,14 +5,16 @@ const { skipUnlessAddonEnabled } = require("../addon-gating");
const { skipUnlessServiceEnabled } = require("../service-gating");
const {
decodeDotenvQuotedValue,
normalizeBaseUrl,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
} = require("../personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const domainPrimary = requireDotenvValue(process.env.DOMAIN_PRIMARY, "DOMAIN_PRIMARY");
test("oidc-authenticator: XWiki login is coupled to the Keycloak OIDC provider", async ({ page }) => {
skipUnlessAddonEnabled("oidc-authenticator");
@@ -53,21 +55,14 @@ test("oidc-authenticator: XWiki login is coupled to the Keycloak OIDC provider",
`expected the XWiki login action to hand off to the Keycloak OIDC authorization endpoint (proves oidc.provider/endpoint.authorization/clientid are wired and authservice=oidc is active), got ${authUrl}`,
).toBe(true);
// The authorization endpoint must live on the external Keycloak/IdP host,
// not on the XWiki host itself — proving the redirect targets the
// configured provider rather than looping back to a local login form.
// Both hosts share the deployment's registrable parent domain (XWiki is
// served at x.wiki.<DOMAIN_PRIMARY>, Keycloak at auth.<DOMAIN_PRIMARY>),
// so the IdP host must differ from the XWiki host yet share that parent.
const idpHost = new URL(authUrl).hostname;
const parentDomain = xwikiHost.split(".").slice(-2).join(".");
expect(
idpHost,
`expected the OIDC authorization endpoint to live on the external Keycloak host, not the XWiki host (${xwikiHost}); got ${idpHost}`,
).not.toBe(xwikiHost);
expect(
idpHost.endsWith(parentDomain),
`expected the OIDC authorization endpoint host (${idpHost}) to belong to the deployment domain (${parentDomain}, derived from ${xwikiHost})`,
idpHost.endsWith(domainPrimary),
`expected the OIDC authorization endpoint host (${idpHost}) to belong to the deployment domain (${domainPrimary})`,
).toBe(true);
// The Keycloak login form (not an error page) must render for the
@@ -1,11 +1,19 @@
const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas");
const {
expectHstsWhenTls,
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
@@ -12,6 +12,8 @@ BIBER_USERNAME={{ lookup('users', 'biber').username | dotenv_quote }}
CANONICAL_DOMAIN={{ lookup('domain', application_id) | dotenv_quote }}
DOMAIN_PRIMARY={{ DOMAIN_PRIMARY | dotenv_quote }}
# nocheck: playwright-service-gate — covered by sys-svc-mail integration tests, not the persona surface
EMAIL_SERVICE_ENABLED={{ lookup('config', application_id, 'services.email.enabled') | string | lower }}
@@ -1,8 +1,12 @@
const { test, expect } = require("@playwright/test");
const { normalizeBaseUrl, decodeDotenvQuotedValue, gotoOnion } = require("./personas");
const {
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
} = require("./personas");
const baseUrl = normalizeBaseUrl(process.env.YOURLS_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.use({ ignoreHTTPSErrors: true });
@@ -2,11 +2,18 @@ const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, gotoOnion, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow } = require("./personas");
const {
gotoOnion,
normalizeBaseUrl,
requireDotenvValue,
runAdminFlow,
runBiberFlow,
runGuestFlow,
} = require("./personas");
test.use({ ignoreHTTPSErrors: true });
const baseUrl = normalizeBaseUrl(process.env.XMPP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
const canonicalDomain = requireDotenvValue(process.env.CANONICAL_DOMAIN, "CANONICAL_DOMAIN");
test.beforeEach(async ({ page }) => {
expect(baseUrl, "XMPP_BASE_URL must be set").toBeTruthy();
@@ -0,0 +1,136 @@
"""Lint guard: an env value defaulted to ``""`` MUST NOT feed a match.
``"".endsWith(x)``, ``x.includes("")`` and ``x.startsWith("")`` are true for
every string. A spec that binds ``const host = decodeDotenvQuotedValue(
process.env.CANONICAL_DOMAIN || "")`` and later asserts
``url.includes(host)`` therefore stops testing anything the moment the deploy
forgets to write that key: the assertion passes, the suite is green, and the
missing value never surfaces.
The repository already refuses the list-shaped version of this: the throwing
``decodeDotenvJsonList`` exists because reading a mis-decoded list as ``[]``
"would pass every assertion that iterates it". This rule is the scalar half.
Bind the value with ``requireDotenvValue(process.env.<KEY>, "<KEY>")`` instead,
which throws while the module loads, or keep the ``|| ""`` and guard the value
before matching on it.
Suppress on the binding line with ``// nocheck: vacuous-env-match -- <reason>``.
"""
from __future__ import annotations
import re
import subprocess
import unittest
from dataclasses import dataclass
from typing import TYPE_CHECKING
from utils.cache.files import read_text
from . import PROJECT_ROOT
if TYPE_CHECKING:
from pathlib import Path
_BINDING_RE = re.compile(
r"^[^\S\n]*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(.*?);[^\S\n]*$",
re.MULTILINE,
)
_EMPTY_DEFAULT_RE = re.compile(r"\|\|\s*\"\"")
_NOCHECK_RE = re.compile(r"//\s*nocheck:\s*vacuous-env-match\b")
_MATCHERS = ("endsWith", "startsWith", "includes")
@dataclass(frozen=True)
class Violation:
file: str
line_no: int
name: str
matcher: str
def _git_ls_files() -> list[str]:
out = subprocess.check_output(
["git", "-c", "safe.directory=*", "-C", str(PROJECT_ROOT), "ls-files"],
text=True,
)
return [line for line in out.splitlines() if line]
def _empty_defaulted_env_names(text: str) -> set[str]:
names: set[str] = set()
for match in _BINDING_RE.finditer(text):
expression = match.group(2)
if "process.env." not in expression:
continue
if not _EMPTY_DEFAULT_RE.search(expression):
continue
if _NOCHECK_RE.search(match.group(0)):
continue
names.add(match.group(1))
return names
def _scan_file(path: Path) -> list[Violation]:
rel = path.relative_to(PROJECT_ROOT).as_posix()
try:
text = read_text(str(path))
except (OSError, UnicodeDecodeError):
return []
names = _empty_defaulted_env_names(text)
if not names:
return []
violations: list[Violation] = []
for name in sorted(names):
for matcher in _MATCHERS:
pattern = re.compile(rf"\.{matcher}\(\s*{re.escape(name)}\s*\)")
for match in pattern.finditer(text):
line_no = text.count("\n", 0, match.start()) + 1
violations.append(Violation(rel, line_no, name, matcher))
return violations
def _scan_targets() -> list[Path]:
return [
PROJECT_ROOT / rel
for rel in _git_ls_files()
if rel.endswith(".js")
and ("/files/playwright/" in rel or "/files/personas/" in rel)
]
class TestVacuousEnvMatch(unittest.TestCase):
def test_empty_defaulted_env_values_never_feed_a_matcher(self) -> None:
targets = _scan_targets()
self.assertTrue(targets, "no Playwright spec files found to scan")
violations: list[Violation] = []
for path in targets:
violations.extend(_scan_file(path))
if violations:
header = [
f"Env values defaulted to \"\" used as a match argument "
f"({len(violations)}):",
"",
"An empty string is a suffix, a prefix and a substring of every",
"string, so these assertions pass unconditionally whenever the",
"deploy does not write the key.",
"",
'Bind with `requireDotenvValue(process.env.<KEY>, "<KEY>")`, which',
"throws at module load, or guard the value before matching on it.",
"Suppress on the binding line with",
"`// nocheck: vacuous-env-match -- <reason>`.",
"",
"Offenders:",
]
body = [
f" {v.file}:{v.line_no}: .{v.matcher}({v.name})"
for v in sorted(violations)
]
self.fail("\n".join(header + body))
if __name__ == "__main__":
unittest.main()