101 lines
5.2 KiB
TypeScript
101 lines
5.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { UserAvatar } from "../src/components/user-avatar.tsx";
|
|
import {
|
|
BEAM_AVATAR_RENDERER_VERSION,
|
|
beamAvatarPalettes,
|
|
beamAvatarPatchSchema,
|
|
beamAvatarSchema,
|
|
createBeamAvatarProfilePatch,
|
|
} from "../src/lib/beam-avatar.ts";
|
|
|
|
const readProjectFile = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
|
|
const accountRoute = readProjectFile("src/app/api/account/route.ts");
|
|
const avatarRoute = readProjectFile("src/app/api/account/avatar/route.ts");
|
|
const avatarComponent = readProjectFile("src/components/user-avatar.tsx");
|
|
const sidebar = readProjectFile("src/components/app-sidebar.tsx");
|
|
const page = readProjectFile("src/app/page.tsx");
|
|
const migration = readProjectFile("db/migrations/20260807010000_profile_beam_avatars.sql");
|
|
|
|
const seed = "11111111-1111-4111-8111-111111111111";
|
|
|
|
test("defines eight reviewed Beam palettes and versioned persisted parameters", () => {
|
|
assert.equal(beamAvatarPalettes.length, 8);
|
|
assert.equal(new Set(beamAvatarPalettes.map((palette) => palette.name)).size, 8);
|
|
assert.equal(beamAvatarPalettes.every((palette) => palette.colors.length === 5), true);
|
|
assert.deepEqual(beamAvatarSchema.parse({
|
|
seed,
|
|
palette: 7,
|
|
rendererVersion: BEAM_AVATAR_RENDERER_VERSION,
|
|
}), { seed, palette: 7, rendererVersion: 1 });
|
|
assert.equal(beamAvatarSchema.safeParse({ seed, palette: 8, rendererVersion: 1 }).success, false);
|
|
assert.equal(beamAvatarSchema.safeParse({ seed: "email@example.test", palette: 0, rendererVersion: 1 }).success, false);
|
|
});
|
|
|
|
test("renders identical Beam SVG for identical persisted parameters", () => {
|
|
const avatar = { seed, palette: 0, rendererVersion: 1 } as const;
|
|
const first = renderToStaticMarkup(createElement(UserAvatar, { avatar, size: 40 }));
|
|
const second = renderToStaticMarkup(createElement(UserAvatar, { avatar, size: 40 }));
|
|
|
|
assert.equal(first, second);
|
|
assert.match(first, /<svg/);
|
|
assert.match(first, /width="40"/);
|
|
assert.match(first, /height="40"/);
|
|
});
|
|
|
|
test("accepts only bounded avatar actions and creates new seeds on the server", () => {
|
|
assert.equal(beamAvatarPatchSchema.safeParse({ action: "set_palette", palette: 3 }).success, true);
|
|
assert.equal(beamAvatarPatchSchema.safeParse({ action: "randomize" }).success, true);
|
|
assert.equal(beamAvatarPatchSchema.safeParse({ action: "randomize", seed }).success, false);
|
|
assert.equal(beamAvatarPatchSchema.safeParse({ action: "upload" }).success, false);
|
|
|
|
assert.deepEqual(
|
|
createBeamAvatarProfilePatch({ action: "set_palette", palette: 3 }, () => seed),
|
|
{ avatar_palette: 3, avatar_renderer_version: 1 },
|
|
);
|
|
assert.deepEqual(
|
|
createBeamAvatarProfilePatch({ action: "randomize" }, () => seed),
|
|
{ avatar_renderer_version: 1, avatar_seed: seed },
|
|
);
|
|
});
|
|
|
|
test("persists random defaults and constraints only for self-hosted profiles", () => {
|
|
assert.match(migration, /to_regclass\('identity\.users'\) is not null/);
|
|
assert.match(migration, /avatar_seed uuid not null default gen_random_uuid\(\)/);
|
|
assert.match(migration, /avatar_palette smallint not null[\s\S]*floor\(random\(\) \* 8\)/);
|
|
assert.match(migration, /check \(avatar_palette between 0 and 7\)/);
|
|
assert.match(migration, /check \(avatar_renderer_version = 1\)/);
|
|
assert.match(migration, /grant update \([\s\S]*avatar_seed[\s\S]*avatar_palette[\s\S]*avatar_renderer_version[\s\S]*\) on table public\.profiles to authenticated/);
|
|
assert.doesNotMatch(migration, /bytea|storage|bucket|upload/i);
|
|
});
|
|
|
|
test("account APIs expose and update Beam parameters through authenticated staging data", () => {
|
|
assert.match(accountRoute, /AUTH_PROVIDER\?\.trim\(\) === "self-hosted"/);
|
|
assert.match(accountRoute, /select\("avatar_seed,avatar_palette,avatar_renderer_version"\)/);
|
|
assert.match(accountRoute, /avatar,/);
|
|
|
|
assert.match(avatarRoute, /AUTH_PROVIDER\?\.trim\(\) === "self-hosted"/);
|
|
assert.match(avatarRoute, /createServerSupabaseClient\(\)/);
|
|
assert.match(avatarRoute, /auth\.getUser\(\)/);
|
|
assert.match(avatarRoute, /beamAvatarPatchSchema\.safeParse/);
|
|
assert.match(avatarRoute, /createBeamAvatarProfilePatch\(parsed\.data\)/);
|
|
assert.match(avatarRoute, /\.eq\("id", user\.id\)/);
|
|
assert.doesNotMatch(avatarRoute, /createAdminSupabaseClient|SUPABASE_SERVICE_ROLE_KEY|upload|bytea/i);
|
|
});
|
|
|
|
test("renders the persisted Beam avatar in the sidebar, account menu, and profile editor", () => {
|
|
assert.match(avatarComponent, /from "boring-avatars"/);
|
|
assert.match(avatarComponent, /variant="beam"/);
|
|
assert.match(avatarComponent, /name=\{avatar\.seed\}/);
|
|
assert.match(sidebar, /<UserAvatar avatar=\{account\.avatar\} size=\{32\}/);
|
|
assert.match(sidebar, /<UserAvatar avatar=\{account\.avatar\} size=\{40\}/);
|
|
assert.match(page, /Beam 形象由随机种子生成/);
|
|
assert.match(page, /role="radiogroup" aria-label="头像配色"/);
|
|
assert.match(page, /persistAvatar\(\{ action: "set_palette", palette: index \}\)/);
|
|
assert.match(page, /persistAvatar\(\{ action: "randomize" \}\)/);
|
|
assert.doesNotMatch(page, /avatar[^\n]{0,80}(?:file|upload|上传图片)/i);
|
|
});
|