mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
Nextcloud#12 of run 33923190220 failed with 'objects before: 4, after: 4, matched: 0'. The bucket held objects already, so the S3 path works; nothing new arrived. The check could not say why, because the action set the file input and moved on: the save button was optional, and if it was not found within two seconds nobody clicked it, the upload never happened, and the failure still read as an object-store problem.
42449ebf0a had fixed a real bug there, isVisible ignores its timeout and never waits, but replaced it with a two second waitFor. Element renders that button after the file is chosen, which is not always inside two seconds on a loaded runner.
Wait for the media upload itself instead. The response wait is armed before the file is set, so it covers whichever interaction triggers the POST, and it gives the flow time rather than racing it. A future failure now separates the two causes it used to merge: Element never POSTed, or Synapse answered with a status.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
98 lines
4.0 KiB
JavaScript
98 lines
4.0 KiB
JavaScript
// SeaweedFS object-store scenario for Matrix Synapse.
|
|
//
|
|
// Synapse is configured with the synapse-s3-storage-provider media storage
|
|
// provider (flavor/ansible/vars.yml.j2:
|
|
// matrix_synapse_ext_synapse_s3_storage_provider_enabled=true,
|
|
// store_synchronous=true, config_endpoint_url pointed at the SeaweedFS S3
|
|
// gateway), so a media file a user uploads through Element's
|
|
// /_matrix/media/v3/upload endpoint is mirrored into the consumer bucket as a
|
|
// new object the moment Synapse stores it. The action signs the administrator
|
|
// into Element and sets a profile avatar; the avatar PNG is uploaded as a
|
|
// Synapse media object, and the shared check proves the bucket grew via the
|
|
// Filer UI.
|
|
//
|
|
// Required env (rendered by templates/playwright.env.j2):
|
|
// ELEMENT_BASE_URL, OIDC_ISSUER_URL, the admin login vars consumed by
|
|
// signInViaElement, and the SEAWEEDFS_* keys consumed by
|
|
// runSeaweedfsStorageCheck.
|
|
|
|
const { test, expect } = require("./onion-test");
|
|
const { resolveTimeout, isOnionTarget } = require("./timeouts");
|
|
const { skipUnlessServiceEnabled } = require("./service-gating");
|
|
const { runSeaweedfsStorageCheck } = require("./personas");
|
|
const shared = require("./_shared");
|
|
|
|
const AVATAR_PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
|
"base64",
|
|
);
|
|
|
|
test.use({ ignoreHTTPSErrors: true });
|
|
|
|
test("seaweedfs: an uploaded Matrix 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");
|
|
skipUnlessServiceEnabled("seaweedfs_frontend");
|
|
test.skip(
|
|
!(process.env.MATRIX_FLAVOR || "").toLowerCase().includes("ansible"),
|
|
"Matrix media is offloaded to SeaweedFS only in the ansible flavor; the compose flavor stores media on local disk, so the bucket never grows.",
|
|
);
|
|
test.setTimeout(resolveTimeout(600_000));
|
|
|
|
await runSeaweedfsStorageCheck(page, browser, {
|
|
label: "a Matrix profile avatar upload",
|
|
action: async (appPage) => {
|
|
await shared.installCspViolationObserver(appPage);
|
|
await shared.signInViaElement(
|
|
appPage,
|
|
shared.env.adminUsername,
|
|
shared.env.adminPassword,
|
|
"administrator",
|
|
);
|
|
|
|
const elementBaseUrl = shared.env.elementBaseUrl.replace(/\/$/, "");
|
|
await appPage.goto(`${elementBaseUrl}/#/settings`, { waitUntil: "domcontentloaded" });
|
|
|
|
const fileInput = appPage
|
|
.locator('input[type="file"][accept*="image" i], .mx_AvatarSetting input[type="file"], input[type="file"]')
|
|
.first();
|
|
await expect(
|
|
fileInput,
|
|
"Element user settings must expose a file input to set a profile avatar",
|
|
).toBeAttached({ timeout: resolveTimeout(60_000) });
|
|
|
|
const marker = `infinito-storage-check-${Date.now()}.png`;
|
|
const upload = appPage.waitForResponse(
|
|
(response) =>
|
|
/\/_matrix\/media\/(v3|r0)\/upload/.test(response.url()) &&
|
|
response.request().method() === "POST",
|
|
{ timeout: resolveTimeout(120_000) },
|
|
);
|
|
|
|
await fileInput.setInputFiles({
|
|
name: marker,
|
|
mimeType: "image/png",
|
|
buffer: AVATAR_PNG,
|
|
});
|
|
|
|
const saveButton = appPage
|
|
.getByRole("button", { name: /^(save|apply|upload|confirm)$/i })
|
|
.first();
|
|
if (await saveButton.waitFor({ state: "visible", timeout: resolveTimeout(30_000) }).then(() => true).catch(() => false)) {
|
|
await saveButton.click().catch(() => {});
|
|
}
|
|
|
|
const response = await upload.catch(() => null);
|
|
expect(
|
|
response,
|
|
"Element never POSTed the avatar to /_matrix/media/*/upload, so nothing could reach the bucket; " +
|
|
"the settings flow did not complete rather than the object store failing",
|
|
).not.toBeNull();
|
|
expect(
|
|
response.status(),
|
|
`Synapse rejected the avatar upload with HTTP ${response.status()}`,
|
|
).toBeLessThan(300);
|
|
},
|
|
});
|
|
});
|