Files
Keep/keep-notes/tests/migration/rollback.test.ts
2026-04-17 21:14:43 +02:00

418 lines
12 KiB
TypeScript

/**
* Rollback Tests
* Validates that migrations can be safely rolled back
* Tests schema rollback, data recovery, and cleanup
* Updated for PostgreSQL
*/
import { PrismaClient } from '@prisma/client'
import {
createTestPrismaClient,
initializeTestDatabase,
cleanupTestDatabase,
createSampleNotes,
createSampleAINotes,
verifyTableExists,
verifyColumnExists,
} from './setup'
describe('Rollback Tests', () => {
let prisma: PrismaClient
beforeAll(async () => {
prisma = createTestPrismaClient()
await initializeTestDatabase(prisma)
})
afterAll(async () => {
await cleanupTestDatabase(prisma)
})
describe('Schema Rollback', () => {
test('should verify schema state before migration', async () => {
const hasUser = await verifyTableExists(prisma, 'User')
expect(hasUser).toBe(true)
})
test('should verify AI tables exist after migration', async () => {
const hasAiFeedback = await verifyTableExists(prisma, 'AiFeedback')
expect(hasAiFeedback).toBe(true)
const hasMemoryEcho = await verifyTableExists(prisma, 'MemoryEchoInsight')
expect(hasMemoryEcho).toBe(true)
const hasUserAISettings = await verifyTableExists(prisma, 'UserAISettings')
expect(hasUserAISettings).toBe(true)
})
test('should verify Note AI columns exist after migration', async () => {
const aiColumns = ['autoGenerated', 'aiProvider', 'aiConfidence', 'language', 'languageConfidence', 'lastAiAnalysis']
for (const column of aiColumns) {
const exists = await verifyColumnExists(prisma, 'Note', column)
expect(exists).toBe(true)
}
})
test('should simulate dropping AI columns (rollback scenario)', async () => {
// In PostgreSQL, ALTER TABLE DROP COLUMN works directly
// This test verifies we can identify which columns would be dropped
const aiColumns = ['autoGenerated', 'aiProvider', 'aiConfidence', 'language', 'languageConfidence', 'lastAiAnalysis']
for (const column of aiColumns) {
const exists = await verifyColumnExists(prisma, 'Note', column)
expect(exists).toBe(true)
}
})
test('should simulate dropping AI tables (rollback scenario)', async () => {
const aiTables = ['AiFeedback', 'MemoryEchoInsight', 'UserAISettings']
for (const table of aiTables) {
const exists = await verifyTableExists(prisma, table)
expect(exists).toBe(true)
}
})
})
describe('Data Recovery After Rollback', () => {
beforeEach(async () => {
// Clean up before each test
await prisma.aiFeedback.deleteMany({})
await prisma.note.deleteMany({})
})
test('should preserve basic note data if AI columns are dropped', async () => {
const noteWithAI = await prisma.note.create({
data: {
title: 'Note with AI',
content: 'This note has AI fields',
userId: 'test-user-id',
autoGenerated: true,
aiProvider: 'openai',
aiConfidence: 95,
language: 'en',
languageConfidence: 0.98,
lastAiAnalysis: new Date()
}
})
expect(noteWithAI.id).toBeDefined()
expect(noteWithAI.title).toBe('Note with AI')
expect(noteWithAI.content).toBe('This note has AI fields')
expect(noteWithAI.userId).toBe('test-user-id')
const basicNote = await prisma.note.findUnique({
where: { id: noteWithAI.id },
select: {
id: true,
title: true,
content: true,
userId: true
}
})
expect(basicNote?.id).toBe(noteWithAI.id)
expect(basicNote?.title).toBe(noteWithAI.title)
expect(basicNote?.content).toBe(noteWithAI.content)
})
test('should preserve note relationships if AI tables are dropped', async () => {
const user = await prisma.user.create({
data: {
email: 'rollback-test@test.com',
name: 'Rollback User'
}
})
const notebook = await prisma.notebook.create({
data: {
name: 'Rollback Notebook',
order: 0,
userId: user.id
}
})
const note = await prisma.note.create({
data: {
title: 'Rollback Test Note',
content: 'Test content',
userId: user.id,
notebookId: notebook.id
}
})
expect(note.userId).toBe(user.id)
expect(note.notebookId).toBe(notebook.id)
const retrievedNote = await prisma.note.findUnique({
where: { id: note.id },
include: {
notebook: true,
user: true
}
})
expect(retrievedNote?.userId).toBe(user.id)
expect(retrievedNote?.notebookId).toBe(notebook.id)
})
test('should handle orphaned records after table drop', async () => {
const note = await prisma.note.create({
data: {
title: 'Orphan 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: 'Test feedback'
}
})
expect(feedback.noteId).toBe(note.id)
const noteExists = await prisma.note.findUnique({
where: { id: note.id }
})
expect(noteExists).toBeDefined()
expect(noteExists?.id).toBe(note.id)
})
test('should verify no orphaned records exist after proper migration', async () => {
const note = await prisma.note.create({
data: {
title: 'Orphan Check Note',
content: 'Test content',
userId: 'test-user-id'
}
})
await prisma.aiFeedback.create({
data: {
noteId: note.id,
userId: 'test-user-id',
feedbackType: 'thumbs_up',
feature: 'title_suggestion',
originalContent: 'Test feedback'
}
})
const allFeedback = await prisma.aiFeedback.findMany()
for (const fb of allFeedback) {
const noteExists = await prisma.note.findUnique({
where: { id: fb.noteId }
})
expect(noteExists).toBeDefined()
}
})
})
describe('Rollback Safety Checks', () => {
test('should verify data before attempting rollback', async () => {
await createSampleNotes(prisma, 10)
const noteCountBefore = await prisma.note.count()
expect(noteCountBefore).toBe(10)
const notes = await prisma.note.findMany()
expect(notes.length).toBe(10)
for (const note of notes) {
expect(note.id).toBeDefined()
expect(note.title).toBeDefined()
expect(note.content).toBeDefined()
}
})
test('should identify tables created by migration', async () => {
const aiTables = ['AiFeedback', 'MemoryEchoInsight', 'UserAISettings']
let found = 0
for (const table of aiTables) {
const exists = await verifyTableExists(prisma, table)
if (exists) found++
}
expect(found).toBeGreaterThanOrEqual(3)
})
test('should identify columns added by migration', async () => {
const aiColumns = ['autoGenerated', 'aiProvider', 'aiConfidence', 'language', 'languageConfidence', 'lastAiAnalysis']
let found = 0
for (const column of aiColumns) {
const exists = await verifyColumnExists(prisma, 'Note', column)
if (exists) found++
}
expect(found).toBe(6)
})
})
describe('Rollback with Data', () => {
test('should preserve essential note data', async () => {
const notes = await createSampleAINotes(prisma, 20)
for (const note of notes) {
expect(note.id).toBeDefined()
expect(note.content).toBeDefined()
}
const allNotes = await prisma.note.findMany()
expect(allNotes.length).toBe(20)
})
test('should handle rollback with complex data structures', async () => {
// With PostgreSQL + Prisma Json type, data is stored as native JSONB
const complexNote = await prisma.note.create({
data: {
title: 'Complex Note',
content: '**Markdown** content with [links](https://example.com)',
checkItems: [
{ text: 'Task 1', done: false },
{ text: 'Task 2', done: true },
{ text: 'Task 3', done: false }
],
images: [
{ url: 'image1.jpg', caption: 'Caption 1' },
{ url: 'image2.jpg', caption: 'Caption 2' }
],
labels: ['label1', 'label2', 'label3'],
userId: 'test-user-id'
}
})
const retrieved = await prisma.note.findUnique({
where: { id: complexNote.id }
})
expect(retrieved?.content).toContain('**Markdown**')
expect(retrieved?.checkItems).toBeDefined()
expect(retrieved?.images).toBeDefined()
expect(retrieved?.labels).toBeDefined()
// Json fields come back already parsed
if (retrieved?.checkItems) {
const checkItems = retrieved.checkItems as any[]
expect(checkItems.length).toBe(3)
}
})
})
describe('Rollback Error Handling', () => {
test('should handle rollback when AI data exists', async () => {
await createSampleAINotes(prisma, 10)
const aiNotes = await prisma.note.findMany({
where: {
OR: [
{ autoGenerated: true },
{ aiProvider: { not: null } },
{ language: { not: null } }
]
}
})
expect(aiNotes.length).toBeGreaterThan(0)
const hasAIData = await prisma.note.findFirst({
where: {
autoGenerated: true
}
})
expect(hasAIData).toBeDefined()
})
test('should handle rollback when feedback exists', async () => {
const note = await prisma.note.create({
data: {
title: 'Feedback Note',
content: 'Test content',
userId: 'test-user-id'
}
})
await prisma.aiFeedback.createMany({
data: [
{
noteId: note.id,
userId: 'test-user-id',
feedbackType: 'thumbs_up',
feature: 'title_suggestion',
originalContent: 'Feedback 1'
},
{
noteId: note.id,
userId: 'test-user-id',
feedbackType: 'thumbs_down',
feature: 'semantic_search',
originalContent: 'Feedback 2'
}
]
})
const feedbackCount = await prisma.aiFeedback.count()
expect(feedbackCount).toBeGreaterThanOrEqual(2)
const feedbacks = await prisma.aiFeedback.findMany()
expect(feedbacks.length).toBeGreaterThanOrEqual(2)
})
})
describe('Rollback Validation', () => {
test('should validate database state after simulated rollback', async () => {
await createSampleNotes(prisma, 5)
const noteCount = await prisma.note.count()
expect(noteCount).toBeGreaterThanOrEqual(5)
const notes = await prisma.note.findMany()
expect(notes.every(n => n.id && n.content)).toBe(true)
})
test('should verify no data corruption in core tables', async () => {
const user = await prisma.user.create({
data: {
email: 'corruption-test@test.com',
name: 'Corruption Test User'
}
})
const notebook = await prisma.notebook.create({
data: {
name: 'Corruption Test Notebook',
order: 0,
userId: user.id
}
})
await prisma.note.create({
data: {
title: 'Corruption Test Note',
content: 'Test content',
userId: user.id,
notebookId: notebook.id
}
})
const retrievedUser = await prisma.user.findUnique({
where: { id: user.id },
include: { notebooks: true, notes: true }
})
expect(retrievedUser?.notebooks.length).toBe(1)
expect(retrievedUser?.notes.length).toBe(1)
})
})
})