import express from 'express'; import type { AddressInfo } from 'node:net'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { MediaLibraryAssetSchema } from '../../packages/api-contracts/src/media.js'; import type { WebContext } from '../../src/web/context.js'; import { createMediaLibraryRouter } from '../../src/web/routes/media-library.js'; const servers: Array> = []; afterEach(async () => { await Promise.all(servers.splice(0).map((server) => new Promise((resolve) => server.close(() => resolve())))); }); describe('media library routes', () => { it('lists private assets without exposing their filesystem path', async () => { const asset = libraryAsset(); const context = { services: { mediaLibrary: { list: () => [asset] } } } as unknown as WebContext; const response = await request(express().use('/api', createMediaLibraryRouter(context)), '/api/media-library'); const body = await response.json(); expect(MediaLibraryAssetSchema.parse(body[0])).toMatchObject({ id: asset.id, urlPath: `/library-media/${asset.relativePath}` }); expect(body[0]).not.toHaveProperty('relativePath'); }); it('requires consent and passes a raw image to classification', async () => { const upload = vi.fn().mockResolvedValue(libraryAsset()); const context = { logger: { error: vi.fn() }, services: { mediaLibrary: { upload } } } as unknown as WebContext; const app = express().use('/api', createMediaLibraryRouter(context)); const response = await request(app, '/api/media-library', { method: 'POST', headers: { 'Content-Type': 'image/jpeg', 'X-File-Name': encodeURIComponent('portrait.jpg'), 'X-Consent-Confirmed': 'true', }, body: Uint8Array.from([1, 2, 3]), }); expect(response.status).toBe(201); expect(upload).toHaveBeenCalledWith(expect.objectContaining({ contentType: 'image/jpeg', originalFileName: 'portrait.jpg', consentConfirmed: true, })); expect(Buffer.isBuffer(upload.mock.calls[0][0].bytes)).toBe(true); }); it('validates tag corrections before updating', async () => { const update = vi.fn(); const context = { services: { mediaLibrary: { update } } } as unknown as WebContext; const app = express().use(express.json()).use('/api', createMediaLibraryRouter(context)); const response = await request(app, `/api/media-library/${libraryAsset().id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tags: [], safety: 'sfw', enabled: true }), }); expect(response.status).toBe(400); expect(update).not.toHaveBeenCalled(); }); }); async function request(app: express.Express, pathname: string, init?: RequestInit): Promise { const server = app.listen(0); servers.push(server); await new Promise((resolve) => server.once('listening', resolve)); const { port } = server.address() as AddressInfo; return fetch(`http://127.0.0.1:${port}${pathname}`, init); } function libraryAsset() { return { id: '019fe789-1b99-7ce1-8402-b3cbc32a445a', tags: ['selfie'] as const, safety: 'sfw' as const, classificationStatus: 'ready' as const, description: 'Portrait', confidence: 0.9, fileName: 'portrait.jpg', relativePath: '019fe789-1b99-7ce1-8402-b3cbc32a445a.jpg', mimeType: 'image/jpeg', sizeBytes: 100, width: 10, height: 10, enabled: true, consentConfirmedAt: 1, createdAt: 1, updatedAt: 1, sendCount: 0, lastSentAt: null, }; }