Add consent-gated media exchange library
This commit is contained in:
1 parent
b8e4a1bb88
commit
bf474dfe10
42 files changed
+2533
-9
No files matched your search
@@ -0,0 +1,75 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user