import { readFileSync } from 'node:fs'; import { cp, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { afterEach, describe, expect, it } from 'vitest'; import { assetUrl } from '@/content/asset-url'; import { loadProductionInputs } from '@/content/load-production-inputs'; import { parsePublicAssetManifest } from '@/content/schemas'; const fixtureUrl = (name: string) => pathToFileURL(resolve(process.cwd(), 'tests/fixtures', name)); const fixtureUrls = { valid: { contentJson: fixtureUrl('portfolio-content.valid.json'), assetManifestJson: fixtureUrl('asset-manifest.valid.json'), preparedAssetRoot: fixtureUrl('prepared-assets.valid/'), siteConfigJson: fixtureUrl('site-config.preview.json'), }, extraPreparedFile: { contentJson: fixtureUrl('portfolio-content.valid.json'), assetManifestJson: fixtureUrl('asset-manifest.valid.json'), preparedAssetRoot: fixtureUrl('prepared-assets.extra/'), siteConfigJson: fixtureUrl('site-config.preview.json'), }, }; const temporaryDirectories: string[] = []; afterEach(async () => { await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); }); describe('production inputs', () => { it('rejects an extra prepared file', async () => { await expect(loadProductionInputs(fixtureUrls.extraPreparedFile)).rejects.toThrow(/extra prepared asset/i); }); it('returns deeply frozen inputs', async () => { const inputs = await loadProductionInputs(fixtureUrls.valid); expect(Object.isFrozen(inputs.content.projects.selected)).toBe(true); expect(() => { (inputs.site as { buildId: string }).buildId = 'mutated'; }).toThrow(); }); it('rejects a symlinked prepared member', async () => { const directory = await mkdtemp(join(tmpdir(), 'portfolio-inputs-')); temporaryDirectories.push(directory); await cp(fixtureUrls.valid.preparedAssetRoot, directory, { recursive: true }); await rm(join(directory, 'selected-build-one.webp')); await symlink(join(directory, 'selected-build-two.webp'), join(directory, 'selected-build-one.webp')); await expect(loadProductionInputs({ ...fixtureUrls.valid, preparedAssetRoot: new URL(`file://${directory}/`) })) .rejects.toThrow(/symlink|special file/i); }); it('rejects duplicate JSON keys before schema validation', async () => { const directory = await mkdtemp(join(tmpdir(), 'portfolio-inputs-')); temporaryDirectories.push(directory); const contentPath = join(directory, 'content.json'); await writeFile(contentPath, '{"identity":{},"identity":{}}'); await expect(loadProductionInputs({ ...fixtureUrls.valid, contentJson: new URL(`file://${contentPath}`) })) .rejects.toThrow(/duplicate JSON key/i); }); it('resolves only an exact manifest asset ID to an encoded deterministic URL', () => { const manifest = parsePublicAssetManifest(JSON.parse(readFileSync(fixtureUrl('asset-manifest.valid.json'), 'utf8'))); expect(assetUrl(manifest, 'selected-build-one')).toBe('/assets/selected-build-one.webp'); expect(assetUrl(manifest, 'selected-build-one', '/assets/cards')).toBe('/assets/cards/selected-build-one.webp'); expect(() => assetUrl(manifest, 'missing')).toThrow(/unknown asset/i); }); it('rejects asset URL base traversal and origin escapes', () => { const manifest = parsePublicAssetManifest(JSON.parse(readFileSync(fixtureUrl('asset-manifest.valid.json'), 'utf8'))); for (const base of [ '//cdn.example.test/assets/', '/assets/../private/', '/assets\\private/', '/assets/%2e%2e/private/', '/assets/%2fprivate/', '/assets/?query=1', '/assets/#fragment', ]) { expect(() => assetUrl(manifest, 'selected-build-one', base)).toThrow(/asset url base/i); } }); it('rejects raw C0 controls and DEL in an asset URL base before URL parsing', () => { const manifest = parsePublicAssetManifest(JSON.parse(readFileSync(fixtureUrl('asset-manifest.valid.json'), 'utf8'))); for (const base of [ '/assets/\n../private/', '/assets/\nprivate/', '/assets/\tprivate/', '/assets/\rprivate/', '/assets/\u007fprivate/', ]) { expect(() => assetUrl(manifest, 'selected-build-one', base)).toThrow(/asset url base/i); } }); });