test(tracker-web): cover untested API routes + tracker-client, enforce coverage

Add unit tests for the previously-untested proxy handlers (auth/mfa/verify,
auth/oauth/[provider], telemetry/ingest) covering success, error-status
forwarding, default error messages, validation, and the 502 unreachable path.
Add tracker-client tests asserting every endpoint path/method/body contract
and x-product-id header injection, plus product-config request resolution.

Overall coverage rises from ~90% to 94% stmts / 87% branch / 94% funcs. Also
fix the vitest coverage thresholds: the legacy global nesting was silently
ignored by the v8 provider, so the 80% gate was never enforced.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
saravanakumardb1 2026-05-28 20:05:33 -07:00
parent 1c231d6659
commit 772609c919
6 changed files with 445 additions and 6 deletions

View File

@ -0,0 +1,67 @@
/**
* Tests for POST /api/auth/mfa/verify (tracker dashboard proxy to platform-service).
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { POST } from '@/app/api/auth/mfa/verify/route';
const mockFetch = vi.fn();
vi.stubGlobal('fetch', mockFetch);
function jsonResponse(data: unknown, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
json: () => Promise.resolve(data),
} as unknown as Response;
}
function callVerify(body: object) {
return POST({ json: async () => body } as never);
}
describe('POST /api/auth/mfa/verify (tracker)', () => {
beforeEach(() => mockFetch.mockReset());
it('forwards a successful verification and returns tokens', async () => {
mockFetch.mockResolvedValue(jsonResponse({ accessToken: 'tok', user: { id: 'u1' } }));
const res = await callVerify({ challengeToken: 'chal', code: '123456', method: 'totp' });
expect(res.status).toBe(200);
const data = await res.json();
expect(data.accessToken).toBe('tok');
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/api/auth/mfa/verify'),
expect.objectContaining({ method: 'POST' })
);
});
it('forwards the platform-service error status and message', async () => {
mockFetch.mockResolvedValue(jsonResponse({ error: 'Invalid code' }, 401));
const res = await callVerify({ challengeToken: 'chal', code: '000000', method: 'totp' });
expect(res.status).toBe(401);
expect((await res.json()).error).toBe('Invalid code');
});
it('uses a default error message when platform-service omits one', async () => {
mockFetch.mockResolvedValue(jsonResponse({}, 403));
const res = await callVerify({ challengeToken: 'chal', code: '000000' });
expect(res.status).toBe(403);
expect((await res.json()).error).toBe('MFA verification failed');
});
it('returns 502 when the platform-service is unreachable', async () => {
mockFetch.mockImplementationOnce(() => Promise.reject(new Error('ECONNREFUSED')));
const res = await callVerify({ challengeToken: 'chal', code: '123456' });
expect(res.status).toBe(502);
expect((await res.json()).error).toBe('Platform service unavailable');
});
});

View File

@ -0,0 +1,93 @@
/**
* Tests for POST /api/auth/oauth/[provider] (tracker dashboard proxy to platform-service).
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { NextRequest } from 'next/server';
import { POST } from '@/app/api/auth/oauth/[provider]/route';
const mockFetch = vi.fn();
vi.stubGlobal('fetch', mockFetch);
function jsonResponse(data: unknown, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
json: () => Promise.resolve(data),
} as unknown as Response;
}
function mockRequest(body: object, headers: Record<string, string> = {}): NextRequest {
const headerMap = new Map(Object.entries(headers));
return {
json: async () => body,
headers: { get: (k: string) => headerMap.get(k.toLowerCase()) ?? null },
} as unknown as NextRequest;
}
function callOAuth(req: NextRequest, provider = 'google') {
return POST(req, { params: Promise.resolve({ provider }) });
}
describe('POST /api/auth/oauth/[provider] (tracker)', () => {
beforeEach(() => mockFetch.mockReset());
it('rejects requests without an idToken (400) and never calls the backend', async () => {
const res = await callOAuth(mockRequest({}));
expect(res.status).toBe(400);
expect((await res.json()).error).toBe('idToken required');
expect(mockFetch).not.toHaveBeenCalled();
});
it('proxies the provider login and returns tokens on success', async () => {
mockFetch.mockResolvedValue(jsonResponse({ accessToken: 'tok' }));
const res = await callOAuth(mockRequest({ idToken: 'google-id-token' }), 'google');
expect(res.status).toBe(200);
expect((await res.json()).accessToken).toBe('tok');
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/api/auth/oauth/google'),
expect.objectContaining({ method: 'POST' })
);
});
it('injects the x-product-id header value into the forwarded body', async () => {
mockFetch.mockResolvedValue(jsonResponse({ accessToken: 'tok' }));
await callOAuth(mockRequest({ idToken: 'tok' }, { 'x-product-id': 'chronomind' }));
const forwardedBody = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(forwardedBody.productId).toBe('chronomind');
expect(forwardedBody.idToken).toBe('tok');
});
it('forwards the platform-service error status', async () => {
mockFetch.mockResolvedValue(jsonResponse({ error: 'unverified' }, 401));
const res = await callOAuth(mockRequest({ idToken: 'tok' }));
expect(res.status).toBe(401);
expect((await res.json()).error).toBe('unverified');
});
it('uses a provider-specific default error message', async () => {
mockFetch.mockResolvedValue(jsonResponse({}, 403));
const res = await callOAuth(mockRequest({ idToken: 'tok' }), 'github');
expect(res.status).toBe(403);
expect((await res.json()).error).toBe('OAuth github login failed');
});
it('returns 502 when the platform-service is unreachable', async () => {
mockFetch.mockImplementationOnce(() => Promise.reject(new Error('ECONNREFUSED')));
const res = await callOAuth(mockRequest({ idToken: 'tok' }));
expect(res.status).toBe(502);
expect((await res.json()).error).toBe('Platform service unavailable');
});
});

View File

@ -0,0 +1,37 @@
/**
* Tests for src/lib/product-config request productId resolution + identity exports.
*/
import { describe, it, expect } from 'vitest';
import type { NextRequest } from 'next/server';
import { getRequestProductId, PRODUCT_ID, KNOWN_PRODUCTS } from '@/lib/product-config';
function reqWithHeader(value: string | null): NextRequest {
return {
headers: { get: (k: string) => (k.toLowerCase() === 'x-product-id' ? value : null) },
} as unknown as NextRequest;
}
describe('getRequestProductId', () => {
it('returns the x-product-id header when present', () => {
expect(getRequestProductId(reqWithHeader('chronomind'))).toBe('chronomind');
});
it('falls back to the configured PRODUCT_ID when the header is absent', () => {
expect(getRequestProductId(reqWithHeader(null))).toBe(PRODUCT_ID);
});
});
describe('product identity exports', () => {
it('exposes a non-empty PRODUCT_ID string', () => {
expect(typeof PRODUCT_ID).toBe('string');
expect(PRODUCT_ID.length).toBeGreaterThan(0);
});
it('lists known products with unique ids', () => {
const ids = KNOWN_PRODUCTS.map(p => p.id);
expect(ids.length).toBeGreaterThan(0);
expect(new Set(ids).size).toBe(ids.length);
});
});

View File

@ -0,0 +1,55 @@
/**
* Tests for POST /api/telemetry/ingest (tracker dashboard proxy to platform-service).
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { NextRequest } from 'next/server';
import { POST } from '@/app/api/telemetry/ingest/route';
const mockFetch = vi.fn();
vi.stubGlobal('fetch', mockFetch);
function mockRequest(body: string): NextRequest {
return { text: async () => body } as unknown as NextRequest;
}
describe('POST /api/telemetry/ingest (tracker)', () => {
beforeEach(() => mockFetch.mockReset());
it('forwards the raw beacon body to the telemetry events endpoint', async () => {
mockFetch.mockResolvedValue({ ok: true, status: 200 } as Response);
const payload = JSON.stringify({ events: [{ eventName: 'page_view' }] });
const res = await POST(mockRequest(payload));
expect(res.status).toBe(200);
expect((await res.json()).ok).toBe(true);
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/api/telemetry/events'),
expect.objectContaining({
method: 'POST',
body: payload,
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
})
);
});
it('mirrors a non-ok platform-service status', async () => {
mockFetch.mockResolvedValue({ ok: false, status: 429 } as Response);
const res = await POST(mockRequest('{}'));
expect(res.status).toBe(429);
expect((await res.json()).ok).toBe(false);
});
it('returns 502 when the platform-service is unreachable', async () => {
mockFetch.mockImplementationOnce(() => Promise.reject(new Error('ECONNREFUSED')));
const res = await POST(mockRequest('{}'));
expect(res.status).toBe(502);
expect((await res.json()).ok).toBe(false);
});
});

View File

@ -0,0 +1,187 @@
/**
* Tests for src/lib/tracker-client request path/header construction.
*
* The shared @bytelyst/api-client is mocked so we can assert exactly which
* path + options the tracker client forwards, without any real network.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
const { fetchSpy } = vi.hoisted(() => ({ fetchSpy: vi.fn() }));
vi.mock('@bytelyst/api-client', () => ({
createApiClient: () => ({ fetch: fetchSpy }),
}));
import {
listItems,
getItem,
createItem,
updateItem,
updateItemStatus,
deleteItem,
getStats,
listComments,
addComment,
toggleVote,
getRoadmapItems,
getRoadmapStats,
getPublicItem,
submitPublicItem,
publicVote,
} from '@/lib/tracker-client';
describe('tracker-client (authenticated API)', () => {
beforeEach(() => {
fetchSpy.mockReset();
fetchSpy.mockResolvedValue({});
});
it('builds a query string for listItems', async () => {
await listItems({ status: 'open', limit: '10' });
const [path] = fetchSpy.mock.calls[0];
expect(path).toBe('/items?status=open&limit=10');
});
it('omits the query string when no params are passed', async () => {
await listItems();
expect(fetchSpy.mock.calls[0][0]).toBe('/items');
});
it('requests a single item by id', async () => {
await getItem('item_42');
expect(fetchSpy.mock.calls[0][0]).toBe('/items/item_42');
});
it('POSTs a serialized body when creating an item', async () => {
await createItem({ title: 'Bug', type: 'bug' });
const [path, options] = fetchSpy.mock.calls[0];
expect(path).toBe('/items');
expect(options.method).toBe('POST');
expect(JSON.parse(options.body)).toEqual({ title: 'Bug', type: 'bug' });
});
it('PATCHes the status sub-resource', async () => {
await updateItemStatus('item_1', 'done');
const [path, options] = fetchSpy.mock.calls[0];
expect(path).toBe('/items/item_1/status');
expect(options.method).toBe('PATCH');
expect(JSON.parse(options.body)).toEqual({ status: 'done' });
});
it('DELETEs an item by id', async () => {
await deleteItem('item_9');
const [path, options] = fetchSpy.mock.calls[0];
expect(path).toBe('/items/item_9');
expect(options.method).toBe('DELETE');
});
it('PUTs an updated item', async () => {
await updateItem('item_1', { title: 'Renamed' });
const [path, options] = fetchSpy.mock.calls[0];
expect(path).toBe('/items/item_1');
expect(options.method).toBe('PUT');
expect(JSON.parse(options.body)).toEqual({ title: 'Renamed' });
});
it('requests stats with an optional productId query', async () => {
await getStats('chronomind');
expect(fetchSpy.mock.calls[0][0]).toBe('/items/stats?productId=chronomind');
});
it('requests stats without a query when no productId is given', async () => {
await getStats();
expect(fetchSpy.mock.calls[0][0]).toBe('/items/stats');
});
it('lists comments for an item', async () => {
await listComments('item_1');
expect(fetchSpy.mock.calls[0][0]).toBe('/items/item_1/comments');
});
it('POSTs a new comment body', async () => {
await addComment('item_1', 'Looks good');
const [path, options] = fetchSpy.mock.calls[0];
expect(path).toBe('/items/item_1/comments');
expect(options.method).toBe('POST');
expect(JSON.parse(options.body)).toEqual({ body: 'Looks good' });
});
it('POSTs a vote toggle', async () => {
await toggleVote('item_1');
const [path, options] = fetchSpy.mock.calls[0];
expect(path).toBe('/items/item_1/vote');
expect(options.method).toBe('POST');
});
});
describe('tracker-client (public roadmap API)', () => {
beforeEach(() => {
fetchSpy.mockReset();
fetchSpy.mockResolvedValue({});
});
it('builds the public roadmap path with query params', async () => {
await getRoadmapItems({ sortBy: 'voteCount', limit: '100' });
expect(fetchSpy.mock.calls[0][0]).toBe('/public/roadmap?sortBy=voteCount&limit=100');
});
it('POSTs to the public submit endpoint', async () => {
await submitPublicItem({ title: 'Idea', email: 'a@b.com', name: 'A' });
const [path, options] = fetchSpy.mock.calls[0];
expect(path).toBe('/public/submit');
expect(options.method).toBe('POST');
expect(JSON.parse(options.body).title).toBe('Idea');
});
it('requests public roadmap stats with optional productId', async () => {
await getRoadmapStats('lysnrai');
expect(fetchSpy.mock.calls[0][0]).toBe('/public/roadmap/stats?productId=lysnrai');
});
it('requests a single public item by id', async () => {
await getPublicItem('item_7');
expect(fetchSpy.mock.calls[0][0]).toBe('/public/items/item_7');
});
it('POSTs a public vote with the voter email', async () => {
await publicVote('item_7', 'voter@example.com');
const [path, options] = fetchSpy.mock.calls[0];
expect(path).toBe('/public/items/item_7/vote');
expect(options.method).toBe('POST');
expect(JSON.parse(options.body)).toEqual({ email: 'voter@example.com' });
});
});
describe('tracker-client product header injection', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
beforeEach(() => {
fetchSpy.mockReset();
fetchSpy.mockResolvedValue({});
});
it('adds x-product-id from localStorage when running in the browser', async () => {
vi.stubGlobal('window', {});
vi.stubGlobal('localStorage', {
getItem: (key: string) => (key === 'tracker_selected_product' ? 'chronomind' : null),
});
await listItems();
const options = fetchSpy.mock.calls[0][1];
expect(options.headers['x-product-id']).toBe('chronomind');
});
it('does not add x-product-id when no product is selected', async () => {
vi.stubGlobal('window', {});
vi.stubGlobal('localStorage', { getItem: () => null });
await listItems();
const options = fetchSpy.mock.calls[0][1];
expect(options.headers['x-product-id']).toBeUndefined();
});
});

View File

@ -18,13 +18,13 @@ export default defineConfig({
'**/*.config.*',
'**/e2e/**',
],
// Vitest reads these keys directly under `thresholds` (the legacy `global`
// nesting is ignored by the v8 provider and silently disables enforcement).
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},