test(platform-service): add tokens route-level tests
This commit is contained in:
parent
dc0cf6d8b4
commit
3e1647ae96
176
services/platform-service/src/modules/tokens/routes.test.ts
Normal file
176
services/platform-service/src/modules/tokens/routes.test.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* Route-level tests for tokens module — Fastify inject.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Fastify from 'fastify';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const repoMock = {
|
||||||
|
list: vi.fn(),
|
||||||
|
listByUser: vi.fn(),
|
||||||
|
hashToken: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
countActive: vi.fn(),
|
||||||
|
revoke: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('./repository.js', () => repoMock);
|
||||||
|
|
||||||
|
const baseToken = {
|
||||||
|
id: 'tok_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
userId: 'admin_1',
|
||||||
|
userName: 'admin@example.com',
|
||||||
|
name: 'ci-token',
|
||||||
|
prefix: 'wai_12345678',
|
||||||
|
status: 'active',
|
||||||
|
scopes: ['read'],
|
||||||
|
createdAt: '2026-02-16T00:00:00Z',
|
||||||
|
expiresAt: '2026-05-16T00:00:00Z',
|
||||||
|
lastUsed: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function buildApp(payload?: { sub: string; productId: string; role?: string; email?: string }) {
|
||||||
|
const { tokenRoutes } = await import('./routes.js');
|
||||||
|
const app = Fastify({ logger: false });
|
||||||
|
if (payload) {
|
||||||
|
app.addHook('onRequest', async req => {
|
||||||
|
(req as typeof req & { jwtPayload?: typeof payload }).jwtPayload = payload;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await app.register(tokenRoutes, { prefix: '/api' });
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('tokenRoutes', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /tokens returns 401 when unauthenticated', async () => {
|
||||||
|
const app = await buildApp();
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/tokens' });
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /tokens lists all tokens for admin', async () => {
|
||||||
|
repoMock.list.mockResolvedValue([baseToken]);
|
||||||
|
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/tokens' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.tokens).toHaveLength(1);
|
||||||
|
expect(repoMock.list).toHaveBeenCalledWith('lysnrai');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /tokens lists own tokens for non-admin user', async () => {
|
||||||
|
repoMock.listByUser.mockResolvedValue([baseToken]);
|
||||||
|
const app = await buildApp({ sub: 'user_1', productId: 'lysnrai', role: 'user' });
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/tokens' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(repoMock.listByUser).toHaveBeenCalledWith('user_1', 'lysnrai');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /tokens returns 403 for non-admin', async () => {
|
||||||
|
const app = await buildApp({ sub: 'user_1', productId: 'lysnrai', role: 'user' });
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/tokens',
|
||||||
|
payload: { name: 'token', scopes: ['read'], expiresInDays: 30 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /tokens creates token for admin', async () => {
|
||||||
|
repoMock.hashToken.mockResolvedValue('hashed_token');
|
||||||
|
repoMock.create.mockResolvedValue(baseToken);
|
||||||
|
const app = await buildApp({
|
||||||
|
sub: 'admin_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
role: 'admin',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/tokens',
|
||||||
|
payload: { name: 'ci-token', scopes: ['read'], expiresInDays: 30 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(201);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data).toHaveProperty('rawToken');
|
||||||
|
expect(data.rawToken.startsWith('wai_')).toBe(true);
|
||||||
|
expect(repoMock.hashToken).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /tokens/count returns count for admin', async () => {
|
||||||
|
repoMock.countActive.mockResolvedValue(4);
|
||||||
|
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'GET', url: '/api/tokens/count' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.count).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PATCH /tokens/:id revokes token', async () => {
|
||||||
|
repoMock.revoke.mockResolvedValue(true);
|
||||||
|
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'PATCH',
|
||||||
|
url: '/api/tokens/tok_1',
|
||||||
|
payload: { action: 'revoke' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PATCH /tokens/:id returns 400 when token not found', async () => {
|
||||||
|
repoMock.revoke.mockResolvedValue(false);
|
||||||
|
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'PATCH',
|
||||||
|
url: '/api/tokens/tok_missing',
|
||||||
|
payload: { action: 'revoke' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DELETE /tokens/:id returns 403 for non-super-admin', async () => {
|
||||||
|
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'DELETE', url: '/api/tokens/tok_1' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DELETE /tokens/:id deletes token for super_admin', async () => {
|
||||||
|
repoMock.remove.mockResolvedValue(true);
|
||||||
|
const app = await buildApp({ sub: 'root_1', productId: 'lysnrai', role: 'super_admin' });
|
||||||
|
|
||||||
|
const res = await app.inject({ method: 'DELETE', url: '/api/tokens/tok_1' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const data = JSON.parse(res.body);
|
||||||
|
expect(data.success).toBe(true);
|
||||||
|
expect(repoMock.remove).toHaveBeenCalledWith('tok_1', 'root_1');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user