mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
## 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>
264 lines
8.5 KiB
TypeScript
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);
|
|
});
|