Files
heygo/tests/api.promotions-ui.test.ts

152 lines
5.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
ApiClientError,
approveSubmission,
listAdminSubmissions,
listMySubmissions,
needsChangesSubmission,
promoteLink,
rejectSubmission,
} from '../src/lib/api';
const originalFetch = globalThis.fetch;
function mockFetch(responder: (input: RequestInfo | URL, init?: RequestInit) => {
status?: number;
body?: unknown;
statusText?: string;
}) {
const calls: { path: string; init?: RequestInit }[] = [];
const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const path = typeof input === 'string' ? input : new URL(input.toString()).pathname;
calls.push({ path, init });
const { status = 200, body = {}, statusText = 'OK' } = responder(input, init);
return new Response(JSON.stringify(body), {
status,
statusText,
headers: { 'content-type': 'application/json' },
});
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
return { calls, fetchMock };
}
afterEach(() => {
globalThis.fetch = originalFetch;
});
describe('promotion api client', () => {
it('promoteLink POSTs privateLinkId, proposedAlias, note to /api/promotions', async () => {
const { calls } = mockFetch((_input, init) => ({
status: 201,
body: { submission: { id: 'sub_1', status: 'pending', proposedAlias: 'docs' } },
}));
const result = await promoteLink({
privateLinkId: 'priv_1',
proposedAlias: 'docs',
note: 'please review',
});
expect(calls[0].path).toBe('/api/promotions');
expect(calls[0].init?.method).toBe('POST');
expect(calls[0].init?.credentials).toBe('include');
const body = JSON.parse(calls[0].init?.body as string);
expect(body).toEqual({ privateLinkId: 'priv_1', proposedAlias: 'docs', note: 'please review' });
expect(result.submission.id).toBe('sub_1');
});
it('promoteLink omits the note field when it is empty or undefined', async () => {
const { calls } = mockFetch(() => ({
status: 201,
body: { submission: { id: 'sub_2', status: 'pending' } },
}));
await promoteLink({ privateLinkId: 'priv_1', proposedAlias: 'docs', note: '' });
const body = JSON.parse(calls[0].init?.body as string);
expect(body).not.toHaveProperty('note');
});
it('listMySubmissions GETs /api/promotions/mine with credentials', async () => {
const { calls } = mockFetch(() => ({
body: { submissions: [{ id: 'sub_1', status: 'pending', proposedAlias: 'docs' }] },
}));
const result = await listMySubmissions();
expect(calls[0].path).toBe('/api/promotions/mine');
expect(calls[0].init?.method).toBeUndefined();
expect(calls[0].init?.credentials).toBe('include');
expect(result.submissions).toHaveLength(1);
});
it('listAdminSubmissions encodes the status filter into the query string', async () => {
const { calls } = mockFetch(() => ({ body: { submissions: [] } }));
await listAdminSubmissions('needs_changes');
expect(calls[0].path).toBe('/api/admin/promotions?status=needs_changes');
});
it('approveSubmission POSTs to /api/admin/promotions/:id/approve and returns publicLink', async () => {
const { calls } = mockFetch(() => ({
body: {
submission: { id: 'sub_1', status: 'approved' },
publicLink: { id: 'pub_1', alias: 'docs', scope: 'public' },
},
}));
const result = await approveSubmission('sub_1', { reason: 'lgtm' });
expect(calls[0].path).toBe('/api/admin/promotions/sub_1/approve');
expect(calls[0].init?.method).toBe('POST');
const body = JSON.parse(calls[0].init?.body as string);
expect(body).toEqual({ reason: 'lgtm' });
expect(result.publicLink.id).toBe('pub_1');
});
it('rejectSubmission posts an empty object when no reason is given', async () => {
const { calls } = mockFetch(() => ({
body: { submission: { id: 'sub_1', status: 'rejected', rejectionReason: null } },
}));
await rejectSubmission('sub_1');
expect(calls[0].path).toBe('/api/admin/promotions/sub_1/reject');
const body = JSON.parse(calls[0].init?.body as string);
expect(body).toEqual({});
});
it('needsChangesSubmission targets the needs-changes endpoint', async () => {
const { calls } = mockFetch(() => ({
body: { submission: { id: 'sub_1', status: 'needs_changes' } },
}));
await needsChangesSubmission('sub_1', { reason: 'tighten description' });
expect(calls[0].path).toBe('/api/admin/promotions/sub_1/needs-changes');
const body = JSON.parse(calls[0].init?.body as string);
expect(body).toEqual({ reason: 'tighten description' });
});
it('encodes submission ids with special characters in review paths', async () => {
const { calls } = mockFetch(() => ({ body: { submission: { id: 'a/b', status: 'approved' } } }));
await approveSubmission('a/b');
expect(calls[0].path).toBe('/api/admin/promotions/a%2Fb/approve');
});
it('surfaces server errors as ApiClientError', async () => {
mockFetch(() => ({ status: 409, body: { error: 'A pending submission for this link and alias already exists' } }));
await expect(promoteLink({ privateLinkId: 'priv_1', proposedAlias: 'docs' }))
.rejects.toMatchObject({
name: 'ApiClientError',
status: 409,
message: 'A pending submission for this link and alias already exists',
});
});
});