test: add unit test coverage for services and lib
- 255 tests across 14 files (was 70 tests in 4 files) - src/services/__tests__: auth (registerUser, updateUserPassword, updateUserProfile), okrs (calculateOKRProgress, createOKR, updateKeyResult, updateOKR), teams (createTeam, addTeamMember, isAdminOfUser, getTeamMemberIdsForAdminTeams, getUserTeams), weather (getPreviousWeatherEntriesForUsers, shareWeatherSessionToTeam, getWeatherSessionsHistory), workshops (createSwotItem, duplicateSwotItem, updateAction, createMotivatorSession, updateCardInfluence, addGifMoodItem, shareGifMoodSessionToTeam, getLatestEventTimestamp, cleanupOldEvents) - src/lib/__tests__: date-utils, weather-utils, okr-utils, gravatar, workshops, share-utils - Update vitest coverage to include src/lib/** Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
156
src/services/__tests__/session-permissions.test.ts
Normal file
156
src/services/__tests__/session-permissions.test.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
createSessionPermissionChecks,
|
||||
withAdminFallback,
|
||||
canDeleteByOwner,
|
||||
} from '@/services/session-permissions';
|
||||
|
||||
vi.mock('@/services/teams', () => ({
|
||||
isAdminOfUser: vi.fn(),
|
||||
}));
|
||||
|
||||
const { isAdminOfUser } = await import('@/services/teams');
|
||||
const mockIsAdminOfUser = vi.mocked(isAdminOfUser);
|
||||
|
||||
// Factory for mock Prisma model delegate
|
||||
function makeModel(countResult: number, ownerId: string | null = 'owner-1') {
|
||||
return {
|
||||
count: vi.fn().mockResolvedValue(countResult),
|
||||
findUnique: vi.fn().mockResolvedValue(ownerId ? { userId: ownerId } : null),
|
||||
};
|
||||
}
|
||||
|
||||
describe('createSessionPermissionChecks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockIsAdminOfUser.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
describe('canAccess', () => {
|
||||
it('returns true when user has direct access (count > 0)', async () => {
|
||||
const { canAccess } = createSessionPermissionChecks(makeModel(1));
|
||||
expect(await canAccess('session-1', 'user-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when no direct access but user is team admin', async () => {
|
||||
mockIsAdminOfUser.mockResolvedValue(true);
|
||||
const { canAccess } = createSessionPermissionChecks(makeModel(0, 'owner-1'));
|
||||
expect(await canAccess('session-1', 'admin-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when no direct access and not admin', async () => {
|
||||
const { canAccess } = createSessionPermissionChecks(makeModel(0, 'owner-1'));
|
||||
expect(await canAccess('session-1', 'stranger')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when no direct access and session owner not found', async () => {
|
||||
const { canAccess } = createSessionPermissionChecks(makeModel(0, null));
|
||||
expect(await canAccess('session-1', 'anyone')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canEdit', () => {
|
||||
it('returns true when user is owner or editor (count > 0)', async () => {
|
||||
const { canEdit } = createSessionPermissionChecks(makeModel(1));
|
||||
expect(await canEdit('session-1', 'editor-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for viewer (count = 0) when not admin', async () => {
|
||||
const { canEdit } = createSessionPermissionChecks(makeModel(0, 'owner-1'));
|
||||
expect(await canEdit('session-1', 'viewer-1')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for viewer when user is team admin', async () => {
|
||||
mockIsAdminOfUser.mockResolvedValue(true);
|
||||
const { canEdit } = createSessionPermissionChecks(makeModel(0, 'owner-1'));
|
||||
expect(await canEdit('session-1', 'admin-1')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canDelete', () => {
|
||||
it('returns true for session owner', async () => {
|
||||
const { canDelete } = createSessionPermissionChecks(makeModel(1, 'owner-1'));
|
||||
expect(await canDelete('session-1', 'owner-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-owner even with EDITOR role', async () => {
|
||||
const { canDelete } = createSessionPermissionChecks(makeModel(1, 'owner-1'));
|
||||
expect(await canDelete('session-1', 'editor-1')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when user is team admin of the owner', async () => {
|
||||
mockIsAdminOfUser.mockResolvedValue(true);
|
||||
const { canDelete } = createSessionPermissionChecks(makeModel(0, 'owner-1'));
|
||||
expect(await canDelete('session-1', 'admin-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when session not found', async () => {
|
||||
const { canDelete } = createSessionPermissionChecks(makeModel(0, null));
|
||||
expect(await canDelete('session-1', 'anyone')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('withAdminFallback', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockIsAdminOfUser.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it('returns true immediately when hasDirectAccess is true (no admin check)', async () => {
|
||||
const getOwnerId = vi.fn();
|
||||
const result = await withAdminFallback(true, getOwnerId, 'session-1', 'user-1');
|
||||
expect(result).toBe(true);
|
||||
expect(getOwnerId).not.toHaveBeenCalled();
|
||||
expect(mockIsAdminOfUser).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to admin check when no direct access', async () => {
|
||||
mockIsAdminOfUser.mockResolvedValue(true);
|
||||
const getOwnerId = vi.fn().mockResolvedValue('owner-1');
|
||||
const result = await withAdminFallback(false, getOwnerId, 'session-1', 'admin-1');
|
||||
expect(result).toBe(true);
|
||||
expect(mockIsAdminOfUser).toHaveBeenCalledWith('owner-1', 'admin-1');
|
||||
});
|
||||
|
||||
it('returns false when no direct access and not admin', async () => {
|
||||
const getOwnerId = vi.fn().mockResolvedValue('owner-1');
|
||||
const result = await withAdminFallback(false, getOwnerId, 'session-1', 'stranger');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when owner not found', async () => {
|
||||
const getOwnerId = vi.fn().mockResolvedValue(null);
|
||||
const result = await withAdminFallback(false, getOwnerId, 'session-1', 'anyone');
|
||||
expect(result).toBe(false);
|
||||
expect(mockIsAdminOfUser).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('canDeleteByOwner', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockIsAdminOfUser.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it('returns true when userId matches ownerId', async () => {
|
||||
const getOwnerId = vi.fn().mockResolvedValue('user-1');
|
||||
expect(await canDeleteByOwner(getOwnerId, 'session-1', 'user-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when userId does not match and not admin', async () => {
|
||||
const getOwnerId = vi.fn().mockResolvedValue('owner-1');
|
||||
expect(await canDeleteByOwner(getOwnerId, 'session-1', 'other')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when user is admin of owner', async () => {
|
||||
mockIsAdminOfUser.mockResolvedValue(true);
|
||||
const getOwnerId = vi.fn().mockResolvedValue('owner-1');
|
||||
expect(await canDeleteByOwner(getOwnerId, 'session-1', 'admin-1')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when session not found (ownerId is null)', async () => {
|
||||
const getOwnerId = vi.fn().mockResolvedValue(null);
|
||||
expect(await canDeleteByOwner(getOwnerId, 'session-1', 'anyone')).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user