import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import sharp from 'sharp'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { MediaLibraryRepository } from '../src/database/media-library-repository.js'; import { MediaLibraryConflictError, MediaLibraryService } from '../src/services/media-library.js'; import { createTestDb } from './testUtils.js'; describe('MediaLibraryService', () => { let directory: string; beforeEach(async () => { directory = await fs.mkdtemp(path.join(os.tmpdir(), 'redditbot-library-')); }); afterEach(async () => { await fs.rm(directory, { recursive: true, force: true }); }); it('re-encodes uploads without metadata and stores editable AI classifications', async () => { const db = createTestDb(); const repository = new MediaLibraryRepository(db); const classifyLibraryImage = vi.fn().mockResolvedValue({ tags: ['selfie', 'topless'], safety: 'nsfw', description: 'A self portrait.', confidence: 0.92, }); const service = new MediaLibraryService(repository, { classifyLibraryImage }, logger(), directory, () => 1234); const source = await sharp({ create: { width: 8, height: 6, channels: 3, background: '#cc8844' } }) .withMetadata({ orientation: 6 }) .jpeg() .toBuffer(); const asset = await service.upload({ bytes: source, contentType: 'image/jpeg', originalFileName: '../private portrait.jpg', consentConfirmed: true, }); expect(asset).toMatchObject({ tags: ['selfie', 'topless'], safety: 'nsfw', classificationStatus: 'ready', enabled: true, consentConfirmedAt: 1234, }); expect(classifyLibraryImage).toHaveBeenCalledOnce(); const outputMetadata = await sharp(service.resolvePath(asset)).metadata(); expect(outputMetadata.exif).toBeUndefined(); expect(outputMetadata.orientation).toBeUndefined(); expect(service.update(asset.id, { tags: ['selfie'], safety: 'sfw', enabled: false })) .toMatchObject({ tags: ['selfie'], safety: 'sfw', enabled: false }); db.close(); }); it('keeps failed or low-confidence classifications disabled and rejects duplicate content', async () => { const db = createTestDb(); const repository = new MediaLibraryRepository(db); const source = await sharp({ create: { width: 4, height: 4, channels: 3, background: '#123456' } }).png().toBuffer(); const classifier = { classifyLibraryImage: vi.fn().mockRejectedValue(new Error('refused')) }; const service = new MediaLibraryService(repository, classifier, logger(), directory); const first = await service.upload({ bytes: source, contentType: 'image/png', originalFileName: 'one.png', consentConfirmed: true }); expect(first).toMatchObject({ classificationStatus: 'failed', safety: 'unknown', tags: [], enabled: false }); await expect(service.upload({ bytes: source, contentType: 'image/png', originalFileName: 'two.png', consentConfirmed: true })) .rejects.toBeInstanceOf(MediaLibraryConflictError); db.close(); }); }); function logger() { return { warn: vi.fn(), info: vi.fn(), debug: vi.fn(), error: vi.fn() } as never; }