186 lines
7.6 KiB
JavaScript
186 lines
7.6 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;
|
|
assert(baseUrl && superuserEmail && superuserPassword, "Workshop integration test environment is incomplete");
|
|
|
|
const root = new PocketBase(baseUrl);
|
|
await root.collection("_superusers").authWithPassword(superuserEmail, superuserPassword);
|
|
const authMethods = await root.collection("users").listAuthMethods();
|
|
assert.equal(authMethods.oauth2.enabled, true, "GitHub OAuth must be configured from the runtime environment");
|
|
assert.equal(authMethods.password.enabled, false, "password authentication must remain disabled");
|
|
assert.deepEqual(
|
|
authMethods.oauth2.providers.map((provider) => provider.name),
|
|
["github"],
|
|
);
|
|
|
|
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 bannedRecord = await createUser("Banned", "member", "posting_banned");
|
|
const disabledRecord = await createUser("Disabled");
|
|
const moderatorRecord = await createUser("Moderator", "moderator");
|
|
const adminRecord = await createUser("Admin", "admin");
|
|
|
|
const member = await root.collection("users").impersonate(memberRecord.id, 3600);
|
|
const secondMember = await root.collection("users").impersonate(secondMemberRecord.id, 3600);
|
|
const banned = await root.collection("users").impersonate(bannedRecord.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");
|
|
|
|
const forgedMember = await member.collection("users").update(memberRecord.id, {
|
|
role: "admin",
|
|
state: "posting_banned",
|
|
moderationReason: "self escalation",
|
|
});
|
|
assert.equal(forgedMember.role, "member", "members must not change their own role");
|
|
assert.equal(forgedMember.state, "active", "members must not change their own account state");
|
|
assert.equal(forgedMember.moderationReason, "", "members must not change protected moderation fields");
|
|
|
|
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(
|
|
() => banned.collection("submissions").create({ type: "css", title: "Blocked theme", content: "body {}" }),
|
|
403,
|
|
);
|
|
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: "posting_banned", 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");
|
|
|
|
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");
|