Files
buzz/desktop/tests/e2e/add-community-screenshots.spec.ts
Kalvin C 8f66f7051c feat: raise hosted community limit to fifty (#8186)
## Summary
- Raise the stock relay active ownership quota from 5 to 50 for creation
and ownership transfer. Raise the absolute lifetime quota from 20 to 50
reserved hosts; completed owner deletions still permanently consume
lifetime capacity to bound create/delete host squatting.
- Honor the owner-list `can_create` projection across Desktop settings,
create flow, and onboarding instead of imposing a client-side count.
Display server `quota_used` / `quota_limit` when available; when the
projection is missing, let the server's `limit_reached` decide.
- Cover 49→50 and 50→51 admissions, deleted-owner lifetime exhaustion,
and rendered create gates under 5/50, 0/50 exhausted, and missing
projections.

## Verification
- Desktop full `pnpm test` and `pnpm typecheck` pass after the 50-cap
update; `cargo fmt --all -- --check` passes. Four targeted Playwright
cases and `pnpm build:e2e` passed for the original dynamic-quota change
at the previous commit, before changing test fixtures from 20 to 50.
- `cargo test -p buzz-db --lib` cannot compile here: macOS dyld rejects
the `sqlx_macros` dylib with `mis-aligned LINKEDIT string pool` on this
host before the crate's tests run. CI must validate the DB package.

## Rollout / manual validation
- Confirm `BUZZ_MAX_COMMUNITIES_PER_OWNER` is unset or 50 on **every**
production relay replica; this per-process cached override takes
precedence over the stock default.
- Confirm Builderlab's list/create/transfer proxy passes the relay's
`quota_used`, `quota_limit`, `can_create`, and `limit_reached` without
its own 5-community gate. Its backing implementation is not in this repo
and was not verified here.
- Human test: with an owner at 49 live communities, create the fiftieth,
verify the fifty-first returns `limit_reached`; transfer into an owner
at 49 then at 50 and confirm the same boundary. Check a deleted-owner
account at 0 active/50 lifetime displays the generic limit and disables
creation. This manual check remains outstanding.

---------

Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz>
2026-10-08 15:40:20 -07:00

264 lines
8.5 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { npubEncode } from "nostr-tools/nip19";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
const OUTDIR = "test-results/add-community";
const DEFAULT_MOCK_PUBKEY = "deadbeef".repeat(8);
const COMMUNITIES = [
{
id: "ws-a",
name: "Alpha",
relayUrl: "ws://localhost:3000",
addedAt: "2026-01-01T00:00:00.000Z",
},
{
id: "ws-b",
name: "Bravo",
relayUrl: "ws://localhost:3001",
addedAt: "2026-01-02T00:00:00.000Z",
},
];
test.beforeEach(async ({ page }) => {
await page.addInitScript((communities) => {
window.localStorage.setItem(
"buzz-communities",
JSON.stringify(communities),
);
window.localStorage.setItem("buzz-active-community-id", communities[0].id);
}, COMMUNITIES);
await installMockBridge(
page,
{
builderlabAuth: {
email: "owner@example.com",
expiresAt: "2099-01-01T00:00:00Z",
},
builderlabIdentity: { pubkey_hex: DEFAULT_MOCK_PUBKEY },
},
{
skipCommunitySeed: true,
},
);
await page.goto("/");
await page.getByTestId("community-rail-add").click();
});
test("identity: npub-only account key is recovery, never a ready create flow", async ({
page,
}) => {
await installMockBridge(
page,
{
builderlabAuth: {
email: "old-owner@example.com",
expiresAt: "2099-01-01T00:00:00Z",
},
builderlabIdentity: {
// The identity object is present but carries no authoritative
// pubkey_hex — only the independent, unverified server npub.
npub: npubEncode("b".repeat(64)),
},
},
{ skipCommunitySeed: true },
);
await page.reload();
await page.getByTestId("community-rail-add").click();
await page.getByTestId("add-community-create").click();
// Presence of the identity object must not read as a ready account: the
// mismatch recovery branch owns the create tab instead.
await expect(
page.getByText("This Builderlab account uses a different Buzz identity."),
).toBeVisible();
await expect(page.getByText("Account: Unavailable")).toBeVisible();
await expect(
page.getByText(`This device: ${npubEncode(DEFAULT_MOCK_PUBKEY)}`),
).toBeVisible();
// The create form — and with it the create action — stays unrendered.
await expect(page.getByTestId("hosted-community-create-name")).toHaveCount(0);
await expect(page.getByTestId("hosted-community-create-submit")).toHaveCount(
0,
);
// The unverified hosted npub spelling never renders.
await expect(page.getByText(npubEncode("b".repeat(64)))).toHaveCount(0);
// Recovery stays explicit.
await expect(
page.getByRole("button", { name: "Use this device" }),
).toBeVisible();
});
test("identity: padded same-key hex is a ready create flow, not a mismatch", async ({
page,
}) => {
await installMockBridge(
page,
{
builderlabAuth: {
email: "owner@example.com",
expiresAt: "2099-01-01T00:00:00Z",
},
builderlabIdentity: {
// This device's own key, padded and uppercased: the same key
// after normalization, so the account is ready — never a mismatch
// demanding a delete/rebind of the identity it already holds.
pubkey_hex: ` ${DEFAULT_MOCK_PUBKEY.toUpperCase()} `,
},
},
{ skipCommunitySeed: true },
);
await page.reload();
await page.getByTestId("community-rail-add").click();
await page.getByTestId("add-community-create").click();
// The create form — and with it the create action — is available.
await expect(page.getByTestId("hosted-community-create-name")).toBeVisible();
await expect(
page.getByText("This Builderlab account uses a different Buzz identity."),
).toHaveCount(0);
});
test("create flow follows server permission with five owned communities", async ({
page,
}) => {
await installMockBridge(
page,
{
builderlabAuth: {
email: "owner@example.com",
expiresAt: "2099-01-01T00:00:00Z",
},
builderlabIdentity: { pubkey_hex: DEFAULT_MOCK_PUBKEY },
builderlabCommunities: Array.from({ length: 5 }, (_, index) => ({
id: `owned-${index}`,
})),
builderlabQuota: { quota_used: 5, quota_limit: 50, can_create: true },
},
{ skipCommunitySeed: true },
);
await page.reload();
await page.getByTestId("community-rail-add").click();
await page.getByTestId("add-community-create").click();
await page.getByTestId("hosted-community-create-name").fill("new-team");
await expect(
page.getByTestId("hosted-community-create-submit"),
).toBeEnabled();
});
test("create flow blocks when lifetime quota is exhausted", async ({
page,
}) => {
await installMockBridge(
page,
{
builderlabAuth: {
email: "owner@example.com",
expiresAt: "2099-01-01T00:00:00Z",
},
builderlabIdentity: { pubkey_hex: DEFAULT_MOCK_PUBKEY },
builderlabCommunities: [],
builderlabQuota: { quota_used: 0, quota_limit: 50, can_create: false },
},
{ skipCommunitySeed: true },
);
await page.reload();
await page.getByTestId("community-rail-add").click();
await page.getByTestId("add-community-create").click();
await expect(page.getByTestId("hosted-community-create-name")).toBeDisabled();
await expect(
page.getByTestId("hosted-community-create-submit"),
).toBeDisabled();
await expect(
page.getByText(/reached your hosted community creation limit/),
).toBeVisible();
});
test("capture: add-community choices", async ({ page }) => {
const dialog = page.getByTestId("add-community-dialog");
await dialog.waitFor();
await waitForAnimations(page);
await dialog.screenshot({ path: `${OUTDIR}/01-choices.png` });
});
test("capture: join an existing community", async ({ page }) => {
await page.getByTestId("add-community-join").click();
const dialog = page.getByTestId("add-community-dialog");
const communityUrl = page.getByLabel("Community URL or invite link");
await communityUrl.fill("community.example.com");
await page.getByTestId("community-api-token-reveal").waitFor({
state: "detached",
});
await waitForAnimations(page);
await dialog.screenshot({ path: `${OUTDIR}/02-join.png` });
});
test("capture: create a new community", async ({ page }) => {
await page.getByTestId("add-community-create").click();
const dialog = page.getByTestId("add-community-dialog");
await page.getByLabel("Community address").waitFor();
await waitForAnimations(page);
await dialog.screenshot({ path: `${OUTDIR}/03-create.png` });
});
test("identity: create owner shows the bound key's npub, never the hosted npub or raw hex", async ({
page,
}) => {
await installMockBridge(
page,
{
builderlabAuth: {
email: "old-owner@example.com",
expiresAt: "2099-01-01T00:00:00Z",
},
builderlabIdentity: {
pubkey_hex: "f".repeat(64),
npub: npubEncode("b".repeat(64)),
},
},
{ skipCommunitySeed: true },
);
await page.reload();
await page.getByTestId("community-rail-add").click();
await page.getByTestId("add-community-create").click();
await expect(
page.getByText("This Builderlab account uses a different Buzz identity."),
).toBeVisible();
await expect(
page.getByText(`Account: ${npubEncode("f".repeat(64))}`),
).toBeVisible();
await expect(
page.getByText(`This device: ${npubEncode(DEFAULT_MOCK_PUBKEY)}`),
).toBeVisible();
await expect(page.getByText(npubEncode("b".repeat(64)))).toHaveCount(0);
await expect(page.getByText("f".repeat(64))).toHaveCount(0);
});
test("identity: create owner renders the neutral label when the bound hex is unusable", async ({
page,
}) => {
await installMockBridge(
page,
{
builderlabAuth: {
email: "old-owner@example.com",
expiresAt: "2099-01-01T00:00:00Z",
},
builderlabIdentity: {
// Valid hex alphabet, wrong length — unusable as an identity key.
pubkey_hex: "f".repeat(63),
npub: npubEncode("b".repeat(64)),
},
},
{ skipCommunitySeed: true },
);
await page.reload();
await page.getByTestId("community-rail-add").click();
await page.getByTestId("add-community-create").click();
await expect(
page.getByText("This Builderlab account uses a different Buzz identity."),
).toBeVisible();
await expect(page.getByText("Account: Unavailable")).toBeVisible();
await expect(page.getByText(npubEncode("b".repeat(64)))).toHaveCount(0);
await expect(page.getByText("f".repeat(63))).toHaveCount(0);
});