import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { createTestContext, hoursAgo } from '../test/helpers.ts' let ctx: Awaited> let reporter: Awaited> let number: number const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 13, 0x49, 0x48, 0x44, 0x52]) const form = (bytes: Uint8Array, name: string, type = 'image/png') => { const data = new FormData() data.append('file', new File([bytes], name, { type })) return data } beforeAll(async () => { ctx = await createTestContext() const site = await ctx.createSite() reporter = await ctx.createUser('reporter') const res = await reporter.post('/api/incidents', { title: 'Spill', type: 'hazard', severity: 'low', occurredAt: hoursAgo(1), siteId: site.id }) number = res.body.incident.number }) afterAll(() => ctx.close()) describe('attachments', () => { it('stores a photo and serves it back safely', async () => { const res = await reporter.post(`/api/incidents/${number}/attachments`, form(PNG, 'spill.png')) expect(res.status).toBe(201) expect(res.body.attachment).toMatchObject({ fileName: 'spill.png', contentType: 'image/png', sizeBytes: PNG.byteLength }) const file = await reporter.get(res.body.attachment.url) expect(file.status).toBe(200) expect(file.headers.get('content-type')).toBe('image/png') expect(file.headers.get('content-security-policy')).toContain('sandbox') expect(new Uint8Array(file.body)).toEqual(PNG) const detail = await reporter.get(`/api/incidents/${number}`) expect(detail.body.incident.attachments).toHaveLength(1) }) it('identifies files by content, not by name or claimed type', async () => { const html = new TextEncoder().encode('') expect((await reporter.post(`/api/incidents/${number}/attachments`, form(html, 'photo.png', 'image/png'))).status).toBe(400) }) it('rejects files over the size limit', async () => { const big = new Uint8Array(ctx.config.maxUploadBytes + 1) big.set(PNG) expect((await reporter.post(`/api/incidents/${number}/attachments`, form(big, 'big.png'))).status).toBe(413) }) it('hides files from people who cannot see the incident', async () => { const res = await reporter.post(`/api/incidents/${number}/attachments`, form(PNG, 'second.png')) const outsider = await ctx.createUser('reporter', 'outsider@example.com') expect((await outsider.get(res.body.attachment.url)).status).toBe(404) expect((await outsider.delete(res.body.attachment.url)).status).toBe(404) }) it('lets the uploader remove their file', async () => { const res = await reporter.post(`/api/incidents/${number}/attachments`, form(PNG, 'remove-me.png')) expect((await reporter.delete(res.body.attachment.url)).status).toBe(204) expect((await reporter.get(res.body.attachment.url)).status).toBe(404) }) }) describe('upload origin check', () => { it('rejects uploads posted from another site', async () => { const res = await ctx.app.request(`/api/incidents/${number}/attachments`, { method: 'POST', headers: { cookie: reporter.cookie!, origin: 'http://evil.example' }, body: form(PNG, 'x.png'), }) expect(res.status).toBe(403) }) })