Files
computer-playbook/roles/web-app-listmonk/files/playwright/playwright.spec.js
kevinveenbirkenbachandClaude Opus 5 c94a0a0b5d test(mcp): ask the ten enabled surfaces the hostile question
Nine roles enabled an MCP surface without a browser-side spec, so nothing
ever asked what the endpoint answers a caller with no credential at all.
Lift gitea's assertion into two shared registrars and register them per
role, and give each role the MCP_SERVICE_ENABLED flag they gate on.

mcpEndpointUrl now takes the base URL rather than reading APP_BASE_URL
unconditionally: jellyfin and zammad export their own base variable, and
declaring APP_BASE_URL for them would un-skip the shared guest persona,
which collapses itself on that variable being absent.

The env-gate scanner follows require("./x") into the shared harness so a
gate declared there counts as consumed, and strips block comments first
so service-gating.js's own doc examples do not credit EMAIL, SSO and OICD
to every role that imports it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 16:06:19 +02:00

70 lines
2.8 KiB
JavaScript

const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = 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 || "");
test.beforeEach(async ({ page }) => {
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
expect(canonicalDomain, "CANONICAL_DOMAIN must be set").toBeTruthy();
await page.context().clearCookies();
});
test("Listmonk front page is served under canonical domain with TLS", async ({ page }) => {
const response = await gotoOnion(page, `${appBaseUrl}/`);
expect(response, "Expected Listmonk response").toBeTruthy();
expect(response.status(), "Expected Listmonk front page status < 400").toBeLessThan(400);
expect(
response.url().includes(canonicalDomain),
`Expected canonical domain "${canonicalDomain}" to back the Listmonk URL`
).toBe(true);
const headers = response.headers();
expectHstsWhenTls(headers, appBaseUrl, "Listmonk");
});
test("Listmonk returns HTML content under canonical domain", async ({ request }) => {
const response = await request.get(`${appBaseUrl}/`, { timeout: resolveTimeout(30_000) });
expect(response.status(), "Expected Listmonk front page status < 400").toBeLessThan(400);
const contentType = response.headers()["content-type"] || "";
expect(
contentType.includes("text/html"),
`Expected HTML content-type, got "${contentType}"`
).toBe(true);
});
// Persona scenarios.
// Bodies live in the shared helper roles/test-e2e-playwright/files/personas
// 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-app-listmonk admin-only interaction: open a management surface.
const link = interactivePage
.getByRole("link", { name: /^(campaigns|subscribers|lists|admin)$/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(
/campaign|subscriber|list|template|listmonk/i,
{ timeout: resolveTimeout(30_000) },
);
}
},
});
});