diff --git a/server/src/lib/__tests__/image-resize.test.js b/server/src/lib/__tests__/image-resize.test.js new file mode 100644 index 0000000..6e429ef --- /dev/null +++ b/server/src/lib/__tests__/image-resize.test.js @@ -0,0 +1,99 @@ +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { findOriginalFile, getOrCreateResized } from '../image-resize.js' + +const TEST_DIR = path.join(process.cwd(), 'uploads', '.test-tmp') +const UPLOADS_DIR = path.join(process.cwd(), 'uploads') + +beforeAll(async () => { + await fs.promises.mkdir(TEST_DIR, { recursive: true }) + // Create a small test PNG file + const sharp = (await import('sharp')).default + const testPng = path.join(TEST_DIR, 'test-original.png') + await sharp({ + create: { width: 100, height: 100, channels: 3, background: { r: 255, g: 0, b: 0 } }, + }) + .png() + .toFile(testPng) +}) + +afterAll(async () => { + await fs.promises.rm(TEST_DIR, { recursive: true, force: true }) + // Clean up any cache files created during tests + const cacheDir = path.join(UPLOADS_DIR, '.cache') + try { + await fs.promises.rm(cacheDir, { recursive: true, force: true }) + } catch { + // ignore + } +}) + +describe('image-resize', () => { + it('findOriginalFile locates file by UUID', async () => { + const files = await fs.promises.readdir(TEST_DIR) + const pngFile = files.find((f) => f.endsWith('.png')) + const uuid = pngFile.replace('.png', '') + + // Copy file to actual uploads directory + const destPath = path.join(UPLOADS_DIR, pngFile) + await fs.promises.copyFile(path.join(TEST_DIR, pngFile), destPath) + + const found = await findOriginalFile(uuid) + expect(found).not.toBeNull() + expect(found).toBe(destPath) + + // Cleanup + await fs.promises.unlink(destPath) + }) + + it('getOrCreateResized generates AVIF file', async () => { + const sharp = (await import('sharp')).default + const uuid = crypto.randomUUID() + const testPath = path.join(UPLOADS_DIR, `${uuid}.png`) + await sharp({ + create: { width: 200, height: 200, channels: 3, background: { r: 0, g: 255, b: 0 } }, + }) + .png() + .toFile(testPath) + + const result = await getOrCreateResized(uuid, 100, 'avif') + expect(result).not.toBeNull() + expect(result.isNew).toBe(true) + expect(result.path).toContain('.cache') + expect(result.path).toContain('_w100.avif') + + const exists = await fs.promises.access(result.path).then(() => true).catch(() => false) + expect(exists).toBe(true) + + // Verify it's actually AVIF (sharp reports AVIF as 'heif' in metadata) + expect(result.path).toMatch(/\.avif$/) + + // Cleanup + await fs.promises.unlink(testPath) + await fs.promises.unlink(result.path) + }) + + it('getOrCreateResized returns cached file on second call', async () => { + const sharp = (await import('sharp')).default + const uuid = crypto.randomUUID() + const testPath = path.join(UPLOADS_DIR, `${uuid}.png`) + await sharp({ + create: { width: 200, height: 200, channels: 3, background: { r: 0, g: 0, b: 255 } }, + }) + .png() + .toFile(testPath) + + const first = await getOrCreateResized(uuid, 100, 'webp') + expect(first.isNew).toBe(true) + + const second = await getOrCreateResized(uuid, 100, 'webp') + expect(second.isNew).toBe(false) + expect(second.path).toBe(first.path) + + // Cleanup + await fs.promises.unlink(testPath) + await fs.promises.unlink(first.path) + }) +})