import { describe, expect, it } from "vitest"; import { TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS } from "../model-import-limits"; import type { ToolcraftModelAppearanceResource } from "../model-import-types"; import { createToolcraftModelAppearanceResourceRef, snapshotToolcraftModelAppearanceResources, } from "./model-appearance-resource"; import { createModelTexture, createValidModelDocument, createValidModelDocumentV2, } from "./model-document-test-support"; import { bytesToHex, sha256 } from "./sha256"; function appearanceResource( values: readonly number[], mimeType: ToolcraftModelAppearanceResource["mimeType"] = "image/png", ): ToolcraftModelAppearanceResource { const bytes = new Uint8Array(values); const contentDigest = `sha256:${bytesToHex(sha256(bytes))}`; return { bytes: bytes.buffer, contentDigest, mimeType, resourceRef: createToolcraftModelAppearanceResourceRef(contentDigest), }; } function documentFor(resources: readonly ToolcraftModelAppearanceResource[]) { return createValidModelDocumentV2({ textures: resources.map((resource, index) => createModelTexture({ contentDigest: resource.contentDigest, id: `texture-${index + 1}`, mimeType: resource.mimeType, resourceRef: resource.resourceRef, })), }); } describe("canonical model appearance resources", () => { it("verifies, owns, and deterministically sorts document-bound bytes", () => { const first = appearanceResource([137, 80, 78, 71, 1]); const second = appearanceResource([137, 80, 78, 71, 2]); const sourceFirstByte = new Uint8Array(first.bytes)[0]; const snapshot = snapshotToolcraftModelAppearanceResources( documentFor([first, second]), [second, first], TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, ); expect(snapshot.map(({ resourceRef }) => resourceRef)).toEqual( [first.resourceRef, second.resourceRef].sort(), ); expect(snapshot.every((resource) => Object.isFrozen(resource))).toBe(true); expect(snapshot[0]?.bytes).not.toBe( snapshot[0]?.resourceRef === first.resourceRef ? first.bytes : second.bytes, ); new Uint8Array(first.bytes)[0] = 0; const ownedFirst = snapshot.find( ({ resourceRef }) => resourceRef === first.resourceRef, ); expect(new Uint8Array(ownedFirst!.bytes)[0]).toBe(sourceFirstByte); }); it("rejects missing, unbound, duplicated, tampered, and oversized resources", () => { const resource = appearanceResource([137, 80, 78, 71]); const document = documentFor([resource]); const tampered = { ...resource, bytes: new Uint8Array([137, 80, 78, 70]).buffer, }; const unbound = appearanceResource([1, 2, 3, 4]); expect(() => snapshotToolcraftModelAppearanceResources( document, [], TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, )).toThrow(/missing appearance resources/u); expect(() => snapshotToolcraftModelAppearanceResources( document, [unbound], TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, )).toThrow(/not bound/u); expect(() => snapshotToolcraftModelAppearanceResources( document, [resource, resource], TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, )).toThrow(/not bound/u); expect(() => snapshotToolcraftModelAppearanceResources( document, [tampered], TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, )).toThrow(/digest verification/u); expect(() => snapshotToolcraftModelAppearanceResources( document, [resource], { maxTextureBytes: resource.bytes.byteLength - 1, maxTextureCount: 1 }, )).toThrow(/maxTextureBytes/u); }); it("keeps v1 geometry-only documents free of appearance resources", () => { const resource = appearanceResource([137, 80, 78, 71]); expect(snapshotToolcraftModelAppearanceResources( createValidModelDocument(), [], TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, )).toEqual([]); expect(() => snapshotToolcraftModelAppearanceResources( createValidModelDocument(), [resource], TOOLCRAFT_DEFAULT_MODEL_IMPORT_LIMITS, )).toThrow(/v1 documents/u); }); });