Keep/keep-notes/tests/migration/data-migration.test.ts
sepehr ddb67ba9e5 fix: unify theme system - fix theme switching persistence
- Unified localStorage key to 'theme-preference' across all components
- Fixed header.tsx using wrong localStorage key ('theme' instead of 'theme-preference')
- Added localStorage hybrid persistence for instant theme changes
- Removed router.refresh() which was causing stale data revert
- Replaced Blue theme with Sepia
- Consolidated auth() calls to prevent race conditions
- Updated UserSettingsData types to include all themes
2026-01-18 22:33:41 +01:00

632 lines
19 KiB
TypeScript

/**
* Data Migration Tests
* Validates that data migration scripts work correctly
* Tests data transformation, integrity, and edge cases
*/
import { PrismaClient } from '@prisma/client'
import {
setupTestEnvironment,
createTestPrismaClient,
initializeTestDatabase,
cleanupTestDatabase,
createSampleNotes,
createSampleAINotes,
verifyDataIntegrity,
measureExecutionTime
} from './setup'
describe('Data Migration Tests', () => {
let prisma: PrismaClient
beforeAll(async () => {
await setupTestEnvironment()
prisma = createTestPrismaClient()
await initializeTestDatabase(prisma)
})
afterAll(async () => {
await cleanupTestDatabase(prisma)
})
describe('Empty Database Migration', () => {
test('should migrate empty database successfully', async () => {
// Verify database is empty
const noteCount = await prisma.note.count()
expect(noteCount).toBe(0)
// Data migration should handle empty database gracefully
// No data should be created or lost
expect(noteCount).toBe(0)
})
})
describe('Basic Data Migration', () => {
beforeEach(async () => {
// Clean up before each test
await prisma.note.deleteMany({})
await prisma.aiFeedback.deleteMany({})
})
test('should migrate basic notes without AI fields', async () => {
// Create sample notes (simulating pre-migration data)
await createSampleNotes(prisma, 10)
// Verify notes are created
const noteCount = await prisma.note.count()
expect(noteCount).toBe(10)
// All notes should have null AI fields (backward compatibility)
const notes = await prisma.note.findMany()
notes.forEach(note => {
expect(note.autoGenerated).toBeNull()
expect(note.aiProvider).toBeNull()
expect(note.aiConfidence).toBeNull()
expect(note.language).toBeNull()
expect(note.languageConfidence).toBeNull()
expect(note.lastAiAnalysis).toBeNull()
})
})
test('should preserve existing note data during migration', async () => {
// Create a note with all fields
const originalNote = await prisma.note.create({
data: {
title: 'Original Note',
content: 'Original content',
color: 'blue',
isPinned: true,
isArchived: false,
type: 'text',
size: 'medium',
userId: 'test-user-id',
order: 0
}
})
// Simulate migration by querying the note
const noteAfterMigration = await prisma.note.findUnique({
where: { id: originalNote.id }
})
// Verify all original fields are preserved
expect(noteAfterMigration?.title).toBe('Original Note')
expect(noteAfterMigration?.content).toBe('Original content')
expect(noteAfterMigration?.color).toBe('blue')
expect(noteAfterMigration?.isPinned).toBe(true)
expect(noteAfterMigration?.isArchived).toBe(false)
expect(noteAfterMigration?.type).toBe('text')
expect(noteAfterMigration?.size).toBe('medium')
expect(noteAfterMigration?.order).toBe(0)
})
})
describe('AI Fields Data Migration', () => {
test('should handle notes with all AI fields populated', async () => {
const testNote = await prisma.note.create({
data: {
title: 'AI Test Note',
content: 'Test content',
userId: 'test-user-id',
autoGenerated: true,
aiProvider: 'openai',
aiConfidence: 95,
language: 'en',
languageConfidence: 0.98,
lastAiAnalysis: new Date()
}
})
// Verify AI fields are correctly stored
expect(testNote.autoGenerated).toBe(true)
expect(testNote.aiProvider).toBe('openai')
expect(testNote.aiConfidence).toBe(95)
expect(testNote.language).toBe('en')
expect(testNote.languageConfidence).toBe(0.98)
expect(testNote.lastAiAnalysis).toBeDefined()
})
test('should handle notes with partial AI fields', async () => {
// Create note with only some AI fields
const note1 = await prisma.note.create({
data: {
title: 'Partial AI Note 1',
content: 'Test content',
userId: 'test-user-id',
autoGenerated: true,
aiProvider: 'ollama'
// Other AI fields are null
}
})
expect(note1.autoGenerated).toBe(true)
expect(note1.aiProvider).toBe('ollama')
expect(note1.aiConfidence).toBeNull()
expect(note1.language).toBeNull()
// Create note with different partial fields
const note2 = await prisma.note.create({
data: {
title: 'Partial AI Note 2',
content: 'Test content',
userId: 'test-user-id',
aiConfidence: 87,
language: 'fr',
languageConfidence: 0.92
// Other AI fields are null
}
})
expect(note2.autoGenerated).toBeNull()
expect(note2.aiProvider).toBeNull()
expect(note2.aiConfidence).toBe(87)
expect(note2.language).toBe('fr')
expect(note2.languageConfidence).toBe(0.92)
})
test('should handle null values in AI fields correctly', async () => {
const note = await prisma.note.create({
data: {
title: 'Null AI Fields Note',
content: 'Test content',
userId: 'test-user-id'
// All AI fields are null by default
}
})
// Verify all AI fields are null
expect(note.autoGenerated).toBeNull()
expect(note.aiProvider).toBeNull()
expect(note.aiConfidence).toBeNull()
expect(note.language).toBeNull()
expect(note.languageConfidence).toBeNull()
expect(note.lastAiAnalysis).toBeNull()
})
})
describe('AiFeedback Data Migration', () => {
test('should create and retrieve AiFeedback entries', async () => {
const note = await prisma.note.create({
data: {
title: 'Feedback Test Note',
content: 'Test content',
userId: 'test-user-id'
}
})
const feedback = await prisma.aiFeedback.create({
data: {
noteId: note.id,
userId: 'test-user-id',
feedbackType: 'thumbs_up',
feature: 'title_suggestion',
originalContent: 'AI suggested title',
metadata: JSON.stringify({
aiProvider: 'openai',
confidence: 95,
timestamp: new Date().toISOString()
})
}
})
// Verify feedback is correctly stored
expect(feedback.noteId).toBe(note.id)
expect(feedback.feedbackType).toBe('thumbs_up')
expect(feedback.feature).toBe('title_suggestion')
expect(feedback.originalContent).toBe('AI suggested title')
expect(feedback.metadata).toBeDefined()
})
test('should handle different feedback types', async () => {
const note = await prisma.note.create({
data: {
title: 'Feedback Types Note',
content: 'Test content',
userId: 'test-user-id'
}
})
const feedbackTypes = [
{ type: 'thumbs_up', feature: 'title_suggestion', content: 'Good suggestion' },
{ type: 'thumbs_down', feature: 'semantic_search', content: 'Bad result' },
{ type: 'correction', feature: 'title_suggestion', content: 'Wrong', corrected: 'Correct' }
]
for (const fb of feedbackTypes) {
const feedback = await prisma.aiFeedback.create({
data: {
noteId: note.id,
userId: 'test-user-id',
feedbackType: fb.type,
feature: fb.feature,
originalContent: fb.content,
correctedContent: fb.corrected
}
})
expect(feedback.feedbackType).toBe(fb.type)
}
})
test('should store and retrieve metadata JSON correctly', async () => {
const note = await prisma.note.create({
data: {
title: 'Metadata Test Note',
content: 'Test content',
userId: 'test-user-id'
}
})
const metadata = {
aiProvider: 'ollama',
model: 'llama2-7b',
confidence: 87,
timestamp: new Date().toISOString(),
additional: {
latency: 234,
tokens: 456
}
}
const feedback = await prisma.aiFeedback.create({
data: {
noteId: note.id,
userId: 'test-user-id',
feedbackType: 'thumbs_up',
feature: 'title_suggestion',
originalContent: 'Test suggestion',
metadata: JSON.stringify(metadata)
}
})
// Parse and verify metadata
const parsedMetadata = JSON.parse(feedback.metadata || '{}')
expect(parsedMetadata.aiProvider).toBe('ollama')
expect(parsedMetadata.confidence).toBe(87)
expect(parsedMetadata.additional).toBeDefined()
})
})
describe('MemoryEchoInsight Data Migration', () => {
test('should create and retrieve MemoryEchoInsight entries', async () => {
const note1 = await prisma.note.create({
data: {
title: 'Echo Note 1',
content: 'Content about programming',
userId: 'test-user-id'
}
})
const note2 = await prisma.note.create({
data: {
title: 'Echo Note 2',
content: 'Content about coding',
userId: 'test-user-id'
}
})
const insight = await prisma.memoryEchoInsight.create({
data: {
userId: 'test-user-id',
note1Id: note1.id,
note2Id: note2.id,
similarityScore: 0.85,
insight: 'These notes are similar because they both discuss programming concepts',
insightDate: new Date()
}
})
expect(insight.note1Id).toBe(note1.id)
expect(insight.note2Id).toBe(note2.id)
expect(insight.similarityScore).toBe(0.85)
expect(insight.insight).toBeDefined()
})
test('should handle insight feedback and dismissal', async () => {
const note1 = await prisma.note.create({
data: {
title: 'Echo Feedback Note 1',
content: 'Content A',
userId: 'test-user-id'
}
})
const note2 = await prisma.note.create({
data: {
title: 'Echo Feedback Note 2',
content: 'Content B',
userId: 'test-user-id'
}
})
const insight = await prisma.memoryEchoInsight.create({
data: {
userId: 'test-user-id',
note1Id: note1.id,
note2Id: note2.id,
similarityScore: 0.75,
insight: 'Test insight',
feedback: 'useful',
dismissed: false
}
})
expect(insight.feedback).toBe('useful')
expect(insight.dismissed).toBe(false)
// Update insight to mark as dismissed
const updatedInsight = await prisma.memoryEchoInsight.update({
where: { id: insight.id },
data: { dismissed: true }
})
expect(updatedInsight.dismissed).toBe(true)
})
})
describe('UserAISettings Data Migration', () => {
test('should create and retrieve UserAISettings', async () => {
const user = await prisma.user.create({
data: {
email: 'ai-settings@test.com',
name: 'AI Settings User'
}
})
const settings = await prisma.userAISettings.create({
data: {
userId: user.id,
titleSuggestions: true,
semanticSearch: false,
memoryEcho: true,
memoryEchoFrequency: 'weekly',
aiProvider: 'ollama',
preferredLanguage: 'fr'
}
})
expect(settings.userId).toBe(user.id)
expect(settings.titleSuggestions).toBe(true)
expect(settings.semanticSearch).toBe(false)
expect(settings.memoryEchoFrequency).toBe('weekly')
expect(settings.aiProvider).toBe('ollama')
expect(settings.preferredLanguage).toBe('fr')
})
test('should handle default values correctly', async () => {
const user = await prisma.user.create({
data: {
email: 'default-ai-settings@test.com',
name: 'Default AI User'
}
})
const settings = await prisma.userAISettings.create({
data: {
userId: user.id
// All other fields should use defaults
}
})
expect(settings.titleSuggestions).toBe(true)
expect(settings.semanticSearch).toBe(true)
expect(settings.memoryEcho).toBe(true)
expect(settings.memoryEchoFrequency).toBe('daily')
expect(settings.aiProvider).toBe('auto')
expect(settings.preferredLanguage).toBe('auto')
})
})
describe('Data Integrity', () => {
test('should verify no data loss after migration', async () => {
// Create initial data
const initialNotes = await createSampleNotes(prisma, 50)
const initialCount = initialNotes.length
// Simulate migration by querying data
const notesAfterMigration = await prisma.note.findMany()
const finalCount = notesAfterMigration.length
// Verify no data loss
expect(finalCount).toBe(initialCount)
// Verify each note's data is intact
for (const note of notesAfterMigration) {
expect(note.title).toBeDefined()
expect(note.content).toBeDefined()
}
})
test('should verify no data corruption after migration', async () => {
// Create notes with complex data
await prisma.note.create({
data: {
title: 'Complex Data Note',
content: 'This is a note with **markdown** formatting',
checkItems: JSON.stringify([{ text: 'Task 1', done: false }, { text: 'Task 2', done: true }]),
images: JSON.stringify([{ url: 'image1.jpg', caption: 'Caption 1' }]),
userId: 'test-user-id'
}
})
const note = await prisma.note.findFirst({
where: { title: 'Complex Data Note' }
})
// Verify complex data is preserved
expect(note?.content).toContain('**markdown**')
if (note?.checkItems) {
const checkItems = JSON.parse(note.checkItems)
expect(checkItems.length).toBe(2)
}
if (note?.images) {
const images = JSON.parse(note.images)
expect(images.length).toBe(1)
}
})
test('should maintain foreign key relationships', async () => {
// Create a user
const user = await prisma.user.create({
data: {
email: 'fk-test@test.com',
name: 'FK Test User'
}
})
// Create a notebook for the user
const notebook = await prisma.notebook.create({
data: {
name: 'FK Test Notebook',
order: 0,
userId: user.id
}
})
// Create notes in the notebook
await prisma.note.createMany({
data: [
{
title: 'FK Note 1',
content: 'Content 1',
userId: user.id,
notebookId: notebook.id
},
{
title: 'FK Note 2',
content: 'Content 2',
userId: user.id,
notebookId: notebook.id
}
]
})
// Verify relationships are maintained
const retrievedNotebook = await prisma.notebook.findUnique({
where: { id: notebook.id },
include: { notes: true }
})
expect(retrievedNotebook?.notes.length).toBe(2)
expect(retrievedNotebook?.userId).toBe(user.id)
})
})
describe('Edge Cases', () => {
test('should handle empty strings in text fields', async () => {
const note = await prisma.note.create({
data: {
title: '',
content: 'Content with empty title',
userId: 'test-user-id'
}
})
expect(note.title).toBe('')
expect(note.content).toBe('Content with empty title')
})
test('should handle very long text content', async () => {
const longContent = 'A'.repeat(10000)
const note = await prisma.note.create({
data: {
title: 'Long Content Note',
content: longContent,
userId: 'test-user-id'
}
})
expect(note.content).toHaveLength(10000)
})
test('should handle special characters in text fields', async () => {
const specialChars = 'Note with émojis 🎉 and spëcial çharacters & spåcial ñumbers 123'
const note = await prisma.note.create({
data: {
title: specialChars,
content: specialChars,
userId: 'test-user-id'
}
})
expect(note.title).toBe(specialChars)
expect(note.content).toBe(specialChars)
})
test('should handle null userId in some tables (optional relationships)', async () => {
const note = await prisma.note.create({
data: {
title: 'No User Note',
content: 'Note without userId',
userId: null
}
})
expect(note.userId).toBeNull()
})
})
describe('Migration Performance', () => {
test('should complete migration within acceptable time for 100 notes', async () => {
// Clean up
await prisma.note.deleteMany({})
// Create 100 notes and measure time
const { result, duration } = await measureExecutionTime(async () => {
await createSampleNotes(prisma, 100)
})
// Migration should complete quickly (< 5 seconds for 100 notes)
expect(duration).toBeLessThan(5000)
expect(result.length).toBe(100)
})
})
describe('Batch Operations', () => {
test('should handle batch insert of notes', async () => {
const notesData = Array.from({ length: 20 }, (_, i) => ({
title: `Batch Note ${i + 1}`,
content: `Batch content ${i + 1}`,
userId: 'test-user-id',
order: i
}))
await prisma.note.createMany({
data: notesData
})
const count = await prisma.note.count()
expect(count).toBe(20)
})
test('should handle batch insert of feedback', async () => {
const note = await prisma.note.create({
data: {
title: 'Batch Feedback Note',
content: 'Test content',
userId: 'test-user-id'
}
})
const feedbackData = Array.from({ length: 10 }, (_, i) => ({
noteId: note.id,
userId: 'test-user-id',
feedbackType: 'thumbs_up',
feature: 'title_suggestion',
originalContent: `Feedback ${i + 1}`
}))
await prisma.aiFeedback.createMany({
data: feedbackData
})
const count = await prisma.aiFeedback.count()
expect(count).toBe(10)
})
})
})