test: add unit tests for lib/ (validate, auth, storage, email, rate-limit)
49 tests for all 10 Zod schemas in validate.ts, 26 tests for auth (hashPassword, verifyPassword, createSession, getSession, getCurrentUser, deleteSession), 11 for storage (magic-byte validation, saveImage, deleteImageFile), 9 for email (sendMail scenarios), and 6 for rate limiting logic.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
const { mockMkdir, mockWriteFile, mockUnlink } = vi.hoisted(() => ({
|
||||
mockMkdir: vi.fn().mockResolvedValue(undefined),
|
||||
mockWriteFile: vi.fn().mockResolvedValue(undefined),
|
||||
mockUnlink: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock('fs/promises', () => ({
|
||||
__esModule: true,
|
||||
default: { mkdir: mockMkdir, writeFile: mockWriteFile, unlink: mockUnlink },
|
||||
mkdir: mockMkdir,
|
||||
writeFile: mockWriteFile,
|
||||
unlink: mockUnlink,
|
||||
}))
|
||||
|
||||
import { saveImage, validateImageMagicBytes, deleteImageFile } from '@/lib/storage'
|
||||
|
||||
function makeJpegBuffer(): Buffer {
|
||||
const b = Buffer.alloc(12)
|
||||
b[0] = 0xff; b[1] = 0xd8; b[2] = 0xff
|
||||
return b
|
||||
}
|
||||
|
||||
function makePngBuffer(): Buffer {
|
||||
const b = Buffer.alloc(12)
|
||||
b[0] = 0x89; b[1] = 0x50; b[2] = 0x4e; b[3] = 0x47
|
||||
return b
|
||||
}
|
||||
|
||||
function makeWebpBuffer(): Buffer {
|
||||
const b = Buffer.alloc(12)
|
||||
b[0] = 0x52; b[1] = 0x49; b[2] = 0x46; b[3] = 0x46
|
||||
b[8] = 0x57; b[9] = 0x45; b[10] = 0x42; b[11] = 0x50
|
||||
return b
|
||||
}
|
||||
|
||||
function makeIcoBuffer(): Buffer {
|
||||
const b = Buffer.alloc(12)
|
||||
b[0] = 0x00; b[1] = 0x00; b[2] = 0x01; b[3] = 0x00
|
||||
return b
|
||||
}
|
||||
|
||||
function makeFileFromBuffer(buf: Buffer, type: string): File {
|
||||
return new File([new Uint8Array(buf)], 'test.img', { type })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockMkdir.mockResolvedValue(undefined)
|
||||
mockWriteFile.mockResolvedValue(undefined)
|
||||
mockUnlink.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
// ─── validateImageMagicBytes ──────────────────────────────────────────────────
|
||||
|
||||
describe('validateImageMagicBytes', () => {
|
||||
it('accepts a valid JPEG', async () => {
|
||||
const file = makeFileFromBuffer(makeJpegBuffer(), 'image/jpeg')
|
||||
expect(await validateImageMagicBytes(file, 'image/jpeg')).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a valid PNG', async () => {
|
||||
const file = makeFileFromBuffer(makePngBuffer(), 'image/png')
|
||||
expect(await validateImageMagicBytes(file, 'image/png')).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a valid WebP', async () => {
|
||||
const file = makeFileFromBuffer(makeWebpBuffer(), 'image/webp')
|
||||
expect(await validateImageMagicBytes(file, 'image/webp')).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a valid ICO', async () => {
|
||||
const file = makeFileFromBuffer(makeIcoBuffer(), 'image/x-icon')
|
||||
expect(await validateImageMagicBytes(file, 'image/x-icon')).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects JPEG buffer declared as PNG', async () => {
|
||||
const file = makeFileFromBuffer(makeJpegBuffer(), 'image/png')
|
||||
expect(await validateImageMagicBytes(file, 'image/png')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects unknown MIME type', async () => {
|
||||
const file = makeFileFromBuffer(makeJpegBuffer(), 'image/bmp')
|
||||
expect(await validateImageMagicBytes(file, 'image/bmp')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a random buffer as JPEG', async () => {
|
||||
const file = makeFileFromBuffer(Buffer.from([0x00, 0x01, 0x02]), 'image/jpeg')
|
||||
expect(await validateImageMagicBytes(file, 'image/jpeg')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── saveImage ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('saveImage', () => {
|
||||
it('creates the product directory and writes file', async () => {
|
||||
const buf = Buffer.from('fakeimage')
|
||||
const url = await saveImage('prod-123', buf, 'photo.jpg')
|
||||
|
||||
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('prod-123'), { recursive: true })
|
||||
expect(mockWriteFile).toHaveBeenCalled()
|
||||
expect(url).toMatch(/^\/uploads\/prod-123\//)
|
||||
expect(url).toMatch(/photo\.jpg$/)
|
||||
})
|
||||
|
||||
it('sanitizes filename (removes special chars)', async () => {
|
||||
const buf = Buffer.from('fakeimage')
|
||||
const url = await saveImage('prod-123', buf, 'my photo (1).JPG')
|
||||
expect(url).toMatch(/my_photo__1_\.jpg$/)
|
||||
})
|
||||
|
||||
it('returns a URL starting with /uploads/', async () => {
|
||||
const url = await saveImage('prod-abc', Buffer.from('x'), 'img.png')
|
||||
expect(url).toMatch(/^\/uploads\/prod-abc\//)
|
||||
})
|
||||
})
|
||||
|
||||
// ─── deleteImageFile ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('deleteImageFile', () => {
|
||||
it('calls unlink with the correct path', async () => {
|
||||
await deleteImageFile('/uploads/prod-1/image.jpg')
|
||||
expect(mockUnlink).toHaveBeenCalledWith(expect.stringContaining('/uploads/prod-1/image.jpg'))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user