76 lines
3.5 KiB
TypeScript
76 lines
3.5 KiB
TypeScript
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<ReturnType<express.Express['listen']>> = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(servers.splice(0).map((server) => new Promise<void>((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<Response> {
|
|
const server = app.listen(0);
|
|
servers.push(server);
|
|
await new Promise<void>((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,
|
|
};
|
|
}
|