diff --git a/src/lib/server/db/collaborators.test.ts b/src/lib/server/db/collaborators.test.ts new file mode 100644 index 0000000..2a634fc --- /dev/null +++ b/src/lib/server/db/collaborators.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createTestDb } from './test-helpers.js'; +import { notes, noteCollaborators, noteUserState, users } from './schema.js'; +import { eq, and } from 'drizzle-orm'; +import { v4 as uuidv4 } from 'uuid'; +import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'; +import type * as schema from './schema.js'; + +let db: BetterSQLite3Database; + +function seedTwoUsers(db: BetterSQLite3Database) { + db.insert(users) + .values([ + { email: 'owner@test.com', displayName: 'Owner', role: 'admin', authProvider: 'password', createdAt: new Date() }, + { email: 'collab@test.com', displayName: 'Collaborator', role: 'user', authProvider: 'password', createdAt: new Date() } + ]) + .run(); +} + +function seedNote(db: BetterSQLite3Database, id: string, userId: number) { + const now = new Date(); + db.insert(notes).values({ id, userId, title: 'Test', content: '', createdAt: now, updatedAt: now }).run(); +} + +beforeEach(() => { + const testDb = createTestDb(); + db = testDb.db; + seedTwoUsers(db); +}); + +describe('noteCollaborators table', () => { + it('should add a collaborator to a note', () => { + seedNote(db, 'n1', 1); + db.insert(noteCollaborators) + .values({ noteId: 'n1', userId: 2, addedBy: 1, addedAt: new Date() }) + .run(); + + const result = db.select().from(noteCollaborators).where(eq(noteCollaborators.noteId, 'n1')).all(); + expect(result).toHaveLength(1); + expect(result[0].userId).toBe(2); + expect(result[0].addedBy).toBe(1); + }); + + it('should enforce unique constraint on (noteId, userId)', () => { + seedNote(db, 'n1', 1); + db.insert(noteCollaborators) + .values({ noteId: 'n1', userId: 2, addedBy: 1, addedAt: new Date() }) + .run(); + + expect(() => + db.insert(noteCollaborators) + .values({ noteId: 'n1', userId: 2, addedBy: 1, addedAt: new Date() }) + .run() + ).toThrow(); + }); + + it('should allow multiple collaborators per note', () => { + seedNote(db, 'n1', 1); + // Add a third user + db.insert(users) + .values({ email: 'third@test.com', displayName: 'Third', role: 'user', authProvider: 'password', createdAt: new Date() }) + .run(); + + db.insert(noteCollaborators).values([ + { noteId: 'n1', userId: 2, addedBy: 1, addedAt: new Date() }, + { noteId: 'n1', userId: 3, addedBy: 1, addedAt: new Date() } + ]).run(); + + const result = db.select().from(noteCollaborators).where(eq(noteCollaborators.noteId, 'n1')).all(); + expect(result).toHaveLength(2); + }); + + it('should remove a collaborator', () => { + seedNote(db, 'n1', 1); + db.insert(noteCollaborators) + .values({ noteId: 'n1', userId: 2, addedBy: 1, addedAt: new Date() }) + .run(); + + db.delete(noteCollaborators) + .where(and(eq(noteCollaborators.noteId, 'n1'), eq(noteCollaborators.userId, 2))) + .run(); + + const result = db.select().from(noteCollaborators).where(eq(noteCollaborators.noteId, 'n1')).all(); + expect(result).toHaveLength(0); + }); +}); + +describe('noteUserState table', () => { + it('should store per-user pin/archive/sortOrder state', () => { + seedNote(db, 'n1', 1); + db.insert(noteUserState) + .values({ noteId: 'n1', userId: 2, pinned: true, archived: false, sortOrder: 5 }) + .run(); + + const result = db.select().from(noteUserState) + .where(and(eq(noteUserState.noteId, 'n1'), eq(noteUserState.userId, 2))) + .get(); + + expect(result).toBeDefined(); + expect(result!.pinned).toBe(true); + expect(result!.archived).toBe(false); + expect(result!.sortOrder).toBe(5); + }); + + it('should enforce unique constraint on (noteId, userId)', () => { + seedNote(db, 'n1', 1); + db.insert(noteUserState) + .values({ noteId: 'n1', userId: 2, pinned: false, archived: false, sortOrder: 0 }) + .run(); + + expect(() => + db.insert(noteUserState) + .values({ noteId: 'n1', userId: 2, pinned: true, archived: false, sortOrder: 0 }) + .run() + ).toThrow(); + }); + + it('should default to false/false/0 for new state', () => { + seedNote(db, 'n1', 1); + db.insert(noteUserState) + .values({ noteId: 'n1', userId: 2 }) + .run(); + + const result = db.select().from(noteUserState) + .where(and(eq(noteUserState.noteId, 'n1'), eq(noteUserState.userId, 2))) + .get(); + + expect(result!.pinned).toBe(false); + expect(result!.archived).toBe(false); + expect(result!.sortOrder).toBe(0); + }); + + it('should allow independent state per user per note', () => { + seedNote(db, 'n1', 1); + db.insert(users) + .values({ email: 'third@test.com', displayName: 'Third', role: 'user', authProvider: 'password', createdAt: new Date() }) + .run(); + + db.insert(noteUserState).values([ + { noteId: 'n1', userId: 2, pinned: true, archived: false, sortOrder: 1 }, + { noteId: 'n1', userId: 3, pinned: false, archived: true, sortOrder: 2 } + ]).run(); + + const user2State = db.select().from(noteUserState) + .where(and(eq(noteUserState.noteId, 'n1'), eq(noteUserState.userId, 2))).get(); + const user3State = db.select().from(noteUserState) + .where(and(eq(noteUserState.noteId, 'n1'), eq(noteUserState.userId, 3))).get(); + + expect(user2State!.pinned).toBe(true); + expect(user2State!.archived).toBe(false); + expect(user3State!.pinned).toBe(false); + expect(user3State!.archived).toBe(true); + }); +});