- Add userName column to AuditEvent schema with index and migration - Add GET /api/v1/auth/me endpoint returning current user identity - AuditCollector auto-fills userName from session→user map, resolved lazily via /auth/me on first session creation - Support userName and date range (from/to) filtering on audit events and sessions endpoints - Audit console sidebar groups sessions by project → user - Add date filter presets (d key: all/today/1h/24h/7d) to console - Add scrolling and page up/down to sidebar navigation - Tests: auth-me (4), audit-username collector (4), route filters (2), smoke tests (2) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
153 lines
4.4 KiB
TypeScript
153 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import Fastify from 'fastify';
|
|
import type { FastifyInstance } from 'fastify';
|
|
import { registerAuthRoutes } from '../src/routes/auth.js';
|
|
import { errorHandler } from '../src/middleware/error-handler.js';
|
|
import type { SafeUser } from '../src/repositories/user.repository.js';
|
|
|
|
let app: FastifyInstance;
|
|
|
|
afterEach(async () => {
|
|
if (app) await app.close();
|
|
});
|
|
|
|
function makeSafeUser(overrides?: Partial<SafeUser>): SafeUser {
|
|
return {
|
|
id: 'user-1',
|
|
email: 'michal@example.com',
|
|
name: 'Michal',
|
|
role: 'user',
|
|
provider: 'local',
|
|
externalId: null,
|
|
version: 1,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createMockDeps() {
|
|
return {
|
|
authService: {
|
|
login: vi.fn(async () => ({})),
|
|
logout: vi.fn(async () => {}),
|
|
findSession: vi.fn(async () => null),
|
|
impersonate: vi.fn(async () => ({})),
|
|
},
|
|
userService: {
|
|
count: vi.fn(async () => 1),
|
|
create: vi.fn(async () => makeSafeUser()),
|
|
list: vi.fn(async () => []),
|
|
getById: vi.fn(async () => makeSafeUser()),
|
|
getByEmail: vi.fn(async () => makeSafeUser()),
|
|
delete: vi.fn(async () => {}),
|
|
},
|
|
groupService: {
|
|
create: vi.fn(async () => ({})),
|
|
list: vi.fn(async () => []),
|
|
getById: vi.fn(async () => null),
|
|
getByName: vi.fn(async () => null),
|
|
update: vi.fn(async () => null),
|
|
delete: vi.fn(async () => {}),
|
|
},
|
|
rbacDefinitionService: {
|
|
create: vi.fn(async () => ({})),
|
|
list: vi.fn(async () => []),
|
|
getById: vi.fn(async () => null),
|
|
getByName: vi.fn(async () => null),
|
|
update: vi.fn(async () => null),
|
|
delete: vi.fn(async () => {}),
|
|
},
|
|
rbacService: {
|
|
canAccess: vi.fn(async () => false),
|
|
canRunOperation: vi.fn(async () => false),
|
|
getPermissions: vi.fn(async () => []),
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('GET /api/v1/auth/me', () => {
|
|
it('returns user identity for authenticated request', async () => {
|
|
const deps = createMockDeps();
|
|
deps.authService.findSession.mockResolvedValue({
|
|
userId: 'user-1',
|
|
expiresAt: new Date(Date.now() + 86400_000),
|
|
});
|
|
deps.userService.getById.mockResolvedValue(makeSafeUser({ id: 'user-1', name: 'Michal', email: 'michal@example.com' }));
|
|
|
|
app = Fastify({ logger: false });
|
|
app.setErrorHandler(errorHandler);
|
|
registerAuthRoutes(app, deps as never);
|
|
await app.ready();
|
|
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: '/api/v1/auth/me',
|
|
headers: { authorization: 'Bearer valid-token' },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
const body = res.json<{ id: string; email: string; name: string | null }>();
|
|
expect(body.id).toBe('user-1');
|
|
expect(body.email).toBe('michal@example.com');
|
|
expect(body.name).toBe('Michal');
|
|
});
|
|
|
|
it('returns null name when user has no name', async () => {
|
|
const deps = createMockDeps();
|
|
deps.authService.findSession.mockResolvedValue({
|
|
userId: 'user-1',
|
|
expiresAt: new Date(Date.now() + 86400_000),
|
|
});
|
|
deps.userService.getById.mockResolvedValue(makeSafeUser({ name: null }));
|
|
|
|
app = Fastify({ logger: false });
|
|
app.setErrorHandler(errorHandler);
|
|
registerAuthRoutes(app, deps as never);
|
|
await app.ready();
|
|
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: '/api/v1/auth/me',
|
|
headers: { authorization: 'Bearer valid-token' },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.json<{ name: string | null }>().name).toBeNull();
|
|
});
|
|
|
|
it('returns 401 for unauthenticated request', async () => {
|
|
const deps = createMockDeps();
|
|
|
|
app = Fastify({ logger: false });
|
|
app.setErrorHandler(errorHandler);
|
|
registerAuthRoutes(app, deps as never);
|
|
await app.ready();
|
|
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: '/api/v1/auth/me',
|
|
});
|
|
|
|
expect(res.statusCode).toBe(401);
|
|
});
|
|
|
|
it('returns 401 for invalid token', async () => {
|
|
const deps = createMockDeps();
|
|
deps.authService.findSession.mockResolvedValue(null);
|
|
|
|
app = Fastify({ logger: false });
|
|
app.setErrorHandler(errorHandler);
|
|
registerAuthRoutes(app, deps as never);
|
|
await app.ready();
|
|
|
|
const res = await app.inject({
|
|
method: 'GET',
|
|
url: '/api/v1/auth/me',
|
|
headers: { authorization: 'Bearer bad-token' },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(401);
|
|
});
|
|
});
|