import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('crypto', () => ({ randomUUID: vi.fn(() => 'test-uuid'), })); vi.mock('../../../lib/object-storage.js', () => ({ put: vi.fn(), })); vi.mock('../../../lib/permission-middleware.js', () => ({ getUserFromRequest: vi.fn(), })); vi.mock('../../../lib/rate-limit.js', () => ({ checkUploadRateLimit: vi.fn(), })); import { put } from '../../../lib/object-storage.js'; import { getUserFromRequest } from '../../../lib/permission-middleware.js'; import { checkUploadRateLimit } from '../../../lib/rate-limit.js'; import handler from '../../../pages/api/scan/upload-image.js'; const TINY_JPEG = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAAv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AL+AAf/Z'; function createRes() { const res = { statusCode: 200, headers: {}, body: null, status(code) { res.statusCode = code; return res; }, json(data) { res.body = data; return res; }, setHeader(name, value) { res.headers[name] = value; }, }; return res; } describe('POST /api/scan/upload-image', () => { beforeEach(() => { vi.clearAllMocks(); getUserFromRequest.mockResolvedValue({ userId: 7, email: 'a@b.c', role: 'user' }); checkUploadRateLimit.mockResolvedValue({ allowed: true, remaining: 9, reset: Date.now() + 60_000 }); put.mockResolvedValue({ url: 'https://blob.example/scans/7/test-uuid.jpg' }); }); it('returns 405 for non-POST methods', async () => { const res = createRes(); await handler({ method: 'GET' }, res); expect(res.statusCode).toBe(405); }); it('returns 401 when unauthenticated', async () => { getUserFromRequest.mockResolvedValue(null); const res = createRes(); await handler({ method: 'POST', body: { imageData: TINY_JPEG } }, res); expect(res.statusCode).toBe(401); expect(put).not.toHaveBeenCalled(); }); it('returns 429 when upload rate limit is exceeded', async () => { checkUploadRateLimit.mockResolvedValue({ allowed: false, remaining: 0, reset: Date.now() + 30_000, }); const res = createRes(); await handler({ method: 'POST', body: { imageData: TINY_JPEG } }, res); expect(res.statusCode).toBe(429); expect(res.headers['Retry-After']).toBeDefined(); expect(put).not.toHaveBeenCalled(); }); it('returns 400 for unsupported image MIME in data URL', async () => { const res = createRes(); await handler( { method: 'POST', body: { imageData: 'data:image/gif;base64,AAAA' } }, res ); expect(res.statusCode).toBe(400); expect(res.body.error).toMatch(/JPEG, PNG, or WebP/); expect(put).not.toHaveBeenCalled(); }); it('uploads a valid JPEG data URL and returns the blob URL', async () => { const res = createRes(); await handler({ method: 'POST', body: { imageData: TINY_JPEG } }, res); expect(res.statusCode).toBe(200); expect(res.body.url).toBe('https://blob.example/scans/7/test-uuid.jpg'); expect(put).toHaveBeenCalledWith( 'scans/7/test-uuid.jpg', expect.any(Buffer), expect.objectContaining({ contentType: 'image/jpeg' }) ); }); });