Files
computer-playbook/roles/web-app-mastodon/files/playwright/test-seaweedfs.js
kevinveenbirkenbachandClaude Opus 5 4f5dc2be44 fix(mastodon): skip the SeaweedFS filer check on an onion target
6f9142027 gave twelve consumer specs the onion skip that nextcloud already
carried, and mastodon is the one it missed. `git show 6f9142027 --stat` names no
mastodon file, and no commit on any ref ever added the gate to the role:
`git log --all -S'isOnionTarget' -- roles/web-app-mastodon/` is empty. Measured
before this change, the role reported 0 occurrences of isOnionTarget while
matrix, nextcloud and taiga reported 2 each; thirteen roles carried the skip,
now fourteen do.

The gap is not cosmetic, because the gate the spec did have guards the wrong
thing. skipUnlessServiceEnabled("seaweedfs") asks about the BACKEND service,
which is enabled on a Tor node; the filer's browser surface is not. The spec
therefore ran, and adminLoginFiler navigated to the clearnet filer host. On a
node whose Tor network is a private chutney instance with no clearnet exit that
host does not answer 404 -- it hangs, and Playwright reported net::ERR_TIMED_OUT.

The skip has to come before the backend gate for that reason, which is where the
thirteen siblings put it and where this change puts it. The message string is
byte-identical to theirs.

Not verified by a run: what is proven is the parity with thirteen siblings and
the domain derivation in timeouts.js:15-17, not an observed skip.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 02:02:30 +02:00

85 lines
3.5 KiB
JavaScript

// SeaweedFS object-store scenario for Mastodon.
//
// Required env (rendered by templates/playwright.env.j2):
// APP_BASE_URL, CANONICAL_DOMAIN, ADMIN_USERNAME, ADMIN_PASSWORD and the
// SEAWEEDFS_* keys consumed by runSeaweedfsStorageCheck.
const { test, expect } = require("@playwright/test");
const { resolveTimeout, isOnionTarget } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { normalizeUrl, readEnv, performKeycloakLogin, clickOidcLoginLink, runSeaweedfsStorageCheck } = require("./personas");
const AVATAR_PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"base64",
);
test.use({ ignoreHTTPSErrors: true });
test("seaweedfs: a saved Mastodon avatar is stored in the SeaweedFS bucket", async ({ page, browser }) => {
test.skip(isOnionTarget(), "SeaweedFS filer UI is not a Tor surface on an onion node (headless backend)");
skipUnlessServiceEnabled("seaweedfs");
test.skip(
(process.env.PERSONA_ADMINISTRATOR_BLOCKED || "").toLowerCase() === "true",
"administrator persona is blocked by the role contract (PERSONA_ADMINISTRATOR_BLOCKED=true); this scenario drives the same admin journey.",
);
test.setTimeout(resolveTimeout(300_000));
const appBaseUrl = normalizeUrl(process.env.APP_BASE_URL);
const canonicalDomain = readEnv("CANONICAL_DOMAIN");
const adminUsername = readEnv("ADMIN_USERNAME");
const adminPassword = readEnv("ADMIN_PASSWORD");
await runSeaweedfsStorageCheck(page, browser, {
label: "a Mastodon profile avatar upload",
action: async (appPage) => {
const base = appBaseUrl.replace(/\/$/, "");
await appPage.goto(`${base}/auth/sign_in`, { waitUntil: "domcontentloaded" });
if (!appPage.url().includes("openid-connect/auth")) {
const strictLogin = appPage
.getByRole("link", { name: /^\s*(log\s*in|sign\s*in|sso)\s*$/i })
.first();
const looseLogin = appPage
.getByRole("link", { name: /log\s*in|sign\s*in|sso/i })
.first();
await clickOidcLoginLink(appPage, strictLogin, looseLogin);
}
if (appPage.url().includes("openid-connect/auth")) {
await performKeycloakLogin(appPage, adminUsername, adminPassword, canonicalDomain);
}
await appPage.goto(`${base}/settings/profile`, { waitUntil: "domcontentloaded" });
const movedEditor = appPage.locator("a[href$='/profile/edit']").first();
const editorMoved = await movedEditor
.waitFor({ state: "attached", timeout: resolveTimeout(5_000) })
.then(() => true)
.catch(() => false);
if (editorMoved) {
await appPage.goto(`${base}/profile/edit`, { waitUntil: "domcontentloaded" });
}
const fileInput = appPage
.locator('input#account_avatar, input[type="file"][name*="avatar" i], input[type="file"]')
.first();
await expect(
fileInput,
"the Mastodon profile editing page must expose an avatar file input",
).toBeAttached({ timeout: resolveTimeout(60_000) });
await fileInput.setInputFiles({
name: `infinito-storage-check-${Date.now()}.png`,
mimeType: "image/png",
buffer: AVATAR_PNG,
});
await appPage
.getByRole("button", { name: /save changes|save|speichern/i })
.or(appPage.locator('button[type="submit"], input[type="submit"]'))
.first()
.click();
await appPage.waitForLoadState("networkidle", { timeout: resolveTimeout(60_000) }).catch(() => {});
},
});
});