Implement permission utilities (task 9)

- Created lib/permissions.ts with four utility functions:
  - canAccessList: Check if user can view a list
  - canEditList: Check if user can edit a list or its items
  - canDeleteList: Check if user can delete a list (owner only)
  - getPermission: Get user's permission level for a list
- Added comprehensive unit tests in lib/__tests__/permissions.test.ts
- All 24 tests passing
- Validates Requirements 4.3, 6.2, 6.3
This commit is contained in:
2025-12-02 00:52:48 +00:00
parent 0bb293e101
commit a8962247fb
3 changed files with 433 additions and 1 deletions

View File

@@ -0,0 +1,268 @@
/**
* Tests for permission utility functions
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { prisma } from '../db'
import {
canAccessList,
canEditList,
canDeleteList,
getPermission,
} from '../permissions'
import bcrypt from 'bcryptjs'
describe('Permission Utilities', () => {
let testUser1Id: string
let testUser2Id: string
let testListId: string
beforeEach(async () => {
// Create test users
const passwordHash = await bcrypt.hash('password123', 10)
const user1 = await prisma.user.create({
data: {
email: `test-perm-user1-${Date.now()}@example.com`,
passwordHash,
name: 'Test User 1',
},
})
testUser1Id = user1.id
const user2 = await prisma.user.create({
data: {
email: `test-perm-user2-${Date.now()}@example.com`,
passwordHash,
name: 'Test User 2',
},
})
testUser2Id = user2.id
// Create a test list owned by user1
const list = await prisma.list.create({
data: {
title: 'Test List',
color: '#3B82F6',
ownerId: testUser1Id,
},
})
testListId = list.id
})
afterEach(async () => {
// Clean up test data
await prisma.list.deleteMany({
where: {
id: testListId,
},
})
await prisma.user.deleteMany({
where: {
id: {
in: [testUser1Id, testUser2Id],
},
},
})
})
describe('canAccessList', () => {
it('should return true for list owner', async () => {
const result = await canAccessList(testUser1Id, testListId)
expect(result).toBe(true)
})
it('should return false for non-owner without collaboration', async () => {
const result = await canAccessList(testUser2Id, testListId)
expect(result).toBe(false)
})
it('should return true for editor collaborator', async () => {
// Add user2 as editor
await prisma.collaborator.create({
data: {
userId: testUser2Id,
listId: testListId,
permission: 'editor',
},
})
const result = await canAccessList(testUser2Id, testListId)
expect(result).toBe(true)
})
it('should return true for viewer collaborator', async () => {
// Add user2 as viewer
await prisma.collaborator.create({
data: {
userId: testUser2Id,
listId: testListId,
permission: 'viewer',
},
})
const result = await canAccessList(testUser2Id, testListId)
expect(result).toBe(true)
})
it('should return false for invalid user ID', async () => {
const result = await canAccessList('', testListId)
expect(result).toBe(false)
})
it('should return false for invalid list ID', async () => {
const result = await canAccessList(testUser1Id, '')
expect(result).toBe(false)
})
})
describe('canEditList', () => {
it('should return true for list owner', async () => {
const result = await canEditList(testUser1Id, testListId)
expect(result).toBe(true)
})
it('should return false for non-owner without collaboration', async () => {
const result = await canEditList(testUser2Id, testListId)
expect(result).toBe(false)
})
it('should return true for editor collaborator', async () => {
// Add user2 as editor
await prisma.collaborator.create({
data: {
userId: testUser2Id,
listId: testListId,
permission: 'editor',
},
})
const result = await canEditList(testUser2Id, testListId)
expect(result).toBe(true)
})
it('should return false for viewer collaborator', async () => {
// Add user2 as viewer
await prisma.collaborator.create({
data: {
userId: testUser2Id,
listId: testListId,
permission: 'viewer',
},
})
const result = await canEditList(testUser2Id, testListId)
expect(result).toBe(false)
})
it('should return false for invalid user ID', async () => {
const result = await canEditList('', testListId)
expect(result).toBe(false)
})
it('should return false for invalid list ID', async () => {
const result = await canEditList(testUser1Id, '')
expect(result).toBe(false)
})
})
describe('canDeleteList', () => {
it('should return true for list owner', async () => {
const result = await canDeleteList(testUser1Id, testListId)
expect(result).toBe(true)
})
it('should return false for non-owner', async () => {
const result = await canDeleteList(testUser2Id, testListId)
expect(result).toBe(false)
})
it('should return false for editor collaborator', async () => {
// Add user2 as editor
await prisma.collaborator.create({
data: {
userId: testUser2Id,
listId: testListId,
permission: 'editor',
},
})
const result = await canDeleteList(testUser2Id, testListId)
expect(result).toBe(false)
})
it('should return false for viewer collaborator', async () => {
// Add user2 as viewer
await prisma.collaborator.create({
data: {
userId: testUser2Id,
listId: testListId,
permission: 'viewer',
},
})
const result = await canDeleteList(testUser2Id, testListId)
expect(result).toBe(false)
})
it('should return false for invalid user ID', async () => {
const result = await canDeleteList('', testListId)
expect(result).toBe(false)
})
it('should return false for invalid list ID', async () => {
const result = await canDeleteList(testUser1Id, '')
expect(result).toBe(false)
})
})
describe('getPermission', () => {
it('should return "owner" for list owner', async () => {
const result = await getPermission(testUser1Id, testListId)
expect(result).toBe('owner')
})
it('should return null for non-owner without collaboration', async () => {
const result = await getPermission(testUser2Id, testListId)
expect(result).toBeNull()
})
it('should return "editor" for editor collaborator', async () => {
// Add user2 as editor
await prisma.collaborator.create({
data: {
userId: testUser2Id,
listId: testListId,
permission: 'editor',
},
})
const result = await getPermission(testUser2Id, testListId)
expect(result).toBe('editor')
})
it('should return "viewer" for viewer collaborator', async () => {
// Add user2 as viewer
await prisma.collaborator.create({
data: {
userId: testUser2Id,
listId: testListId,
permission: 'viewer',
},
})
const result = await getPermission(testUser2Id, testListId)
expect(result).toBe('viewer')
})
it('should return null for invalid user ID', async () => {
const result = await getPermission('', testListId)
expect(result).toBeNull()
})
it('should return null for invalid list ID', async () => {
const result = await getPermission(testUser1Id, '')
expect(result).toBeNull()
})
})
})

164
lib/permissions.ts Normal file
View File

@@ -0,0 +1,164 @@
'use server'
import { prisma } from './db'
export type Permission = 'owner' | 'editor' | 'viewer'
/**
* Check if a user can access a list (view it)
* Users can access a list if they are the owner or a collaborator
*
* @param userId - The ID of the user
* @param listId - The ID of the list
* @returns true if the user can access the list, false otherwise
*/
export async function canAccessList(
userId: string,
listId: string
): Promise<boolean> {
try {
if (!userId || !listId) {
return false
}
const list = await prisma.list.findFirst({
where: {
id: listId,
OR: [
{ ownerId: userId },
{
collaborators: {
some: {
userId: userId,
},
},
},
],
},
})
return list !== null
} catch (error) {
console.error('Error checking list access:', error)
return false
}
}
/**
* Check if a user can edit a list or its items
* Users can edit if they are the owner or have editor permission
*
* @param userId - The ID of the user
* @param listId - The ID of the list
* @returns true if the user can edit the list, false otherwise
*/
export async function canEditList(
userId: string,
listId: string
): Promise<boolean> {
try {
if (!userId || !listId) {
return false
}
const list = await prisma.list.findFirst({
where: {
id: listId,
OR: [
{ ownerId: userId },
{
collaborators: {
some: {
userId: userId,
permission: 'editor',
},
},
},
],
},
})
return list !== null
} catch (error) {
console.error('Error checking list edit permission:', error)
return false
}
}
/**
* Check if a user can delete a list
* Only the owner can delete a list
*
* @param userId - The ID of the user
* @param listId - The ID of the list
* @returns true if the user can delete the list, false otherwise
*/
export async function canDeleteList(
userId: string,
listId: string
): Promise<boolean> {
try {
if (!userId || !listId) {
return false
}
const list = await prisma.list.findFirst({
where: {
id: listId,
ownerId: userId,
},
})
return list !== null
} catch (error) {
console.error('Error checking list delete permission:', error)
return false
}
}
/**
* Get the permission level a user has for a list
*
* @param userId - The ID of the user
* @param listId - The ID of the list
* @returns The permission level ('owner', 'editor', 'viewer') or null if no access
*/
export async function getPermission(
userId: string,
listId: string
): Promise<Permission | null> {
try {
if (!userId || !listId) {
return null
}
// Check if user is the owner
const ownedList = await prisma.list.findFirst({
where: {
id: listId,
ownerId: userId,
},
})
if (ownedList) {
return 'owner'
}
// Check if user is a collaborator
const collaborator = await prisma.collaborator.findFirst({
where: {
userId: userId,
listId: listId,
},
})
if (collaborator) {
return collaborator.permission as Permission
}
return null
} catch (error) {
console.error('Error getting user permission:', error)
return null
}
}