test(platform-service): add route coverage for items invitations audit
This commit is contained in:
parent
7179f1e7c4
commit
9fbaf60ba8
101
services/platform-service/src/modules/audit/routes.test.ts
Normal file
101
services/platform-service/src/modules/audit/routes.test.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Route-level tests for audit module — Fastify inject.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Fastify from 'fastify';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const repoMock = {
|
||||||
|
create: vi.fn(),
|
||||||
|
query: vi.fn(),
|
||||||
|
getStats: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('./repository.js', () => repoMock);
|
||||||
|
vi.mock('../../lib/request-context.js', () => ({
|
||||||
|
getRequestProductId: () => 'lysnrai',
|
||||||
|
}));
|
||||||
|
|
||||||
|
async function buildApp() {
|
||||||
|
const { auditRoutes } = await import('./routes.js');
|
||||||
|
const app = Fastify({ logger: false });
|
||||||
|
await app.register(auditRoutes, { prefix: '/api' });
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('auditRoutes', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /audit accepts valid payload (fire-and-forget)', async () => {
|
||||||
|
repoMock.create.mockResolvedValue(undefined);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/audit',
|
||||||
|
payload: {
|
||||||
|
userId: 'user_1',
|
||||||
|
action: 'item.created',
|
||||||
|
category: 'tracker',
|
||||||
|
details: { itemId: 'trk_1' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(202);
|
||||||
|
expect(JSON.parse(res.body).accepted).toBe(true);
|
||||||
|
expect(repoMock.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /audit returns 400 on invalid payload', async () => {
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/audit',
|
||||||
|
payload: { action: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /audit returns records', async () => {
|
||||||
|
repoMock.query.mockResolvedValue([{ id: 'aud_1', action: 'item.created' }]);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/audit?userId=user_1&days=7&limit=10&offset=0',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.records).toHaveLength(1);
|
||||||
|
expect(data.count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /audit returns 400 for invalid query', async () => {
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/audit?days=0' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /audit/stats returns stats for given days', async () => {
|
||||||
|
repoMock.getStats.mockResolvedValue({ item_created: 3, login: 5 });
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/audit/stats?days=14' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.days).toBe(14);
|
||||||
|
expect(data.stats.item_created).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
179
services/platform-service/src/modules/invitations/routes.test.ts
Normal file
179
services/platform-service/src/modules/invitations/routes.test.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* Route-level tests for invitations module — Fastify inject.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Fastify from 'fastify';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const repoMock = {
|
||||||
|
list: vi.fn(),
|
||||||
|
count: vi.fn(),
|
||||||
|
getById: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
redeem: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const webhookMock = {
|
||||||
|
dispatchInvitationRedeemed: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('./repository.js', () => repoMock);
|
||||||
|
vi.mock('../../lib/webhooks.js', () => webhookMock);
|
||||||
|
vi.mock('../../lib/request-context.js', () => ({
|
||||||
|
getRequestProductId: () => 'lysnrai',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const invitation = {
|
||||||
|
id: 'inv_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
code: 'WELCOME-1',
|
||||||
|
description: 'Welcome offer',
|
||||||
|
createdBy: 'admin',
|
||||||
|
grantPlan: 'pro',
|
||||||
|
grantTrialDays: 14,
|
||||||
|
bonusTokens: 100,
|
||||||
|
maxUses: 5,
|
||||||
|
currentUses: 1,
|
||||||
|
redeemedBy: ['user_1'],
|
||||||
|
status: 'active',
|
||||||
|
expiresAt: null,
|
||||||
|
createdAt: '2026-02-16T00:00:00Z',
|
||||||
|
updatedAt: '2026-02-16T00:00:00Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function buildApp() {
|
||||||
|
const { invitationRoutes } = await import('./routes.js');
|
||||||
|
const app = Fastify({ logger: false });
|
||||||
|
await app.register(invitationRoutes, { prefix: '/api' });
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('invitationRoutes', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /invitations lists codes', async () => {
|
||||||
|
repoMock.list.mockResolvedValue([invitation]);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/invitations?limit=10&offset=0' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.invitations).toHaveLength(1);
|
||||||
|
expect(data.count).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /invitations/count returns total', async () => {
|
||||||
|
repoMock.count.mockResolvedValue(3);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/invitations/count' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(JSON.parse(res.body).count).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /invitations/:id returns 404 when not found', async () => {
|
||||||
|
repoMock.getById.mockResolvedValue(null);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/invitations/missing' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /invitations creates code', async () => {
|
||||||
|
repoMock.create.mockResolvedValue(invitation);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/invitations',
|
||||||
|
payload: {
|
||||||
|
code: 'welcome-1',
|
||||||
|
createdBy: 'admin',
|
||||||
|
grantPlan: 'pro',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(201);
|
||||||
|
expect(repoMock.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /invitations/:id updates code', async () => {
|
||||||
|
repoMock.update.mockResolvedValue({ ...invitation, status: 'disabled' });
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/api/invitations/inv_1',
|
||||||
|
payload: { status: 'disabled' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(JSON.parse(res.body).status).toBe('disabled');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DELETE /invitations/:id returns 204', async () => {
|
||||||
|
repoMock.remove.mockResolvedValue(true);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'DELETE', url: '/api/invitations/inv_1' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /invitations/bulk returns 207 with mixed success', async () => {
|
||||||
|
repoMock.create.mockResolvedValueOnce(invitation).mockRejectedValueOnce(new Error('duplicate'));
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/invitations/bulk',
|
||||||
|
payload: [
|
||||||
|
{ code: 'first', createdBy: 'admin', grantPlan: 'pro' },
|
||||||
|
{ code: 'second', createdBy: 'admin', grantPlan: 'pro' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(207);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.created).toBe(1);
|
||||||
|
expect(data.failed).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /invitations/redeem redeems and dispatches webhook', async () => {
|
||||||
|
repoMock.redeem.mockResolvedValue(invitation);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/invitations/redeem',
|
||||||
|
payload: { code: 'WELCOME-1', userId: 'user_2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(webhookMock.dispatchInvitationRedeemed).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /invitations/redeem returns 400 when invalid', async () => {
|
||||||
|
repoMock.redeem.mockResolvedValue(null);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/invitations/redeem',
|
||||||
|
payload: { code: 'BAD', userId: 'user_2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
163
services/platform-service/src/modules/items/routes.test.ts
Normal file
163
services/platform-service/src/modules/items/routes.test.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* Route-level tests for tracker items module — Fastify inject.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Fastify from 'fastify';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const repoMock = {
|
||||||
|
list: vi.fn(),
|
||||||
|
getById: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const authMock = {
|
||||||
|
extractAuth: vi.fn(async () => ({ sub: 'user_1', role: 'admin' })),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('./repository.js', () => repoMock);
|
||||||
|
vi.mock('../../lib/auth.js', () => authMock);
|
||||||
|
vi.mock('../../lib/request-context.js', () => ({
|
||||||
|
getRequestProductId: () => 'lysnrai',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const baseItem = {
|
||||||
|
id: 'trk_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
type: 'feature',
|
||||||
|
status: 'open',
|
||||||
|
priority: 'high',
|
||||||
|
title: 'Add better dictation UX',
|
||||||
|
description: 'Improve flow',
|
||||||
|
labels: ['ux'],
|
||||||
|
assignee: null,
|
||||||
|
reportedBy: 'user_1',
|
||||||
|
source: 'internal',
|
||||||
|
visibility: 'internal',
|
||||||
|
voteCount: 1,
|
||||||
|
commentCount: 0,
|
||||||
|
priorityOrder: 1,
|
||||||
|
targetRelease: null,
|
||||||
|
createdAt: '2026-02-16T00:00:00Z',
|
||||||
|
updatedAt: '2026-02-16T00:00:00Z',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function buildApp() {
|
||||||
|
const { itemRoutes } = await import('./routes.js');
|
||||||
|
const app = Fastify({ logger: false });
|
||||||
|
await app.register(itemRoutes, { prefix: '/api' });
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('itemRoutes', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /items/stats returns aggregates', async () => {
|
||||||
|
repoMock.list.mockResolvedValue({ items: [baseItem], total: 1 });
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/items/stats' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.total).toBe(1);
|
||||||
|
expect(data.byType.feature).toBe(1);
|
||||||
|
expect(data.requestedBy).toBe('user_1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /items returns 400 for invalid query', async () => {
|
||||||
|
const app = await buildApp();
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/items?limit=0' });
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /items returns paginated list', async () => {
|
||||||
|
repoMock.list.mockResolvedValue({ items: [baseItem], total: 1 });
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/items?limit=10&offset=0' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.items).toHaveLength(1);
|
||||||
|
expect(data.total).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /items/:id returns 404 when missing', async () => {
|
||||||
|
repoMock.getById.mockResolvedValue(null);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/items/missing' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /items creates item', async () => {
|
||||||
|
repoMock.create.mockResolvedValue(baseItem);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/items',
|
||||||
|
payload: {
|
||||||
|
type: 'feature',
|
||||||
|
priority: 'high',
|
||||||
|
title: 'Add better dictation UX',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(201);
|
||||||
|
expect(repoMock.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /items/:id updates item and priorityOrder', async () => {
|
||||||
|
repoMock.getById.mockResolvedValue(baseItem);
|
||||||
|
repoMock.update.mockResolvedValue({ ...baseItem, priority: 'critical', priorityOrder: 0 });
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'PUT',
|
||||||
|
url: '/api/items/trk_1',
|
||||||
|
payload: { priority: 'critical', title: 'Urgent dictation fix' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.priority).toBe('critical');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PATCH /items/:id/status updates status', async () => {
|
||||||
|
repoMock.getById.mockResolvedValue(baseItem);
|
||||||
|
repoMock.update.mockResolvedValue({ ...baseItem, status: 'done' });
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'PATCH',
|
||||||
|
url: '/api/items/trk_1/status',
|
||||||
|
payload: { status: 'done' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.status).toBe('done');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DELETE /items/:id deletes existing item', async () => {
|
||||||
|
repoMock.getById.mockResolvedValue(baseItem);
|
||||||
|
repoMock.remove.mockResolvedValue(undefined);
|
||||||
|
const app = await buildApp();
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'DELETE', url: '/api/items/trk_1' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(JSON.parse(res.body).success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user