import { afterAll, describe, expect, test } from "bun:test"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import type { ArtOptions, ExportedGame } from "@playnite/contract"; import { artPaths, buildArtwork, fileToDataUrl } from "../src/domain/art.js"; const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pf-playnite-art-")); const write = (name: string, bytes: Buffer): string => { const p = path.join(dir, name); fs.writeFileSync(p, bytes); return p; }; afterAll(() => fs.rmSync(dir, { recursive: true, force: true })); const DEFAULT_ART: ArtOptions = { mode: "host", maxBytes: 1_500_000, includeBackground: false, }; const game = (over: Partial): ExportedGame => ({ id: "11111111-1111-1111-1111-111111111111", name: "Game", installed: true, hidden: false, source: "Steam", platforms: [], cover: null, background: null, icon: null, ...over, }); describe("fileToDataUrl", () => { test("encodes a small png as a data URL", () => { const p = write("cover.png", Buffer.from([1, 2, 3, 4])); expect(fileToDataUrl(p, 1000)).toBe( `data:image/png;base64,${Buffer.from([1, 2, 3, 4]).toString("base64")}`, ); }); test("drops files over the size cap", () => { const p = write("big.jpg", Buffer.alloc(2000, 7)); expect(fileToDataUrl(p, 1000)).toBeNull(); }); test("rejects unknown extensions and null", () => { const p = write("weird.xyz", Buffer.from([1])); expect(fileToDataUrl(p, 1000)).toBeNull(); expect(fileToDataUrl(null, 1000)).toBeNull(); }); test("mime follows the extension", () => { const p = write("c.jpeg", Buffer.from([9])); expect(fileToDataUrl(p, 1000)?.startsWith("data:image/jpeg;base64,")).toBe( true, ); }); test("a remote reference passes through untouched", () => { expect(fileToDataUrl("https://cdn.example/cover.jpg", 10)).toBe( "https://cdn.example/cover.jpg", ); }); }); describe("buildArtwork", () => { test("off mode yields no art", () => { const cover = write("c1.png", Buffer.from([1])); expect( buildArtwork(game({ cover }), { ...DEFAULT_ART, mode: "off" }), ).toBeUndefined(); }); test("host mode (default) emits the raw cover path — no bytes", () => { const cover = write("c2.png", Buffer.from([1, 2])); const art = buildArtwork(game({ cover }), DEFAULT_ART); expect(art?.portrait).toBe(cover); expect(art?.hero).toBeUndefined(); }); test("host mode includeBackground adds the hero path", () => { const cover = write("c2b.png", Buffer.from([1])); const background = write("b2b.jpg", Buffer.from([2])); const art = buildArtwork(game({ cover, background }), { ...DEFAULT_ART, includeBackground: true, }); expect(art?.portrait).toBe(cover); expect(art?.hero).toBe(background); }); test("host mode with no cover yields undefined", () => { expect(buildArtwork(game({ cover: null }), DEFAULT_ART)).toBeUndefined(); }); test("dataurl mode embeds the cover as a portrait data URL", () => { const cover = write("c3.png", Buffer.from([1, 2])); const art = buildArtwork(game({ cover }), { ...DEFAULT_ART, mode: "dataurl", }); expect(art?.portrait?.startsWith("data:image/png;base64,")).toBe(true); expect(art?.hero).toBeUndefined(); }); test("dataurl includeBackground adds a hero data URL", () => { const cover = write("c4.png", Buffer.from([1])); const background = write("b4.jpg", Buffer.from([2])); const art = buildArtwork(game({ cover, background }), { ...DEFAULT_ART, mode: "dataurl", includeBackground: true, }); expect(art?.portrait).toBeTruthy(); expect(art?.hero?.startsWith("data:image/jpeg;base64,")).toBe(true); }); test("dataurl mode with no readable art yields undefined", () => { expect( buildArtwork(game({ cover: "/does/not/exist.png" }), { ...DEFAULT_ART, mode: "dataurl", }), ).toBeUndefined(); }); }); describe("artPaths (the /api/art allow-list)", () => { const exp = (games: ExportedGame[]) => ({ schema: 1, generatedAt: "2026-01-01T00:00:00Z", playnite: { version: "10", mode: "Desktop" }, games, }); test("collects every local cover/background/icon the export references", () => { const paths = artPaths( exp([ game({ cover: "/a/cover.png", background: "/a/bg.jpg" }), game({ icon: "/a/icon.ico" }), ]), ); expect([...paths].sort()).toEqual([ "/a/bg.jpg", "/a/cover.png", "/a/icon.ico", ]); }); test("excludes remote references and unservable extensions", () => { const paths = artPaths( exp([ game({ cover: "https://cdn.example/cover.jpg" }), game({ cover: "/a/notes.txt" }), ]), ); expect(paths.size).toBe(0); }); test("a path the export never mentions is not allow-listed", () => { const paths = artPaths(exp([game({ cover: "/a/cover.png" })])); expect(paths.has("/etc/shadow")).toBe(false); }); });