- Created migration-actions.ts with server actions for claiming lists - Added ClaimListsModal component for UI during registration - Integrated claim modal into RegisterForm - Created anonymous-lists.ts utilities for tracking lists in localStorage - Added migrate-lists.ts script for deployment migration - Created comprehensive MIGRATION_GUIDE.md documentation - Added migration tests with 100% pass rate - Updated package.json with db:migrate script - Added tsx as dev dependency for running migration scripts Requirements: 10.2, 10.3, 10.4, 10.5 - Users can claim existing lists on first registration - All list data is preserved during migration - All lists have an owner after migration - Migrated lists appear in user's account
202 lines
5.9 KiB
TypeScript
202 lines
5.9 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { prisma } from '../db'
|
|
import { claimLists, getClaimableLists, hasExistingLists } from '../migration-actions'
|
|
import bcrypt from 'bcryptjs'
|
|
|
|
// Mock auth
|
|
vi.mock('@/auth', () => ({
|
|
auth: vi.fn(),
|
|
}))
|
|
|
|
const { auth } = await import('@/auth')
|
|
|
|
describe('Migration Actions', () => {
|
|
let testUser: any
|
|
let testLists: any[]
|
|
|
|
beforeEach(async () => {
|
|
// Clean up test data
|
|
await prisma.list.deleteMany({})
|
|
await prisma.user.deleteMany({})
|
|
|
|
// Create test user
|
|
const passwordHash = await bcrypt.hash('password123', 10)
|
|
testUser = await prisma.user.create({
|
|
data: {
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
passwordHash,
|
|
},
|
|
})
|
|
|
|
// Create test lists owned by test user
|
|
testLists = await Promise.all([
|
|
prisma.list.create({
|
|
data: {
|
|
title: 'Test List 1',
|
|
color: '#FF0000',
|
|
ownerId: testUser.id,
|
|
},
|
|
}),
|
|
prisma.list.create({
|
|
data: {
|
|
title: 'Test List 2',
|
|
color: '#00FF00',
|
|
ownerId: testUser.id,
|
|
},
|
|
}),
|
|
])
|
|
})
|
|
|
|
describe('getClaimableLists', () => {
|
|
it('should return lists that exist in the database', async () => {
|
|
const listIds = testLists.map(l => l.id)
|
|
const claimable = await getClaimableLists(listIds)
|
|
|
|
expect(claimable).toHaveLength(2)
|
|
expect(claimable[0].title).toBe('Test List 1')
|
|
expect(claimable[1].title).toBe('Test List 2')
|
|
expect(claimable[0].itemCount).toBe(0)
|
|
})
|
|
|
|
it('should return empty array for non-existent list IDs', async () => {
|
|
const claimable = await getClaimableLists(['non-existent-id'])
|
|
expect(claimable).toHaveLength(0)
|
|
})
|
|
|
|
it('should return empty array for empty input', async () => {
|
|
const claimable = await getClaimableLists([])
|
|
expect(claimable).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('claimLists', () => {
|
|
let newUser: any
|
|
|
|
beforeEach(async () => {
|
|
// Create a new user to claim lists
|
|
const passwordHash = await bcrypt.hash('password456', 10)
|
|
newUser = await prisma.user.create({
|
|
data: {
|
|
email: 'newuser@example.com',
|
|
name: 'New User',
|
|
passwordHash,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('should transfer lists to the authenticated user', async () => {
|
|
// Mock auth to return new user
|
|
vi.mocked(auth).mockResolvedValue({
|
|
user: { id: newUser.id, email: newUser.email, name: newUser.name },
|
|
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
})
|
|
|
|
const listIds = testLists.map(l => l.id)
|
|
const result = await claimLists(listIds)
|
|
|
|
expect(result.success).toBe(true)
|
|
expect(result.migratedCount).toBe(2)
|
|
|
|
// Verify lists are now owned by new user
|
|
const updatedLists = await prisma.list.findMany({
|
|
where: { id: { in: listIds } },
|
|
})
|
|
|
|
updatedLists.forEach(list => {
|
|
expect(list.ownerId).toBe(newUser.id)
|
|
})
|
|
})
|
|
|
|
it('should preserve all list data during migration', async () => {
|
|
// Mock auth
|
|
vi.mocked(auth).mockResolvedValue({
|
|
user: { id: newUser.id, email: newUser.email, name: newUser.name },
|
|
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
})
|
|
|
|
const originalList = testLists[0]
|
|
const listIds = [originalList.id]
|
|
|
|
await claimLists(listIds)
|
|
|
|
// Verify list data is preserved
|
|
const updatedList = await prisma.list.findUnique({
|
|
where: { id: originalList.id },
|
|
})
|
|
|
|
expect(updatedList).toBeDefined()
|
|
expect(updatedList!.title).toBe(originalList.title)
|
|
expect(updatedList!.color).toBe(originalList.color)
|
|
expect(updatedList!.createdAt.getTime()).toBe(originalList.createdAt.getTime())
|
|
// Only ownerId should change
|
|
expect(updatedList!.ownerId).toBe(newUser.id)
|
|
})
|
|
|
|
it('should return error when not authenticated', async () => {
|
|
// Mock auth to return null
|
|
vi.mocked(auth).mockResolvedValue(null)
|
|
|
|
const result = await claimLists([testLists[0].id])
|
|
|
|
expect(result.success).toBe(false)
|
|
expect(result.error).toContain('Unauthorized')
|
|
})
|
|
|
|
it('should handle empty list IDs', async () => {
|
|
// Mock auth
|
|
vi.mocked(auth).mockResolvedValue({
|
|
user: { id: newUser.id, email: newUser.email, name: newUser.name },
|
|
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
})
|
|
|
|
const result = await claimLists([])
|
|
|
|
expect(result.success).toBe(true)
|
|
expect(result.migratedCount).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('hasExistingLists', () => {
|
|
it('should return true when user has lists', async () => {
|
|
// Mock auth
|
|
vi.mocked(auth).mockResolvedValue({
|
|
user: { id: testUser.id, email: testUser.email, name: testUser.name },
|
|
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
})
|
|
|
|
const result = await hasExistingLists()
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('should return false when user has no lists', async () => {
|
|
// Create user with no lists
|
|
const passwordHash = await bcrypt.hash('password789', 10)
|
|
const emptyUser = await prisma.user.create({
|
|
data: {
|
|
email: 'empty@example.com',
|
|
name: 'Empty User',
|
|
passwordHash,
|
|
},
|
|
})
|
|
|
|
// Mock auth
|
|
vi.mocked(auth).mockResolvedValue({
|
|
user: { id: emptyUser.id, email: emptyUser.email, name: emptyUser.name },
|
|
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
})
|
|
|
|
const result = await hasExistingLists()
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('should return false when not authenticated', async () => {
|
|
// Mock auth to return null
|
|
vi.mocked(auth).mockResolvedValue(null)
|
|
|
|
const result = await hasExistingLists()
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
})
|