deckhearth/test/api/scan/upload-image.test.js

105 lines
3.2 KiB
JavaScript
Raw Permalink Normal View History

import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('crypto', () => ({
randomUUID: vi.fn(() => 'test-uuid'),
}));
vi.mock('@vercel/blob', () => ({
put: vi.fn(),
}));
vi.mock('../../../lib/permission-middleware.js', () => ({
getUserFromRequest: vi.fn(),
}));
vi.mock('../../../lib/rate-limit.js', () => ({
checkUploadRateLimit: vi.fn(),
}));
import { put } from '@vercel/blob';
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({ access: 'public', contentType: 'image/jpeg' })
);
});
});