import { describe, expect, it, vi, beforeEach } from 'vitest'; vi.mock('../../lib/request-context.js', () => ({ getUserId: vi.fn(() => 'user_1'), getRequestProductId: vi.fn(() => 'notelett'), })); vi.mock('../../lib/feature-flags.js', () => ({ isFeatureEnabled: vi.fn(() => true), })); vi.mock('../../lib/telemetry.js', () => ({ trackEvent: vi.fn() })); vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' })); vi.mock('../../lib/config.js', () => ({ config: {} })); vi.mock('../../lib/embeddings.js', () => ({ stripHtmlForEmbedding: vi.fn((s: string) => s.replace(/<[^>]*>/g, ' ').trim()), })); type NoteDoc = { id: string; userId?: string; productId?: string; title?: string; body?: string }; type CollabDoc = Record; const getNoteMock = vi.fn<(id: string, userId: string) => Promise>(); vi.mock('../notes/repository.js', () => ({ getNote: (...args: [string, string]) => getNoteMock(...args), })); const createCollaboratorMock = vi.fn<(doc: CollabDoc) => Promise>(); const listCollaboratorsForNoteMock = vi.fn<(noteId: string, productId: string) => Promise>(); const listSharedWithMeMock = vi.fn<(userId: string, productId: string) => Promise>(); const findCollaboratorMock = vi.fn<(noteId: string, userId: string, productId: string) => Promise>(); const deleteCollaboratorMock = vi.fn<(id: string, partitionKey: string) => Promise>(); vi.mock('./repository.js', () => ({ createCollaborator: (...args: [CollabDoc]) => createCollaboratorMock(...args), listCollaboratorsForNote: (...args: [string, string]) => listCollaboratorsForNoteMock(...args), listSharedWithMe: (...args: [string, string]) => listSharedWithMeMock(...args), findCollaborator: (...args: [string, string, string]) => findCollaboratorMock(...args), deleteCollaborator: (...args: [string, string]) => deleteCollaboratorMock(...args), })); import { buildTestApp } from '../../test-helpers.js'; import { noteCollaboratorRoutes } from './routes.js'; async function buildApp() { return buildTestApp(noteCollaboratorRoutes); } describe('note-collaborators routes', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('POST /notes/:id/share-with-user', () => { it('shares a note with another user', async () => { getNoteMock.mockResolvedValueOnce({ id: 'n1', userId: 'user_1', productId: 'notelett' }); findCollaboratorMock.mockResolvedValueOnce(null); const app = await buildApp(); const res = await app.inject({ method: 'POST', url: '/api/notes/n1/share-with-user', payload: { workspaceId: 'ws-1', sharedWithUserId: 'user_2', permission: 'view' }, }); expect(res.statusCode).toBe(201); expect(createCollaboratorMock).toHaveBeenCalledOnce(); }); it('rejects sharing with yourself', async () => { const app = await buildApp(); const res = await app.inject({ method: 'POST', url: '/api/notes/n1/share-with-user', payload: { workspaceId: 'ws-1', sharedWithUserId: 'user_1', permission: 'view' }, }); expect(res.statusCode).toBe(400); }); it('rejects duplicate share', async () => { getNoteMock.mockResolvedValueOnce({ id: 'n1', userId: 'user_1', productId: 'notelett' }); findCollaboratorMock.mockResolvedValueOnce({ id: 'existing' }); const app = await buildApp(); const res = await app.inject({ method: 'POST', url: '/api/notes/n1/share-with-user', payload: { workspaceId: 'ws-1', sharedWithUserId: 'user_2', permission: 'view' }, }); expect(res.statusCode).toBe(400); }); }); describe('GET /notes/:id/collaborators', () => { it('lists collaborators', async () => { listCollaboratorsForNoteMock.mockResolvedValueOnce([ { id: 'c1', sharedWithUserId: 'user_2', permission: 'view' }, ]); const app = await buildApp(); const res = await app.inject({ method: 'GET', url: '/api/notes/n1/collaborators' }); expect(res.statusCode).toBe(200); expect(res.json().items).toHaveLength(1); }); }); describe('GET /shared-with-me', () => { it('lists notes shared with current user', async () => { listSharedWithMeMock.mockResolvedValueOnce([ { id: 'c1', noteId: 'n1', sharedWithUserId: 'user_1' }, ]); const app = await buildApp(); const res = await app.inject({ method: 'GET', url: '/api/shared-with-me' }); expect(res.statusCode).toBe(200); expect(res.json().items).toHaveLength(1); }); }); describe('DELETE /notes/:noteId/collaborators/:userId', () => { it('removes a collaborator', async () => { findCollaboratorMock.mockResolvedValueOnce({ id: 'c1', sharedByUserId: 'user_1', sharedWithUserId: 'user_2', }); const app = await buildApp(); const res = await app.inject({ method: 'DELETE', url: '/api/notes/n1/collaborators/user_2', }); expect(res.statusCode).toBe(204); expect(deleteCollaboratorMock).toHaveBeenCalledOnce(); }); it('returns 404 for missing collaborator', async () => { findCollaboratorMock.mockResolvedValueOnce(null); const app = await buildApp(); const res = await app.inject({ method: 'DELETE', url: '/api/notes/n1/collaborators/user_2', }); expect(res.statusCode).toBe(404); }); }); describe('POST /notes/:id/export-text', () => { it('exports note as text formats', async () => { getNoteMock.mockResolvedValueOnce({ id: 'n1', userId: 'user_1', productId: 'notelett', title: 'Test Note', body: '

Hello world

', }); const app = await buildApp(); const res = await app.inject({ method: 'POST', url: '/api/notes/n1/export-text', payload: { workspaceId: 'ws-1' }, }); expect(res.statusCode).toBe(200); const body = res.json(); expect(body.title).toBe('Test Note'); expect(body.plaintext).toBeDefined(); expect(body.markdown).toBeDefined(); expect(body.html).toBe('

Hello world

'); }); it('returns 404 for missing note', async () => { getNoteMock.mockResolvedValueOnce(null); const app = await buildApp(); const res = await app.inject({ method: 'POST', url: '/api/notes/n1/export-text', payload: { workspaceId: 'ws-1' }, }); expect(res.statusCode).toBe(404); }); }); describe('GET /notes/:id/deep-link', () => { it('returns deep link URLs', async () => { getNoteMock.mockResolvedValueOnce({ id: 'n1', productId: 'notelett' }); const app = await buildApp(); const res = await app.inject({ method: 'GET', url: '/api/notes/n1/deep-link?workspaceId=ws-1', }); expect(res.statusCode).toBe(200); const body = res.json(); expect(body.web).toContain('/notes/n1'); expect(body.mobile).toBe('notelett://note/n1'); }); it('requires workspaceId query param', async () => { const app = await buildApp(); const res = await app.inject({ method: 'GET', url: '/api/notes/n1/deep-link' }); expect(res.statusCode).toBe(400); }); }); });