feat(phase2): phase 6 - integration & polish
Fix all build/type/lint errors, write 60 new tests (175 total), update seed with new widget types and team board permissions, install missing svelte-i18n dependency, fix DynamicIcon for Svelte 5.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import { requireAdmin } from '$lib/server/middleware/authorize.js';
|
||||
import { testConnection, invalidateOAuthCache } from '$lib/server/services/oauthService.js';
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock the permission service
|
||||
vi.mock('$lib/server/services/permissionService.js', () => ({
|
||||
checkPermission: vi.fn(),
|
||||
getPermissionsForEntity: vi.fn(),
|
||||
grantPermission: vi.fn(),
|
||||
revokePermission: vi.fn()
|
||||
}));
|
||||
|
||||
import * as permissionService from '$lib/server/services/permissionService.js';
|
||||
import { GET, POST, DELETE } from '../+server.js';
|
||||
|
||||
const mockPermission = permissionService as unknown as Record<string, ReturnType<typeof vi.fn>>;
|
||||
|
||||
function createMockEvent(
|
||||
overrides: {
|
||||
user?: { id: string; role: string } | null;
|
||||
params?: Record<string, string>;
|
||||
body?: unknown;
|
||||
} = {}
|
||||
) {
|
||||
const { user = { id: 'u1', role: 'admin' }, params = { id: 'b1' }, body = {} } = overrides;
|
||||
|
||||
return {
|
||||
locals: { user },
|
||||
params,
|
||||
request: {
|
||||
json: vi.fn().mockResolvedValue(body)
|
||||
},
|
||||
url: new URL('http://localhost/api/boards/b1/permissions')
|
||||
} as unknown as Parameters<typeof GET>[0];
|
||||
}
|
||||
|
||||
describe('Board Permissions API', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('GET /api/boards/:id/permissions', () => {
|
||||
it('returns permissions for admin users', async () => {
|
||||
const permissions = [
|
||||
{ id: 'p1', entityType: 'board', entityId: 'b1', targetType: 'user', targetId: 'u2', level: 'view' }
|
||||
];
|
||||
mockPermission.getPermissionsForEntity.mockResolvedValue(permissions);
|
||||
|
||||
const response = await GET(createMockEvent());
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.data).toEqual(permissions);
|
||||
});
|
||||
|
||||
it('returns 401 for unauthenticated requests', async () => {
|
||||
const response = await GET(createMockEvent({ user: null }));
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
|
||||
it('checks edit permission for non-admin users', async () => {
|
||||
mockPermission.checkPermission.mockResolvedValue({ hasPermission: true, effectiveLevel: 'edit', source: 'user' });
|
||||
mockPermission.getPermissionsForEntity.mockResolvedValue([]);
|
||||
|
||||
const response = await GET(
|
||||
createMockEvent({ user: { id: 'u2', role: 'user' } })
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(mockPermission.checkPermission).toHaveBeenCalledWith('board', 'b1', 'u2', 'edit');
|
||||
});
|
||||
|
||||
it('returns 403 for non-admin users without edit permission', async () => {
|
||||
mockPermission.checkPermission.mockResolvedValue({ hasPermission: false });
|
||||
|
||||
const response = await GET(
|
||||
createMockEvent({ user: { id: 'u2', role: 'user' } })
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/boards/:id/permissions', () => {
|
||||
it('grants a permission for admin users', async () => {
|
||||
const permission = {
|
||||
id: 'p1',
|
||||
entityType: 'board',
|
||||
entityId: 'b1',
|
||||
targetType: 'user',
|
||||
targetId: 'u2',
|
||||
level: 'view'
|
||||
};
|
||||
mockPermission.grantPermission.mockResolvedValue(permission);
|
||||
|
||||
const response = await POST(
|
||||
createMockEvent({
|
||||
body: { targetType: 'user', targetId: 'u2', level: 'view' }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.data).toEqual(permission);
|
||||
});
|
||||
|
||||
it('validates targetType', async () => {
|
||||
const response = await POST(
|
||||
createMockEvent({
|
||||
body: { targetType: 'invalid', targetId: 'u2', level: 'view' }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toContain('targetType');
|
||||
});
|
||||
|
||||
it('validates targetId', async () => {
|
||||
const response = await POST(
|
||||
createMockEvent({
|
||||
body: { targetType: 'user', level: 'view' }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toContain('targetId');
|
||||
});
|
||||
|
||||
it('validates level', async () => {
|
||||
const response = await POST(
|
||||
createMockEvent({
|
||||
body: { targetType: 'user', targetId: 'u2', level: 'superadmin' }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toContain('level');
|
||||
});
|
||||
|
||||
it('returns 401 for unauthenticated requests', async () => {
|
||||
const response = await POST(createMockEvent({ user: null }));
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/boards/:id/permissions', () => {
|
||||
it('revokes a permission for admin users', async () => {
|
||||
mockPermission.revokePermission.mockResolvedValue(undefined);
|
||||
|
||||
const response = await DELETE(
|
||||
createMockEvent({
|
||||
body: { targetType: 'user', targetId: 'u2' }
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(mockPermission.revokePermission).toHaveBeenCalledWith('board', 'b1', 'user', 'u2');
|
||||
});
|
||||
|
||||
it('validates targetType', async () => {
|
||||
const response = await DELETE(
|
||||
createMockEvent({
|
||||
body: { targetType: 'invalid', targetId: 'u2' }
|
||||
})
|
||||
);
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('validates targetId', async () => {
|
||||
const response = await DELETE(
|
||||
createMockEvent({
|
||||
body: { targetType: 'user' }
|
||||
})
|
||||
);
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('returns 401 for unauthenticated requests', async () => {
|
||||
const response = await DELETE(createMockEvent({ user: null }));
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import DraggableBoard from '$lib/components/board/DraggableBoard.svelte';
|
||||
import BoardAccessControl from '$lib/components/board/BoardAccessControl.svelte';
|
||||
import { WidgetType } from '$lib/utils/constants.js';
|
||||
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user