102 lines
3.8 KiB
TypeScript
102 lines
3.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { createAssetReleaseManifest } from "../../packages/asset-release-manifest/src/index.js";
|
|
|
|
const releaseVersion = "asset-20260803.1";
|
|
const publicId = "7f0c9530-a7d9-4bf1-8c65-0e9298dd04ac";
|
|
const previewId = "ab18fd72-60e1-44e3-a9a0-3dfccb12e17c";
|
|
const privateId = "e3792605-5252-4d3b-a101-827408ab3515";
|
|
|
|
function fixture() {
|
|
return createAssetReleaseManifest({
|
|
items: [
|
|
{
|
|
access_class: "public_release_asset",
|
|
cache_kind: "thumbnail",
|
|
content: Buffer.from("public-content"),
|
|
mime_type: "image/png",
|
|
relative_path: "public/thumbnails/FLOWER001.png",
|
|
resource_id: publicId,
|
|
root_ref: "canonical-assets",
|
|
},
|
|
{
|
|
access_class: "internal_preview_asset",
|
|
content: Buffer.from("preview-content"),
|
|
mime_type: "image/webp",
|
|
relative_path: "preview/batch-7/FLOWER009.webp",
|
|
resource_id: previewId,
|
|
root_ref: "canonical-assets",
|
|
},
|
|
{
|
|
access_class: "private_user_asset",
|
|
content: Buffer.from("private-content"),
|
|
mime_type: "image/png",
|
|
owner_id: "owner-1",
|
|
relative_path: "private/owner-1/generated.png",
|
|
resource_id: privateId,
|
|
root_ref: "managed-assets",
|
|
},
|
|
],
|
|
release_version: releaseVersion,
|
|
});
|
|
}
|
|
|
|
describe("TASK-WP5-04 immutable asset release manifest", () => {
|
|
it("projects only the selected access class and never exposes source paths", () => {
|
|
const manifest = fixture();
|
|
const projected = manifest.project("public_release_asset", releaseVersion);
|
|
|
|
expect(Object.isFrozen(projected)).toBe(true);
|
|
expect(projected?.manifest_sha256).toMatch(/^[0-9a-f]{64}$/);
|
|
expect(projected?.items).toEqual([expect.objectContaining({
|
|
access_class: "public_release_asset",
|
|
resource_id: publicId,
|
|
sha256: createHash("sha256").update("public-content").digest("hex"),
|
|
url: `/api/v1/assets/public/${releaseVersion}/${publicId}`,
|
|
})]);
|
|
const serialized = JSON.stringify(projected);
|
|
expect(serialized).not.toContain(previewId);
|
|
expect(serialized).not.toContain(privateId);
|
|
expect(serialized).not.toContain("relative_path");
|
|
expect(serialized).not.toContain("root_ref");
|
|
expect(serialized).not.toMatch(/[A-Z]:\\\\/i);
|
|
});
|
|
|
|
it("keeps resource identifiers opaque, verifies file hashes, and rejects unsafe manifests", () => {
|
|
const manifest = fixture();
|
|
expect(manifest.read("public_release_asset", releaseVersion, publicId)?.bytes).toEqual(Buffer.from("public-content"));
|
|
expect(manifest.read("public_release_asset", releaseVersion, previewId)).toBeUndefined();
|
|
expect(manifest.project("private_user_asset", releaseVersion, { ownerId: "owner-1" })?.items)
|
|
.toEqual([expect.objectContaining({ resource_id: privateId })]);
|
|
expect(manifest.project("private_user_asset", releaseVersion, { ownerId: "owner-2" })?.items).toEqual([]);
|
|
|
|
expect(() => createAssetReleaseManifest({
|
|
items: [{
|
|
access_class: "public_release_asset",
|
|
cache_kind: "thumbnail",
|
|
content: Buffer.from("tampered"),
|
|
mime_type: "image/png",
|
|
relative_path: "public/tampered.png",
|
|
resource_id: publicId,
|
|
root_ref: "canonical-assets",
|
|
sha256: "0".repeat(64),
|
|
}],
|
|
release_version: releaseVersion,
|
|
})).toThrow(/sha-?256/i);
|
|
expect(() => createAssetReleaseManifest({
|
|
items: [{
|
|
access_class: "public_release_asset",
|
|
cache_kind: "thumbnail",
|
|
content: Buffer.from("unsafe"),
|
|
mime_type: "image/png",
|
|
relative_path: "/outside/private.png",
|
|
resource_id: publicId,
|
|
root_ref: "canonical-assets",
|
|
}],
|
|
release_version: releaseVersion,
|
|
})).toThrow(/relative path/i);
|
|
});
|
|
});
|