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:
2026-03-24 23:43:31 +03:00
parent 5bb4fbcedf
commit 87ed928a3a
17 changed files with 2057 additions and 59 deletions
+1 -1
View File
@@ -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();