Files
f069a85503 feat(admin): show reported message content in report detail (#3149)
## Summary

- include the reported event's complete stored content, author, creation
time, and deletion state in the admin report detail response
- resolve the event through a community-scoped join so an event ID
collision cannot cross tenant boundaries
- render the message only on report detail, with an explicit unavailable
state when retention has removed it
- preserve the existing report list contract so message bodies are not
returned during queue browsing

## Security

- the existing admin host/origin authorization runs before the detail
database read; a route test pins that ordering
- the target event is selected using both `events.community_id =
moderation_reports.community_id` and `events.id =
moderation_reports.target_event_id`
- the client supplies only the report UUID; it cannot choose a community
or arbitrary event ID
- soft-deleted content is visible only through this restricted admin
detail route and is labeled deleted
- responses retain the admin API's `no-store`, CSP, `nosniff`, frame
denial, and referrer policy middleware

## Testing

- `pnpm -C admin-web check`
- `pnpm -C admin-web test:e2e` (10 passed)
- `cargo test -p buzz-db` (84 passed, 130 ignored)
- `cargo clippy -p buzz-db -p buzz-relay --all-targets -- -D warnings`
- focused admin authorization tests
- pre-push Rust and desktop/Tauri suites passed

The new Postgres integration test is ignored under the repository
convention and will run when explicitly enabled against migrated
Postgres. Local Postgres and Redis were unavailable, so the full
`buzz-relay --lib` run had 8 existing infrastructure-dependent failures
after 749 tests passed.

---------

Signed-off-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Signed-off-by: Kalvin Chau <kalvin@block.xyz>
Signed-off-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
Co-authored-by: npub1dccv64krpcpse5cmkzfeh998cftungyatw3djt8jwdw6g43f7fyqzzmrf7 <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
2026-07-27 12:49:21 -07:00

319 lines
10 KiB
TypeScript

import { expect, test } from "@playwright/test";
test.beforeEach(async ({ page }) => {
await page.route("**/api/admin/v1/**", async (route) => {
await route.fulfill({ contentType: "application/json", body: "[]" });
});
});
for (const [path, heading] of [
["/reports", "Open reports"],
["/feedback", "Feedback"],
]) {
test(`${path} supports a deep link and empty state`, async ({ page }) => {
await page.goto(path);
await expect(page.getByRole("heading", { name: heading })).toBeVisible();
await expect(page.getByText("No records.")).toBeVisible();
});
}
test("forbidden reads have an explicit state", async ({ page }) => {
await page.route("**/api/admin/v1/reports?**", (route) =>
route.fulfill({
status: 403,
contentType: "application/json",
body: JSON.stringify({
error: { code: "forbidden", message: "request is not authorized" },
}),
}),
);
await page.goto("/reports");
await expect(
page.getByRole("heading", { name: "Access denied" }),
).toBeVisible();
});
test("report rows render the relay response contract", async ({ page }) => {
await page.route("**/api/admin/v1/reports?**", (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify([
{
id: "0e6caad8-1e18-4cd7-84fa-7264103f0a08",
communityId: "6d474feb-c50a-44e4-a0b5-f30532df49bc",
communityHost: "design.buzz.xyz",
reporterPubkey: "21".repeat(32),
targetKind: "event",
target: "12".repeat(32),
reportType: "spam",
status: "open",
createdAt: "2026-07-17T17:30:00Z",
},
]),
}),
);
await page.goto("/reports");
await expect(page.getByText("design.buzz.xyz")).toBeVisible();
await expect(page.getByText("spam")).toBeVisible();
await expect(page.getByText("Unknown date")).toHaveCount(0);
});
test("event report detail renders the reported message content", async ({
page,
}) => {
const id = "0e6caad8-1e18-4cd7-84fa-7264103f0a08";
await page.route(`**/api/admin/v1/reports/${id}`, (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify({
id,
communityId: "6d474feb-c50a-44e4-a0b5-f30532df49bc",
communityHost: "design.buzz.xyz",
reporterPubkey: "21".repeat(32),
targetKind: "event",
target: "12".repeat(32),
reportType: "spam",
status: "open",
createdAt: "2026-07-17T17:30:00Z",
message: {
authorPubkey: "31".repeat(32),
content:
"This is the complete reported message.\nIt preserves lines.",
createdAt: "2026-07-17T17:25:00Z",
deletedAt: null,
},
}),
}),
);
await page.goto(`/reports/${id}`);
await expect(
page.getByText("This is the complete reported message.", { exact: false }),
).toBeVisible();
await expect(page.getByText("31".repeat(32))).toBeVisible();
await expect(
page.getByText("Message content is unavailable", { exact: false }),
).toHaveCount(0);
});
test("event report detail explains when message content is unavailable", async ({
page,
}) => {
const id = "0e6caad8-1e18-4cd7-84fa-7264103f0a09";
await page.route(`**/api/admin/v1/reports/${id}`, (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify({
id,
communityId: "6d474feb-c50a-44e4-a0b5-f30532df49bc",
communityHost: "design.buzz.xyz",
reporterPubkey: "21".repeat(32),
targetKind: "event",
target: "12".repeat(32),
reportType: "spam",
status: "open",
createdAt: "2026-07-17T17:30:00Z",
message: null,
}),
}),
);
await page.goto(`/reports/${id}`);
await expect(
page.getByText("Message content is unavailable", { exact: false }),
).toBeVisible();
});
test("feedback cards open the complete submission", async ({ page }) => {
const id = "feed0000-0000-4000-8000-000000000001";
const fullBody = `${"Long feedback ".repeat(30)}end of feedback`;
await page.route(`**/api/admin/v1/feedback/${id}`, (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify({
id,
communityId: "6d474feb-c50a-44e4-a0b5-f30532df49bc",
communityHost: "design.buzz.xyz",
eventId: "31".repeat(32),
submitterPubkey: "21".repeat(32),
category: "needs-work",
body: fullBody,
tags: [],
eventCreatedAt: "2026-07-17T17:25:00Z",
receivedAt: "2026-07-17T17:30:00Z",
}),
}),
);
await page.route("**/api/admin/v1/feedback", (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify([
{
id,
communityId: "6d474feb-c50a-44e4-a0b5-f30532df49bc",
communityHost: "design.buzz.xyz",
submitterPubkey: "21".repeat(32),
category: "needs-work",
bodySummary: `${fullBody.slice(0, 240)}…`,
receivedAt: "2026-07-17T17:30:00Z",
},
]),
}),
);
await page.goto("/feedback");
const card = page.locator(".feedback-record");
await expect(card.locator(".record-provenance")).toContainText(
"design.buzz.xyz",
);
await card.locator(".feedback-main-link").click();
await expect(page).toHaveURL(`/feedback/${id}`);
await expect(
page.getByRole("heading", { name: "Feedback detail" }),
).toBeVisible();
await expect(
page.getByText("end of feedback", { exact: false }),
).toBeVisible();
});
test("feedback can be searched and filtered by community and time", async ({
page,
}) => {
const recent = new Date(Date.now() - 60 * 60 * 1000).toISOString();
const old = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString();
await page.route("**/api/admin/v1/feedback", (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify([
{
id: "recent",
communityId: "one",
communityHost: "design.buzz.xyz",
submitterPubkey: "21".repeat(32),
category: "bug",
bodySummary: "Composer freezes after sleep",
receivedAt: recent,
},
{
id: "old",
communityId: "two",
communityHost: "engineering.buzz.xyz",
submitterPubkey: "22".repeat(32),
category: "praise",
bodySummary: "Calls are much more reliable",
receivedAt: old,
},
]),
}),
);
await page.goto("/feedback");
await expect(page.getByText("2 of 2 submissions")).toBeVisible();
await page.getByRole("searchbox", { name: "Search feedback" }).fill("calls");
await expect(page.getByText("Calls are much more reliable")).toBeVisible();
await expect(page.getByText("Composer freezes after sleep")).toHaveCount(0);
await page.getByRole("searchbox", { name: "Search feedback" }).fill("");
await page.getByLabel("Community").selectOption("design.buzz.xyz");
await expect(page.getByText("Composer freezes after sleep")).toBeVisible();
await expect(page.getByText("Calls are much more reliable")).toHaveCount(0);
await page.getByLabel("Community").selectOption("all");
await page.getByLabel("Received").selectOption("day");
await expect(page.getByText("Composer freezes after sleep")).toBeVisible();
await expect(page.getByText("Calls are much more reliable")).toHaveCount(0);
});
test("feedback status is stored locally by feedback id", async ({ page }) => {
await page.route("**/api/admin/v1/feedback", (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify([
{
id: "feedback-one",
communityId: "one",
communityHost: "design.buzz.xyz",
submitterPubkey: "21".repeat(32),
category: "bug",
bodySummary: "Composer freezes after sleep",
receivedAt: new Date().toISOString(),
},
]),
}),
);
await page.goto("/feedback");
await page.getByRole("checkbox", { name: "Acted on" }).check();
await page.reload();
await expect(page.getByRole("checkbox", { name: "Acted on" })).toBeChecked();
await page.getByLabel("Status").selectOption("acted-on");
await expect(page.getByText("Composer freezes after sleep")).toBeVisible();
await page.getByLabel("Status").selectOption("pending");
await expect(page.getByText("No matching feedback.")).toBeVisible();
});
test("feedback attachments render from imeta without raw markdown", async ({
page,
}) => {
const id = "feedback-with-attachments";
const imageUrl = `https://design.buzz.xyz/media/${"a".repeat(64)}.png`;
const fileUrl = `https://design.buzz.xyz/media/${"b".repeat(64)}.txt`;
await page.route(`**/api/admin/v1/feedback/${id}`, (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify({
id,
communityId: "one",
communityHost: "design.buzz.xyz",
eventId: "31".repeat(32),
submitterPubkey: "21".repeat(32),
category: "bug",
body: `Composer froze.\n![image](${imageUrl})\n[diagnostics.txt](${fileUrl})`,
tags: [
[
"imeta",
`url ${imageUrl}`,
"m image/png",
`x ${"a".repeat(64)}`,
"size 48213",
"dim 1280x720",
"filename screenshot.png",
],
[
"imeta",
`url ${fileUrl}`,
"m text/plain",
`x ${"b".repeat(64)}`,
"size 391",
"filename diagnostics.txt",
],
],
eventCreatedAt: "2026-07-17T17:25:00Z",
receivedAt: "2026-07-17T17:30:00Z",
}),
}),
);
await page.goto(`/feedback/${id}`);
await expect(
page.getByText("Composer froze.", { exact: true }),
).toBeVisible();
await expect(page.getByText("![image]", { exact: false })).toHaveCount(0);
await expect(
page.getByRole("img", { name: "screenshot.png" }),
).toHaveAttribute(
"src",
`/api/admin/v1/feedback/${id}/attachments/${"a".repeat(64)}`,
);
await expect(
page.getByRole("link", { name: /diagnostics.txt/ }),
).toHaveAttribute(
"href",
`/api/admin/v1/feedback/${id}/attachments/${"b".repeat(64)}`,
);
const fileHeight = await page
.locator(".file-attachment")
.evaluate((element) => element.getBoundingClientRect().height);
expect(fileHeight).toBeLessThan(100);
});