Files
Reactive-Resume/packages/api/src/features/resume/public-style-projection.test.ts
T
2026-07-30 12:39:15 +02:00

191 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { PublicStyleProjection } from "@reactive-resume/pdf/public-projection";
import { describe, expect, it, vi } from "vitest";
import { createPublicStyleProjection } from "@reactive-resume/pdf/public-projection";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { resumeDto } from "../../dto/resume";
import { getStyleProjection } from "./public-style-projection";
const source = {
languageVersion: 1,
text: "@version 1;\nname { color: #123456; }\n",
};
const buildResume = () => {
const data = structuredClone(defaultResumeData);
data.metadata.stylesheet = { mode: "semantic", source, applied: source };
return {
id: "resume-1",
userId: "owner-1",
name: "Private dashboard title",
slug: "resume",
data,
isPublic: true,
passwordHash: null,
};
};
const input = {
username: "jane",
slug: "resume",
requestHeaders: new Headers({ "x-forwarded-for": "203.0.113.7" }),
trustedClient: "203.0.113.9",
};
describe("getStyleProjection", () => {
it("authorizes before consuming budget or looking up a cached projection", async () => {
const consume = vi.fn();
const createProjection = vi.fn();
const cache = new Map<string, PublicStyleProjection>([
["cached", { renderDataHash: "cached" } as PublicStyleProjection],
]);
await expect(
getStyleProjection(input, {
findResume: vi.fn().mockResolvedValue({ ...buildResume(), isPublic: false }),
hasPasswordAccess: vi.fn(),
rateLimiter: { consume },
createProjection,
cache,
}),
).rejects.toMatchObject({ code: "NOT_FOUND" });
expect(consume).not.toHaveBeenCalled();
expect(createProjection).not.toHaveBeenCalled();
});
it("preserves password authorization before consuming the shared render budget", async () => {
const consume = vi.fn();
await expect(
getStyleProjection(input, {
findResume: vi.fn().mockResolvedValue({ ...buildResume(), passwordHash: "hash" }),
hasPasswordAccess: vi.fn().mockReturnValue(false),
rateLimiter: { consume },
createProjection: vi.fn(),
cache: new Map(),
}),
).rejects.toMatchObject({ code: "NEED_PASSWORD", status: 401 });
expect(consume).not.toHaveBeenCalled();
});
it("returns a source-free resolved projection and consumes one render token", async () => {
const consume = vi.fn();
const result = await getStyleProjection(input, {
findResume: vi.fn().mockResolvedValue(buildResume()),
hasPasswordAccess: vi.fn(),
rateLimiter: { consume },
createProjection: createPublicStyleProjection,
cache: new Map(),
});
expect(result).toEqual(expect.objectContaining({ formatVersion: 1, nodes: expect.any(Object) }));
expect(JSON.stringify(result)).not.toContain("@version");
expect(consume).toHaveBeenCalledOnce();
expect(consume).toHaveBeenCalledWith({ trustedClient: "203.0.113.9", resumeId: "resume-1" });
});
it("normalizes stored data before public projection", async () => {
const resume = buildResume();
resume.data.customSections = [
{
id: "custom-experience",
type: "experience",
title: "Experience",
icon: "",
columns: 1,
hidden: false,
keepTogether: false,
startOnNewPage: false,
items: [
{
id: "experience-item",
hidden: false,
company: "Analytical Engines",
position: "Programmer",
location: "London",
period: "18421843",
description: "<p>Wrote the first algorithm.</p>",
content: "<p>Compatible overlap</p>",
},
],
} as never,
];
const projection = {
formatVersion: 1,
languageVersion: 1,
semanticTreeVersion: 1,
registryFingerprint: "registry",
adapterFingerprint: "adapter",
renderDataHash: "render-hash",
nodes: {},
} satisfies PublicStyleProjection;
const createProjection = vi.fn().mockResolvedValue(projection);
await getStyleProjection(input, {
findResume: vi.fn().mockResolvedValue(resume),
hasPasswordAccess: vi.fn(),
rateLimiter: { consume: vi.fn() },
createProjection,
cache: new Map(),
});
expect(createProjection.mock.calls[0]?.[0].data.customSections[0]?.items[0]).toMatchObject({
content: "<p>Compatible overlap</p>",
roles: [],
website: { url: "", label: "", inlineLink: false },
});
});
it("caches only authorized projections by renderDataHash", async () => {
const projection = {
formatVersion: 1,
languageVersion: 1,
semanticTreeVersion: 1,
registryFingerprint: "registry",
adapterFingerprint: "adapter",
renderDataHash: "render-hash",
nodes: {},
} satisfies PublicStyleProjection;
const createProjection = vi
.fn()
.mockResolvedValueOnce(projection)
.mockResolvedValueOnce({ ...projection });
const cache = new Map<string, PublicStyleProjection>();
const dependencies = {
findResume: vi.fn().mockResolvedValue(buildResume()),
hasPasswordAccess: vi.fn(),
rateLimiter: { consume: vi.fn() },
createProjection,
cache,
};
const first = await getStyleProjection(input, dependencies);
const second = await getStyleProjection(input, dependencies);
expect(cache.get("render-hash")).toBe(first);
expect(second).toBe(first);
});
it("uses a strict source-free public projection DTO", async () => {
const projection = await createPublicStyleProjection({ data: buildResume().data });
expect(resumeDto.getStyleProjection.output.safeParse(projection).success).toBe(true);
expect(
resumeDto.getStyleProjection.output.safeParse({
...projection,
source,
}).success,
).toBe(false);
expect(
resumeDto.getStyleProjection.output.safeParse({
...projection,
nodes: {
...projection.nodes,
private: { diagnostics: [{ message: "source location" }] },
},
}).success,
).toBe(false);
});
});