/** * Integration Tests for All API Endpoints * * Tests success and error cases for all routes: * - /api/notes (GET, POST) * - /api/notes/[id] (GET, PUT, DELETE) * - /api/notes/quick (POST) * - /api/tags (GET) * - /api/search (GET) * - /api/export-import (POST) */ import { NextRequest } from 'next/server' // Complete mock Prisma client const mockPrisma = { note: { findMany: jest.fn().mockResolvedValue([]), findUnique: jest.fn(), findFirst: jest.fn(), create: jest.fn(), update: jest.fn(), delete: jest.fn(), deleteMany: jest.fn(), }, tag: { findMany: jest.fn().mockResolvedValue([]), findUnique: jest.fn(), upsert: jest.fn(), create: jest.fn(), }, noteTag: { create: jest.fn(), deleteMany: jest.fn(), }, backlink: { deleteMany: jest.fn(), create: jest.fn(), createMany: jest.fn(), }, $transaction: jest.fn((callback) => callback(mockPrisma)), } // Mock prisma before imports jest.mock('@/lib/prisma', () => ({ prisma: mockPrisma, })) // Mock string-similarity used in search jest.mock('string-similarity', () => ({ compareTwoStrings: jest.fn().mockReturnValue(0.5), })) describe('API Integration Tests', () => { beforeEach(() => { jest.clearAllMocks() // Reset transaction mock to properly chain mockPrisma.$transaction.mockImplementation(async (callback) => { return callback(mockPrisma) }) }) // Helper to parse wrapped response function getData(response: { json: () => Promise<{ data?: unknown; error?: unknown; success: boolean }> }) { return response.json().then((r) => r.data ?? r) } function expectSuccess(response: { status: number; json: () => Promise<{ success: boolean }> }) { expect(response.status).toBeLessThan(400) return response.json().then((r) => expect(r.success).toBe(true)) } // ============================================ // GET /api/notes - List all notes // ============================================ describe('GET /api/notes', () => { it('returns empty array when no notes exist', async () => { mockPrisma.note.findMany.mockResolvedValue([]) const { GET } = await import('@/app/api/notes/route') const response = await GET(new NextRequest('http://localhost/api/notes')) const data = await getData(response) expect(response.status).toBe(200) expect(data).toEqual([]) }) it('returns all notes with tags', async () => { const mockNotes = [ { id: '1', title: 'Note 1', content: 'Content 1', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), tags: [{ tag: { id: 't1', name: 'javascript' } }], }, ] mockPrisma.note.findMany.mockResolvedValue(mockNotes) const { GET } = await import('@/app/api/notes/route') const response = await GET(new NextRequest('http://localhost/api/notes')) const data = await getData(response) expect(response.status).toBe(200) expect(Array.isArray(data)).toBe(true) }) it('filters notes with search query', async () => { mockPrisma.note.findMany.mockResolvedValue([]) const { GET } = await import('@/app/api/notes/route') const response = await GET(new NextRequest('http://localhost/api/notes?q=test')) expect(response.status).toBe(200) }) }) // ============================================ // POST /api/notes - Create note // ============================================ describe('POST /api/notes', () => { it('creates a note without tags', async () => { const newNote = { id: '1', title: 'New Note', content: 'Note content', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), tags: [], } mockPrisma.note.create.mockResolvedValue(newNote) mockPrisma.backlink.deleteMany.mockResolvedValue([]) const { POST } = await import('@/app/api/notes/route') const request = new NextRequest('http://localhost/api/notes', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: 'New Note', content: 'Note content', }), }) const response = await POST(request) const data = await response.json() expect(response.status).toBe(201) expect(data.success).toBe(true) expect(data.data.title).toBe('New Note') }) it('creates a note with tags', async () => { const newNote = { id: '1', title: 'Tagged Note', content: 'Content', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), tags: [{ tag: { id: '1', name: 'javascript' } }], } mockPrisma.note.create.mockResolvedValue(newNote) mockPrisma.tag.upsert.mockResolvedValue({ id: '1', name: 'javascript' }) mockPrisma.noteTag.create.mockResolvedValue({ noteId: '1', tagId: '1' }) mockPrisma.backlink.deleteMany.mockResolvedValue([]) const { POST } = await import('@/app/api/notes/route') const request = new NextRequest('http://localhost/api/notes', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: 'Tagged Note', content: 'Content', tags: ['JavaScript'], }), }) const response = await POST(request) const data = await response.json() expect(response.status).toBe(201) expect(data.success).toBe(true) }) it('returns 400 when title is missing', async () => { const { POST } = await import('@/app/api/notes/route') const request = new NextRequest('http://localhost/api/notes', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: '', content: 'Some content', }), }) const response = await POST(request) expect(response.status).toBe(400) }) it('returns 400 when content is missing', async () => { const { POST } = await import('@/app/api/notes/route') const request = new NextRequest('http://localhost/api/notes', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: 'Valid Title', content: '', }), }) const response = await POST(request) expect(response.status).toBe(400) }) }) // ============================================ // GET /api/notes/[id] - Get single note // ============================================ describe('GET /api/notes/[id]', () => { it('returns note when found', async () => { const mockNote = { id: '1', title: 'Test Note', content: 'Content', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), tags: [], } mockPrisma.note.findUnique.mockResolvedValue(mockNote) const { GET } = await import('@/app/api/notes/[id]/route') const request = new NextRequest('http://localhost/api/notes/1') const response = await GET(request, { params: Promise.resolve({ id: '1' }) }) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(data.data.id).toBe('1') }) it('returns 404 when note not found', async () => { mockPrisma.note.findUnique.mockResolvedValue(null) const { GET } = await import('@/app/api/notes/[id]/route') const request = new NextRequest('http://localhost/api/notes/nonexistent') const response = await GET(request, { params: Promise.resolve({ id: 'nonexistent' }) }) expect(response.status).toBe(404) }) }) // ============================================ // PUT /api/notes/[id] - Update note // ============================================ describe('PUT /api/notes/[id]', () => { it('updates note title', async () => { const existingNote = { id: '1', title: 'Old Title', content: 'Content', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), tags: [], } const updatedNote = { ...existingNote, title: 'New Title', } mockPrisma.note.findUnique.mockResolvedValue(existingNote) mockPrisma.note.update.mockResolvedValue(updatedNote) mockPrisma.noteTag.deleteMany.mockResolvedValue([]) mockPrisma.backlink.deleteMany.mockResolvedValue([]) const { PUT } = await import('@/app/api/notes/[id]/route') const request = new NextRequest('http://localhost/api/notes/1', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: '1', title: 'New Title' }), }) const response = await PUT(request, { params: Promise.resolve({ id: '1' }) }) expect(response.status).toBe(200) }) it('returns 404 when note to update not found', async () => { mockPrisma.note.findUnique.mockResolvedValue(null) const { PUT } = await import('@/app/api/notes/[id]/route') const request = new NextRequest('http://localhost/api/notes/nonexistent', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: 'nonexistent', title: 'New Title' }), }) const response = await PUT(request, { params: Promise.resolve({ id: 'nonexistent' }) }) expect(response.status).toBe(404) }) it('returns 400 for invalid update data', async () => { const existingNote = { id: '1', title: 'Test', content: 'Content', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), tags: [], } mockPrisma.note.findUnique.mockResolvedValue(existingNote) const { PUT } = await import('@/app/api/notes/[id]/route') const request = new NextRequest('http://localhost/api/notes/1', { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id: '1', title: '', content: '' }), }) const response = await PUT(request, { params: Promise.resolve({ id: '1' }) }) expect(response.status).toBe(400) }) }) // ============================================ // DELETE /api/notes/[id] - Delete note // ============================================ describe('DELETE /api/notes/[id]', () => { it('deletes note successfully', async () => { const existingNote = { id: '1', title: 'Test', content: 'Content', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), tags: [], } mockPrisma.note.findUnique.mockResolvedValue(existingNote) mockPrisma.backlink.deleteMany.mockResolvedValue([]) mockPrisma.note.delete.mockResolvedValue({ id: '1' }) const { DELETE } = await import('@/app/api/notes/[id]/route') const request = new NextRequest('http://localhost/api/notes/1', { method: 'DELETE', }) const response = await DELETE(request, { params: Promise.resolve({ id: '1' }) }) expect(response.status).toBe(200) }) it('returns 404 when deleting non-existent note', async () => { mockPrisma.note.findUnique.mockResolvedValue(null) const { DELETE } = await import('@/app/api/notes/[id]/route') const request = new NextRequest('http://localhost/api/notes/nonexistent', { method: 'DELETE', }) const response = await DELETE(request, { params: Promise.resolve({ id: 'nonexistent' }) }) expect(response.status).toBe(404) }) }) // ============================================ // POST /api/notes/quick - Quick add note // ============================================ describe('POST /api/notes/quick', () => { it('creates note from plain text', async () => { const createdNote = { id: '1', title: 'Quick Note', content: 'Quick Note', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), tags: [], } mockPrisma.note.create.mockResolvedValue(createdNote) const { POST } = await import('@/app/api/notes/quick/route') const request = new NextRequest('http://localhost/api/notes/quick', { method: 'POST', headers: { 'content-type': 'text/plain' }, body: 'Quick Note', }) const response = await POST(request) const json = await response.json() const data = json.data ?? json expect(response.status).toBe(201) expect(data).toHaveProperty('title', 'Quick Note') }) it('creates note with type prefix', async () => { const createdNote = { id: '1', title: 'Deploy script', content: 'deploy script content', type: 'command', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), tags: [], } mockPrisma.note.create.mockResolvedValue(createdNote) const { POST } = await import('@/app/api/notes/quick/route') const request = new NextRequest('http://localhost/api/notes/quick', { method: 'POST', headers: { 'content-type': 'text/plain' }, body: 'cmd: Deploy script\ndeploy script content', }) const response = await POST(request) const json = await response.json() const data = json.data ?? json expect(response.status).toBe(201) expect(data).toHaveProperty('type', 'command') }) it('returns 400 when text is empty', async () => { const { POST } = await import('@/app/api/notes/quick/route') const request = new NextRequest('http://localhost/api/notes/quick', { method: 'POST', headers: { 'content-type': 'text/plain' }, body: '', }) const response = await POST(request) expect(response.status).toBe(400) }) it('returns 400 for JSON with missing text', async () => { const { POST } = await import('@/app/api/notes/quick/route') const request = new NextRequest('http://localhost/api/notes/quick', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({}), }) const response = await POST(request) expect(response.status).toBe(400) }) }) // ============================================ // GET /api/tags - List tags / suggestions // ============================================ describe('GET /api/tags', () => { it('returns all tags when no query', async () => { const mockTags = [ { id: '1', name: 'javascript' }, { id: '2', name: 'python' }, { id: '3', name: 'typescript' }, ] mockPrisma.tag.findMany.mockResolvedValue(mockTags) const { GET } = await import('@/app/api/tags/route') const request = new NextRequest('http://localhost/api/tags') const response = await GET(request) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(data.data).toEqual(mockTags) }) it('returns tag suggestions when q param provided', async () => { const mockTags = [ { id: '1', name: 'javascript' }, { id: '2', name: 'java' }, ] mockPrisma.tag.findMany.mockResolvedValue(mockTags) const { GET } = await import('@/app/api/tags/route') const request = new NextRequest('http://localhost/api/tags?q=java') const response = await GET(request) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(Array.isArray(data.data)).toBe(true) }) it('returns empty array when no tags exist', async () => { mockPrisma.tag.findMany.mockResolvedValue([]) const { GET } = await import('@/app/api/tags/route') const request = new NextRequest('http://localhost/api/tags') const response = await GET(request) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(data.data).toEqual([]) }) }) // ============================================ // GET /api/tags/suggest - Suggest tags based on content // ============================================ describe('GET /api/tags/suggest', () => { it('suggests tags based on title keywords', async () => { const { GET } = await import('@/app/api/tags/suggest/route') const request = new NextRequest('http://localhost/api/tags/suggest?title=Docker%20deployment&content=') const response = await GET(request) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(Array.isArray(data.data)).toBe(true) }) it('suggests tags based on content keywords', async () => { const { GET } = await import('@/app/api/tags/suggest/route') const request = new NextRequest('http://localhost/api/tags/suggest?title=&content=Docker%20and%20Kubernetes%20deployment') const response = await GET(request) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(Array.isArray(data.data)) }) it('combines title and content for suggestions', async () => { const { GET } = await import('@/app/api/tags/suggest/route') const request = new NextRequest('http://localhost/api/tags/suggest?title=Python%20script&content=SQL%20database%20query') const response = await GET(request) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) }) it('returns empty array for generic content', async () => { const { GET } = await import('@/app/api/tags/suggest/route') const request = new NextRequest('http://localhost/api/tags/suggest?title=Note&content=content') const response = await GET(request) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) expect(Array.isArray(data.data)).toBe(true) }) it('handles empty parameters gracefully', async () => { const { GET } = await import('@/app/api/tags/suggest/route') const request = new NextRequest('http://localhost/api/tags/suggest') const response = await GET(request) const data = await response.json() expect(response.status).toBe(200) expect(data.success).toBe(true) }) }) // ============================================ // GET /api/search - Search notes // ============================================ describe('GET /api/search', () => { it('returns search results with query', async () => { const mockNotes = [ { id: '1', title: 'JavaScript Guide', content: 'Learning JavaScript basics', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), tags: [], }, ] mockPrisma.note.findMany.mockResolvedValue(mockNotes) const { GET } = await import('@/app/api/search/route') const request = new NextRequest('http://localhost/api/search?q=javascript') const response = await GET(request) expect(response.status).toBe(200) }) it('filters by type', async () => { mockPrisma.note.findMany.mockResolvedValue([]) const { GET } = await import('@/app/api/search/route') const request = new NextRequest('http://localhost/api/search?type=command') const response = await GET(request) expect(response.status).toBe(200) }) it('filters by tag', async () => { mockPrisma.note.findMany.mockResolvedValue([]) const { GET } = await import('@/app/api/search/route') const request = new NextRequest('http://localhost/api/search?tag=python') const response = await GET(request) expect(response.status).toBe(200) }) }) // ============================================ // POST /api/export-import - Import notes // ============================================ describe('POST /api/export-import', () => { it('imports valid notes array', async () => { mockPrisma.note.findUnique.mockResolvedValue(null) mockPrisma.note.findFirst.mockResolvedValue(null) mockPrisma.note.create.mockResolvedValue({ id: '1', title: 'Imported Note', content: 'Content', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), }) mockPrisma.tag.upsert.mockResolvedValue({ id: '1', name: 'imported' }) mockPrisma.noteTag.create.mockResolvedValue({ noteId: '1', tagId: '1' }) mockPrisma.noteTag.deleteMany.mockResolvedValue([]) const { POST } = await import('@/app/api/export-import/route') const request = new NextRequest('http://localhost/api/export-import', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify([ { title: 'Imported Note', content: 'Content', type: 'note', tags: ['imported'], }, ]), }) const response = await POST(request) expect(response.status).toBe(201) }) it('returns 400 for non-array input', async () => { const { POST } = await import('@/app/api/export-import/route') const request = new NextRequest('http://localhost/api/export-import', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: 'Single note' }), }) const response = await POST(request) expect(response.status).toBe(400) }) it('returns 400 for invalid note in array', async () => { const { POST } = await import('@/app/api/export-import/route') const request = new NextRequest('http://localhost/api/export-import', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify([ { title: '', content: 'Invalid note', }, ]), }) const response = await POST(request) expect(response.status).toBe(400) }) it('updates existing note when id matches', async () => { const existingNote = { id: '1', title: 'Old Title', content: 'Old content', type: 'note', isFavorite: false, isPinned: false, createdAt: new Date(), updatedAt: new Date(), } mockPrisma.note.findUnique.mockResolvedValue(existingNote) mockPrisma.note.update.mockResolvedValue({ ...existingNote, title: 'Updated Title', }) mockPrisma.noteTag.deleteMany.mockResolvedValue([]) const { POST } = await import('@/app/api/export-import/route') const request = new NextRequest('http://localhost/api/export-import', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify([ { id: '1', title: 'Updated Title', content: 'Updated content', type: 'note', }, ]), }) const response = await POST(request) expect(response.status).toBe(201) }) }) })