fix: reject linked social preview outputs

This commit is contained in:
2026-09-04 23:20:20 +03:00
parent c60477a475
commit 4735ceffe5
2 changed files with 96 additions and 4 deletions

View File

@@ -320,6 +320,55 @@ describe('deterministic asset preparation', () => {
.rejects.toThrow(/SOCIAL-PREVIEW-PATH-001/u);
});
it('rejects a leaf symlink without changing its out-of-root target', async () => {
const root = await createRoot();
const outsideRoot = await mkdtemp(join(process.cwd(), '.portfolio-social-preview-test-'));
const targetPath = join(outsideRoot, 'protected-target.png');
const outputPath = join(root, 'social-preview.png');
const originalTarget = Buffer.from('outside target must remain unchanged');
let error: unknown;
let targetHashBefore = '';
let targetHashAfter = '';
try {
await writeFile(targetPath, originalTarget);
targetHashBefore = await hashFile(targetPath);
await symlink(targetPath, outputPath);
try {
await generateSocialPreview(outputPath);
} catch (caught) {
error = caught;
}
targetHashAfter = await hashFile(targetPath);
} finally {
await rm(outsideRoot, { recursive: true, force: true });
}
expect.soft(error).toBeInstanceOf(Error);
expect.soft((error as Error | undefined)?.message).toMatch(/SOCIAL-PREVIEW-PATH-001/u);
expect(targetHashAfter).toBe(targetHashBefore);
});
it('rejects a hard-linked destination without changing the linked file', async () => {
const root = await createRoot();
const targetPath = join(root, 'protected-target.png');
const outputPath = join(root, 'social-preview.png');
const originalTarget = Buffer.from('hard-linked target must remain unchanged');
await writeFile(targetPath, originalTarget);
const targetHashBefore = await hashFile(targetPath);
await link(targetPath, outputPath);
let error: unknown;
try {
await generateSocialPreview(outputPath);
} catch (caught) {
error = caught;
}
expect.soft(error).toBeInstanceOf(Error);
expect.soft((error as Error | undefined)?.message).toMatch(/SOCIAL-PREVIEW-PATH-001/u);
expect(await hashFile(targetPath)).toBe(targetHashBefore);
});
it('re-encodes images without metadata under generated safe filenames, identically across runs', async () => {
const root = await createRoot();
const inputs = await makeInputs(root, baseRecipe());