feat: MVP-2 completion - search, quick add, backlinks, guided forms
## Search & Retrieval - Improved search ranking with scoring (title match, favorites, recency) - Highlight matches with excerpt extraction - Fuzzy search with string-similarity - Unified noteQuery function ## Quick Capture - Quick Add API (POST /api/notes/quick) with type prefixes - Quick add parser with tag extraction - Global Quick Add UI (Ctrl+N shortcut) - Tag autocomplete in forms ## Note Relations - Automatic backlinks with sync on create/update/delete - Backlinks API (GET /api/notes/[id]/backlinks) - Related notes with scoring and reasons ## Guided Forms - Type-specific form fields (command, snippet, decision, recipe, procedure, inventory) - Serialization to/from markdown - Tag suggestions based on content (GET /api/tags/suggest) ## UX by Type - Command: Copy button for code blocks - Snippet: Syntax highlighting with react-syntax-highlighter - Procedure: Interactive checkboxes ## Quality - Standardized error handling across all APIs - Integration tests (28 tests passing) - Unit tests for search, tags, quick-add Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
690
__tests__/api.integration.test.ts
Normal file
690
__tests__/api.integration.test.ts
Normal file
@@ -0,0 +1,690 @@
|
||||
/**
|
||||
* 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/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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user