CI / test (push) Successful in 1m11s
The header shows T5DE's logo (the client's, also the tab icon) and Online, Rooms, Products and Catalogs; the person's pages (Account, Library, Console for moderators) and signing out are in a menu under their picture: their first linked IMVU account's avatar picture, which /me now gives (kept an hour), or their initial. Creating a product moves to Products (New product, /products/new; /create redirects there), as creating a room is on Rooms. Co-Authored-By: Claude Opus 5.5 <[email protected]>
122 lines
5.5 KiB
TypeScript
122 lines
5.5 KiB
TypeScript
// The T5 badge on people: who shows it, as the proxy asks.
|
|
import { afterAll, beforeAll, expect, test } from "bun:test";
|
|
import { eq, openDatabase, schema, type Database } from "@t5de/db";
|
|
import { createApp } from "../src/app";
|
|
import { createAuth } from "../src/auth";
|
|
import { ImvuClient, type ImvuUser } from "../src/imvu";
|
|
|
|
const webURL = "http://web.test";
|
|
const accounts = new Map<number, ImvuUser>();
|
|
|
|
const fakeIMVU: typeof fetch = (async (input: RequestInfo | URL) => {
|
|
const url = new URL(String(input));
|
|
const u = [...accounts.values()].find(
|
|
(u) => url.pathname === `/user/user-${u.id}` || url.searchParams.get("username")?.toLowerCase() === u.username.toLowerCase(),
|
|
);
|
|
return Response.json({
|
|
denormalized: u
|
|
? { [`https://api.imvu.com/user/user-${u.id}`]: { data: { legacy_cid: u.id, username: u.username, tagline: u.tagline, is_ageverified: u.ageVerified, is_ap: u.accessPass } } }
|
|
: {},
|
|
});
|
|
}) as typeof fetch;
|
|
|
|
let db: Database;
|
|
let close: () => Promise<void>;
|
|
let base: string;
|
|
let stop: () => void;
|
|
|
|
beforeAll(async () => {
|
|
({ db, close } = await openDatabase("memory"));
|
|
const auth = createAuth(db, { apiURL: "http://api.test", webURL, secret: "test-secret-test-secret-test-secret-1234", rateLimit: false });
|
|
const hub = createApp({ db, auth, imvu: new ImvuClient("http://imvu.test", fakeIMVU) });
|
|
hub.app.listen({ port: 0, hostname: "127.0.0.1" });
|
|
const server = hub.app.server as unknown as { port: number; stop: () => void };
|
|
stop = () => server.stop();
|
|
base = `http://127.0.0.1:${server.port}`;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
stop();
|
|
await close();
|
|
});
|
|
|
|
type Person = { cookie: string; token: string };
|
|
|
|
async function call(p: Person | undefined, method: string, path: string, body?: unknown, proxy = false) {
|
|
const auth: Record<string, string> = p ? (proxy ? { Authorization: `Bearer ${p.token}` } : { Cookie: p.cookie }) : {};
|
|
const res = await fetch(base + path, {
|
|
method,
|
|
headers: { Origin: webURL, ...(body !== undefined ? { "Content-Type": "application/json" } : {}), ...auth },
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
});
|
|
const text = await res.text();
|
|
return { status: res.status, json: text ? JSON.parse(text) : undefined };
|
|
}
|
|
|
|
/** Signs a person up and links their IMVU accounts. */
|
|
async function person(name: string, ...ids: number[]): Promise<Person> {
|
|
const res = await fetch(base + "/api/auth/sign-up/email", {
|
|
method: "POST",
|
|
headers: { Origin: webURL, "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: `${name}@example.test`, password: "a long password", name, username: name }),
|
|
});
|
|
const p = {
|
|
cookie: res.headers.getSetCookie().map((c) => c.split(";")[0]).join("; "),
|
|
token: res.headers.get("set-auth-token") ?? "",
|
|
};
|
|
expect(p.token).not.toBe("");
|
|
for (const [i, id] of ids.entries()) {
|
|
const username = `${name}Av${i}`;
|
|
accounts.set(id, { id, username, tagline: "", ageVerified: false, accessPass: false, picture: null });
|
|
const started = await call(p, "POST", "/api/v1/account/links", { username });
|
|
accounts.get(id)!.tagline = started.json.challenge.code;
|
|
expect((await call(p, "POST", `/api/v1/account/links/${id}/check`)).status).toBe(200);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
const badges = async (p: Person | undefined, imvuIds: unknown) => call(p, "POST", "/api/v1/badges", { imvuIds }, true);
|
|
|
|
test("only linked accounts whose badge is on, of people who aren't banned, get the badge", async () => {
|
|
const admin = await person("badgeadmin");
|
|
const ann = await person("ann", 301, 302);
|
|
const bob = await person("bob", 303);
|
|
const asker = await person("asker");
|
|
|
|
// Off by default.
|
|
const account = (await call(ann, "GET", "/api/v1/account")).json;
|
|
expect(account.links.map((l: any) => l.showBadge)).toEqual([false, false]);
|
|
expect((await badges(asker, [301, 302, 303, 999])).json).toEqual({ badges: [] });
|
|
|
|
// Turned on for one account; the other stays off.
|
|
const patched = await call(ann, "PATCH", "/api/v1/account/links/301", { showBadge: true });
|
|
expect(patched.json.link.showBadge).toBe(true);
|
|
await call(bob, "PATCH", "/api/v1/account/links/303", { showBadge: true });
|
|
const got = await badges(asker, [301, 302, 303, 999, 301]);
|
|
expect(got.status).toBe(200);
|
|
expect(got.json.badges.sort()).toEqual([301, 303]);
|
|
expect(Object.keys(got.json)).toEqual(["badges"]);
|
|
|
|
// Turned off again.
|
|
await call(ann, "PATCH", "/api/v1/account/links/301", { showBadge: false });
|
|
expect((await badges(asker, [301, 303])).json).toEqual({ badges: [303] });
|
|
|
|
// A banned person's accounts don't.
|
|
const bobId = (await call(bob, "GET", "/api/v1/me")).json.person.id;
|
|
await db.update(schema.user).set({ banned: true }).where(eq(schema.user.id, bobId));
|
|
expect((await badges(asker, [303])).json).toEqual({ badges: [] });
|
|
|
|
expect((await badges(asker, [])).json).toEqual({ badges: [] });
|
|
expect((await badges(admin, [303])).status).toBe(200);
|
|
});
|
|
|
|
test("lookups are validated and need signing in", async () => {
|
|
const p = await person("carol");
|
|
expect((await badges(undefined, [1])).status).toBe(401);
|
|
expect((await badges(p, Array.from({ length: 201 }, (_, i) => i + 1))).status).toBe(400);
|
|
expect((await badges(p, Array.from({ length: 200 }, (_, i) => i + 1))).status).toBe(200);
|
|
for (const bad of [[0], [-1], [1.5], [Number.MAX_SAFE_INTEGER + 1], ["x"], null, "x", { id: 1 }])
|
|
expect(JSON.stringify(bad) + " " + (await badges(p, bad)).status).toBe(JSON.stringify(bad) + " 400");
|
|
expect((await call(p, "POST", "/api/v1/badges", {}, true)).status).toBe(400);
|
|
});
|