import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import { UserAvatar } from "../src/components/layout/user-avatar";
import { resolveUserAvatarURL } from "../src/lib/user-avatar-url";
import { DEFAULT_USER_AVATARS, isDefaultUserAvatarID } from "../src/lib/default-user-avatars";
import { apiBaseURL } from "../src/services/api/request";
import type { LocalUser } from "../src/stores/use-user-store";
const user: LocalUser = { id: "user-1", username: "user", displayName: "User", role: "user", status: "active" };
function renderAvatar(avatarUrl?: string) {
return renderToStaticMarkup();
}
describe("shared user avatar image sources", () => {
test("renders authenticated profile images and linked HTTP avatars", () => {
for (const source of ["/api/auth/profile/avatar/user-1?v=012abc", "https://example.com/avatar.png", "http://example.com/avatar.jpg"]) {
const markup = renderAvatar(source);
expect(markup).toContain(`
{
for (const source of [
undefined,
"",
"/assets/avatar.png",
"/api/auth/profile/avatar/../settings",
"/api/auth/profile/avatar/user-1/../../settings",
"/api/auth/profile/avatar/user-1?redirect=https://example.com",
"//example.com/avatar.png",
"data:image/svg+xml,test",
"javascript:alert(1)",
"blob:https://example.com/test",
]) {
const markup = renderAvatar(source);
expect(markup).not.toContain("
{
const markup = renderToStaticMarkup();
expect(markup).toContain("app-workspace-account-icon");
expect(markup).not.toContain("
{
const canonical = "/api/auth/profile/avatar/user-1?v=012abc";
test("resolves the canonical path onto API origins and proxy prefixes", () => {
for (const [base, expected] of [
["/api", canonical],
["/api/", canonical],
["/proxy/yingce/api", "/proxy/yingce/api/auth/profile/avatar/user-1?v=012abc"],
["/proxy/yingce/api///", "/proxy/yingce/api/auth/profile/avatar/user-1?v=012abc"],
["https://backend.example.com/api", "https://backend.example.com/api/auth/profile/avatar/user-1?v=012abc"],
["https://backend.example.com/proxy/yingce/", "https://backend.example.com/proxy/yingce/auth/profile/avatar/user-1?v=012abc"],
["http://localhost:8080", "http://localhost:8080/auth/profile/avatar/user-1?v=012abc"],
["/", "/auth/profile/avatar/user-1?v=012abc"],
]) {
expect(resolveUserAvatarURL(canonical, base)).toBe(expected);
}
});
test("keeps third-party URLs independent of the configured API base", () => {
for (const source of ["https://thirdparty.example.com/avatar.png?q=123", "http://thirdparty.example.com/avatar.jpg"]) {
expect(resolveUserAvatarURL(source, "https://backend.example.com/proxy/api")).toBe(source);
}
});
test("rejects noncanonical paths before joining with a custom backend", () => {
for (const source of [
undefined,
"",
"/api/auth/profile/avatar/user-1",
"/api/auth/profile/avatar/user-1?v=",
"/api/auth/profile/avatar/user-1?v=012abc&next=/settings",
"/api/auth/profile/avatar/%2e%2e?v=012abc",
"/api/auth/profile/avatar/user-1/../other?v=012abc",
"/api/auth/profile/avatar/user-1?v=012abc#fragment",
"/proxy/api/auth/profile/avatar/user-1?v=012abc",
"//thirdparty.example.com/avatar.png",
"data:image/png;base64,AAAA",
]) {
expect(resolveUserAvatarURL(source, "https://backend.example.com/proxy/api")).toBe("");
}
});
});
describe("system avatar catalog and shared rendering", () => {
test("exposes eight stable distinct presets and validates exact IDs", () => {
expect(DEFAULT_USER_AVATARS.map((avatar) => avatar.id)).toEqual(["person", "director", "camera", "cat", "rabbit", "robot", "rocket", "sparkles"]);
expect(new Set(DEFAULT_USER_AVATARS.map((avatar) => avatar.id)).size).toBe(8);
for (const avatar of DEFAULT_USER_AVATARS) {
expect(isDefaultUserAvatarID(avatar.id)).toBe(true);
expect(avatar.label).not.toBe("");
}
for (const invalid of [undefined, null, "", "Cat", " cat", "cat ", "default", "../person"]) {
expect(isDefaultUserAvatarID(invalid)).toBe(false);
}
});
test("renders the selected icon globally and overrides inherited image URLs", () => {
const iconClasses = ["lucide-circle-user-round", "lucide-clapperboard", "lucide-camera", "lucide-cat", "lucide-rabbit", "lucide-bot", "lucide-rocket", "lucide-sparkles"];
for (const [index, preset] of DEFAULT_USER_AVATARS.entries()) {
const markup = renderToStaticMarkup(
,
);
expect(markup).not.toContain("
{
const markup = renderToStaticMarkup();
expect(markup).toContain("
);
expect(legacyDefault).toContain("lucide-circle-user-round");
});
});