Replace all @Controller('acadenice/...') decorators with 'v1/...' on 16 NestJS controllers. Update all client services, hooks, tests, extension-clipper, and doc comments to match. DB table names (acadenice_*) and folder structure untouched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { sendClip, fetchSpaces, ClipPayload } from '../src/lib/api-client';
|
|
|
|
const BASE_URL = 'http://localhost:3000';
|
|
const TOKEN = 'clip_testtoken';
|
|
|
|
const samplePayload: ClipPayload = {
|
|
url: 'https://example.com/article',
|
|
title: 'My Article',
|
|
target_workspace_id: 'ws-uuid',
|
|
target_space_id: 'space-uuid',
|
|
};
|
|
|
|
function mockFetch(status: number, body: unknown): void {
|
|
global.fetch = vi.fn().mockResolvedValue({
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
statusText: 'OK',
|
|
json: async () => body,
|
|
} as Response);
|
|
}
|
|
|
|
describe('sendClip', () => {
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
it('returns ClipResult on 201 Created', async () => {
|
|
const expected = { pageId: 'p-1', slugId: 'slug-1', url: samplePayload.url };
|
|
mockFetch(201, expected);
|
|
|
|
const result = await sendClip(BASE_URL, TOKEN, samplePayload);
|
|
expect(result.pageId).toBe('p-1');
|
|
expect(result.slugId).toBe('slug-1');
|
|
});
|
|
|
|
it('sends X-Clipper-Token header', async () => {
|
|
mockFetch(201, { pageId: 'p-1', slugId: 's-1', url: '' });
|
|
await sendClip(BASE_URL, TOKEN, samplePayload);
|
|
const [_url, init] = (global.fetch as any).mock.calls[0];
|
|
expect((init as RequestInit).headers!['X-Clipper-Token']).toBe(TOKEN);
|
|
});
|
|
|
|
it('posts to the correct endpoint', async () => {
|
|
mockFetch(201, { pageId: 'p-1', slugId: 's-1', url: '' });
|
|
await sendClip(BASE_URL, TOKEN, samplePayload);
|
|
const [url] = (global.fetch as any).mock.calls[0];
|
|
expect(url).toBe(`${BASE_URL}/api/v1/clipper/import`);
|
|
});
|
|
|
|
it('strips trailing slash from apiUrl', async () => {
|
|
mockFetch(201, { pageId: 'p-1', slugId: 's-1', url: '' });
|
|
await sendClip(`${BASE_URL}/`, TOKEN, samplePayload);
|
|
const [url] = (global.fetch as any).mock.calls[0];
|
|
expect(url).toBe(`${BASE_URL}/api/v1/clipper/import`);
|
|
});
|
|
|
|
it('throws ApiError with statusCode on 401', async () => {
|
|
mockFetch(401, { message: 'Unauthorized' });
|
|
await expect(sendClip(BASE_URL, TOKEN, samplePayload)).rejects.toMatchObject({
|
|
statusCode: 401,
|
|
});
|
|
});
|
|
|
|
it('throws ApiError with statusCode on 400', async () => {
|
|
mockFetch(400, { message: 'Bad request' });
|
|
await expect(sendClip(BASE_URL, TOKEN, samplePayload)).rejects.toMatchObject({
|
|
statusCode: 400,
|
|
message: 'Bad request',
|
|
});
|
|
});
|
|
|
|
it('throws Error with network message when fetch rejects', async () => {
|
|
global.fetch = vi.fn().mockRejectedValue(new TypeError('Failed to fetch'));
|
|
await expect(sendClip(BASE_URL, TOKEN, samplePayload)).rejects.toThrow(
|
|
/Network error/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('fetchSpaces', () => {
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
it('returns empty array on network error', async () => {
|
|
global.fetch = vi.fn().mockRejectedValue(new Error('Network down'));
|
|
const result = await fetchSpaces(BASE_URL, TOKEN, 'ws-uuid');
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('returns empty array on non-ok response', async () => {
|
|
mockFetch(404, {});
|
|
const result = await fetchSpaces(BASE_URL, TOKEN, 'ws-uuid');
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('maps space items correctly', async () => {
|
|
const spaces = [
|
|
{ id: 's-1', name: 'General', slug: 'general' },
|
|
{ id: 's-2', name: 'Engineering', slug: 'engineering' },
|
|
];
|
|
mockFetch(200, spaces);
|
|
const result = await fetchSpaces(BASE_URL, TOKEN, 'ws-uuid');
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0].slug).toBe('general');
|
|
});
|
|
|
|
it('handles paginated response with items array', async () => {
|
|
mockFetch(200, { items: [{ id: 's-1', name: 'General', slug: 'general' }], total: 1 });
|
|
const result = await fetchSpaces(BASE_URL, TOKEN, 'ws-uuid');
|
|
expect(result[0].id).toBe('s-1');
|
|
});
|
|
});
|