Files
homarr/apps/workshop/tests/workshop.integration.mjs
T

201 lines
7.8 KiB
JavaScript

import assert from "node:assert/strict";
import PocketBase, { ClientResponseError } from "pocketbase";
const baseUrl = process.env.WORKSHOP_TEST_URL;
const superuserEmail = process.env.WORKSHOP_TEST_SUPERUSER_EMAIL;
const superuserPassword = process.env.WORKSHOP_TEST_SUPERUSER_PASSWORD;
const verifyConfigurationOnly = process.env.WORKSHOP_VERIFY_CONFIGURATION_ONLY === "true";
assert(baseUrl && superuserEmail && superuserPassword, "Workshop integration test environment is incomplete");
const root = new PocketBase(baseUrl);
await root.collection("_superusers").authWithPassword(superuserEmail, superuserPassword);
if (!verifyConfigurationOnly) {
await root.collections.update("users", {
oauth2: {
enabled: true,
providers: [{ name: "github", clientId: "integration-client", clientSecret: "integration-secret" }],
},
});
}
const authMethods = await root.collection("users").listAuthMethods();
assert.equal(authMethods.oauth2.enabled, true, "GitHub OAuth must retain its persisted PocketBase configuration");
assert.equal(authMethods.password.enabled, false, "password authentication must remain disabled");
assert.deepEqual(
authMethods.oauth2.providers.map((provider) => provider.name),
["github"],
);
if (verifyConfigurationOnly) {
console.log("Workshop PocketBase configuration persisted across restart");
process.exit(0);
}
const createUser = async (name, role = "member", state = "active") => {
const password = `Workshop${name}Password123!`;
return root.collection("users").create({
email: `${name.toLowerCase()}@homarr.test`,
password,
passwordConfirm: password,
verified: true,
displayName: name,
role,
state,
});
};
const expectStatus = async (operation, status) => {
const statuses = Array.isArray(status) ? status : [status];
await assert.rejects(operation, (error) => error instanceof ClientResponseError && statuses.includes(error.status));
};
const memberRecord = await createUser("Member");
const secondMemberRecord = await createUser("SecondMember");
const disabledRecord = await createUser("Disabled");
const moderatorRecord = await createUser("Moderator", "moderator");
const adminRecord = await createUser("Admin", "admin");
await root.collection("workshop_staff").create({ user: moderatorRecord.id, role: "moderator" });
await root.collection("workshop_staff").create({ user: adminRecord.id, role: "admin" });
const member = await root.collection("users").impersonate(memberRecord.id, 3600);
const secondMember = await root.collection("users").impersonate(secondMemberRecord.id, 3600);
const disabled = await root.collection("users").impersonate(disabledRecord.id, 3600);
const moderator = await root.collection("users").impersonate(moderatorRecord.id, 3600);
const admin = await root.collection("users").impersonate(adminRecord.id, 3600);
assert.equal((await member.collection("users").getList()).totalItems, 0, "members must not list Workshop accounts");
await expectStatus(
() =>
member.collection("users").update(memberRecord.id, {
role: "admin",
state: "disabled",
moderationReason: "self escalation",
}),
[403, 404],
);
await admin.send(`/api/workshop/moderation/users/${disabledRecord.id}/state`, {
method: "POST",
body: { state: "disabled", reason: "Integration disable" },
});
const submission = await member.collection("submissions").create({
type: "widget",
title: "Integration widget",
description: "A backend integration fixture",
schemaVersion: "forged-version",
content: JSON.stringify({
$schema: "homarr-custom-widget-v2",
name: "Integration widget",
url: "https://example.com/status",
authType: "none",
method: "GET",
displayType: "raw",
displayConfig: { type: "raw" },
integrationTestPadding: "x".repeat(150_000),
}),
author: secondMemberRecord.id,
authorName: "Forged",
revision: 99,
});
assert.equal(submission.author, memberRecord.id, "server must enforce ownership");
assert.equal(submission.authorName, "Member", "server must snapshot the authenticated author name");
assert.equal(submission.revision, 1, "server must own the initial revision");
assert.equal(submission.schemaVersion, "homarr-custom-widget-v2", "server must own schemaVersion");
await expectStatus(() => disabled.collection("votes").create({ submission: submission.id, value: 1 }), [400, 403]);
await expectStatus(
() =>
member.collection("submissions").create({
type: "widget",
title: "Invalid widget",
content: JSON.stringify({ $schema: "homarr-custom-widget-v2", name: "Missing contract" }),
}),
400,
);
const vote = await member.collection("votes").create({ submission: submission.id, value: 1 });
await expectStatus(() => member.collection("votes").create({ submission: submission.id, value: -1 }), 400);
assert.equal(
(await member.collection("votes").update(vote.id, { submission: "forged", user: "forged", value: -1 })).submission,
submission.id,
);
const report = await secondMember.collection("reports").create({
submission: submission.id,
reporter: memberRecord.id,
category: "spam",
explanation: "Integration report",
status: "dismissed",
});
assert.equal(report.reporter, secondMemberRecord.id);
assert.equal(report.status, "open");
assert.equal((await member.collection("reports").getList()).totalItems, 0, "reports must remain private from members");
assert.equal((await moderator.collection("reports").getList()).items.length, 1);
await expectStatus(
() =>
member.send(`/api/workshop/moderation/users/${secondMemberRecord.id}/state`, {
method: "POST",
body: { state: "disabled", reason: "No permission" },
}),
403,
);
await expectStatus(
() =>
moderator.send(`/api/workshop/moderation/users/${adminRecord.id}/state`, {
method: "POST",
body: { state: "disabled", reason: "Cannot alter admin" },
}),
403,
);
await expectStatus(
() =>
moderator.send(`/api/workshop/moderation/users/${secondMemberRecord.id}/role`, {
method: "POST",
body: { role: "moderator", reason: "Cannot promote" },
}),
403,
);
await admin.send(`/api/workshop/moderation/users/${secondMemberRecord.id}/role`, {
method: "POST",
body: { role: "moderator", reason: "Integration promotion" },
});
assert.equal((await root.collection("users").getOne(secondMemberRecord.id)).role, "moderator");
assert.equal(
(await root.collection("workshop_staff").getFirstListItem(`user = '${secondMemberRecord.id}'`)).role,
"moderator",
);
await moderator.send(`/api/workshop/moderation/reports/${report.id}/resolve`, {
method: "POST",
body: { status: "resolved", reason: "Integration resolution" },
});
assert.equal((await root.collection("reports").getOne(report.id)).status, "resolved");
await moderator.send(`/api/workshop/moderation/submissions/${submission.id}/remove`, {
method: "POST",
body: { reason: "Integration removal" },
});
await expectStatus(() => root.collection("submissions").getOne(submission.id), 404);
await expectStatus(() => root.collection("votes").getOne(vote.id), 404);
await expectStatus(() => root.collection("reports").getOne(report.id), 404);
const actions = await root.collection("moderation_actions").getFullList({ sort: "created" });
assert.deepEqual(
actions.map((action) => action.action),
["set_account_state", "set_role", "resolve_report", "remove_submission"],
);
assert(actions.at(-1)?.snapshot.includes("Integration widget"), "removal audit must retain a target snapshot");
assert(!actions.at(-1)?.snapshot.includes("integrationTestPadding"), "removal audit must omit large source content");
assert(actions.at(-1)?.snapshot.includes('"contentLength"'), "removal audit must retain the source length");
const anonymous = new PocketBase(baseUrl);
const listings = await anonymous.collection("workshop_listings").getList();
assert.equal(listings.totalItems, 0);
await expectStatus(() => anonymous.collection("submissions").getList(), 403);
console.log("Workshop backend integration matrix passed");