- 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>
117 lines
3.4 KiB
TypeScript
117 lines
3.4 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { AuditCollector } from '../src/audit/collector.js';
|
|
import type { McpdClient } from '../src/http/mcpd-client.js';
|
|
|
|
function mockClient(): McpdClient {
|
|
return {
|
|
post: vi.fn(async () => ({})),
|
|
get: vi.fn(async () => ({})),
|
|
put: vi.fn(async () => ({})),
|
|
delete: vi.fn(async () => {}),
|
|
forward: vi.fn(async () => ({ status: 200, body: {} })),
|
|
withHeaders: vi.fn(() => mockClient()),
|
|
} as unknown as McpdClient;
|
|
}
|
|
|
|
describe('AuditCollector userName', () => {
|
|
let collector: AuditCollector;
|
|
let client: McpdClient;
|
|
|
|
afterEach(async () => {
|
|
if (collector) await collector.dispose();
|
|
});
|
|
|
|
it('auto-fills userName from session map', async () => {
|
|
client = mockClient();
|
|
collector = new AuditCollector(client, 'test-project');
|
|
collector.setSessionUserName('sess-1', 'michal');
|
|
|
|
collector.emit({
|
|
timestamp: new Date().toISOString(),
|
|
sessionId: 'sess-1',
|
|
eventKind: 'session_bind',
|
|
source: 'mcplocal',
|
|
verified: true,
|
|
payload: {},
|
|
});
|
|
|
|
await collector.flush();
|
|
|
|
const posted = vi.mocked(client.post).mock.calls[0]![1] as Array<Record<string, unknown>>;
|
|
expect(posted).toHaveLength(1);
|
|
expect(posted[0]!['userName']).toBe('michal');
|
|
expect(posted[0]!['projectName']).toBe('test-project');
|
|
});
|
|
|
|
it('does not overwrite explicitly set userName', async () => {
|
|
client = mockClient();
|
|
collector = new AuditCollector(client, 'test-project');
|
|
collector.setSessionUserName('sess-1', 'michal');
|
|
|
|
collector.emit({
|
|
timestamp: new Date().toISOString(),
|
|
sessionId: 'sess-1',
|
|
eventKind: 'tool_call_trace',
|
|
source: 'mcplocal',
|
|
verified: true,
|
|
userName: 'admin',
|
|
payload: {},
|
|
});
|
|
|
|
await collector.flush();
|
|
|
|
const posted = vi.mocked(client.post).mock.calls[0]![1] as Array<Record<string, unknown>>;
|
|
expect(posted[0]!['userName']).toBe('admin');
|
|
});
|
|
|
|
it('leaves userName undefined when no session mapping exists', async () => {
|
|
client = mockClient();
|
|
collector = new AuditCollector(client, 'test-project');
|
|
|
|
collector.emit({
|
|
timestamp: new Date().toISOString(),
|
|
sessionId: 'sess-unknown',
|
|
eventKind: 'gate_decision',
|
|
source: 'client',
|
|
verified: false,
|
|
payload: {},
|
|
});
|
|
|
|
await collector.flush();
|
|
|
|
const posted = vi.mocked(client.post).mock.calls[0]![1] as Array<Record<string, unknown>>;
|
|
expect(posted[0]!['userName']).toBeUndefined();
|
|
});
|
|
|
|
it('fills userName for multiple sessions independently', async () => {
|
|
client = mockClient();
|
|
collector = new AuditCollector(client, 'test-project');
|
|
collector.setSessionUserName('sess-a', 'alice');
|
|
collector.setSessionUserName('sess-b', 'bob');
|
|
|
|
collector.emit({
|
|
timestamp: new Date().toISOString(),
|
|
sessionId: 'sess-a',
|
|
eventKind: 'session_bind',
|
|
source: 'mcplocal',
|
|
verified: true,
|
|
payload: {},
|
|
});
|
|
collector.emit({
|
|
timestamp: new Date().toISOString(),
|
|
sessionId: 'sess-b',
|
|
eventKind: 'session_bind',
|
|
source: 'mcplocal',
|
|
verified: true,
|
|
payload: {},
|
|
});
|
|
|
|
await collector.flush();
|
|
|
|
const posted = vi.mocked(client.post).mock.calls[0]![1] as Array<Record<string, unknown>>;
|
|
expect(posted).toHaveLength(2);
|
|
expect(posted[0]!['userName']).toBe('alice');
|
|
expect(posted[1]!['userName']).toBe('bob');
|
|
});
|
|
});
|