import { describe, it, expect, vi, beforeEach } from 'vitest'; import { join, sep } from 'node:path'; const { mockReadFile, mockHandle, mockRegisterSchemes } = vi.hoisted(() => ({ mockReadFile: vi.fn(), mockHandle: vi.fn(), mockRegisterSchemes: vi.fn() })); vi.mock('node:fs/promises', () => ({ readFile: mockReadFile })); vi.mock('electron', () => ({ default: { protocol: { registerSchemesAsPrivileged: mockRegisterSchemes, handle: mockHandle } } })); import { registerInklingMediaProtocol, inferMime } from '../../src/main/protocol/inklingMedia'; describe('inferMime', () => { it('returns image/png for .png', () => { expect(inferMime('foo.png')).toBe('image/png'); }); it('returns image/jpeg for .jpg and .jpeg', () => { expect(inferMime('foo.jpg')).toBe('image/jpeg'); expect(inferMime('foo.jpeg')).toBe('image/jpeg'); }); it('returns image/gif for .gif', () => { expect(inferMime('foo.gif')).toBe('image/gif'); }); it('returns image/webp for .webp', () => { expect(inferMime('foo.webp')).toBe('image/webp'); }); it('returns application/octet-stream for unknown', () => { expect(inferMime('foo.xyz')).toBe('application/octet-stream'); }); }); describe('inkling-media protocol handler', () => { beforeEach(() => { vi.clearAllMocks(); }); function getHandler(profileDir: string): (req: { url: string }) => Promise { registerInklingMediaProtocol(profileDir); const call = mockHandle.mock.calls[0]; if (!call) throw new Error('protocol.handle not called'); return call[1] as (req: { url: string }) => Promise; } // 실 운영 (Electron protocol.handle) 에서는 req.url 이 raw 문자열로 전달되지만, // vitest 의 `new Request()` constructor 는 url 을 즉시 normalize (`/../` 제거) 함. // 따라서 traversal 검사 로직이 raw URL 단계에서 작동하는지 검증하려면 // raw url 을 보존한 minimal mock 을 직접 전달. function rawReq(url: string): { url: string } { return { url }; } it('serves valid file with correct mime', async () => { mockReadFile.mockResolvedValueOnce(Buffer.from([1, 2, 3])); const handler = getHandler('/profile'); const res = await handler(rawReq('inkling-media://media/note1/img.png')); expect(res.status).toBe(200); expect(res.headers.get('content-type')).toBe('image/png'); expect(mockReadFile).toHaveBeenCalledWith(join('/profile', 'media', 'note1', 'img.png')); }); it('returns 403 on path traversal attempt', async () => { const handler = getHandler('/profile'); const res = await handler(rawReq('inkling-media://media/../etc/passwd')); expect(res.status).toBe(403); expect(mockReadFile).not.toHaveBeenCalled(); }); it('returns 404 when file missing', async () => { mockReadFile.mockRejectedValueOnce(new Error('ENOENT')); const handler = getHandler('/profile'); const res = await handler(rawReq('inkling-media://media/note1/missing.png')); expect(res.status).toBe(404); }); });