3c6791658c
- Created server/utils/logCapture.js to intercept and buffer server output, stripping ANSI escape codes. - Created server/middleware/logStreamAuth.js enforcing subnet IP filtering (LOG_ALLOW_SUBNETS), Emby session cookie, Basic Auth fallback, and X-Webhook-Secret header bypass. - Created server/routes/debug.js with SSE streams /api/debug/server-logs, /api/debug/client-logs and batched POST /api/debug/client-logs. Exposes public configuration status at /api/debug/status. - Integrated log capture and mounted debug routes in server/app.js and server/index.js. - Implemented client/src/utils/clientLogCapture.js in the frontend SPA to hook console log/warn/error and flush batched console events. - Documented all endpoints in OpenAPI server/openapi.yaml, ARCHITECTURE.md, and README.md. - Wrote route integration tests and frontend console capture tests, with full validation in swagger-coverage.
111 lines
3.4 KiB
JavaScript
111 lines
3.4 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
/**
|
|
* @vitest-environment jsdom
|
|
* Tests for client/src/utils/clientLogCapture.js
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { initClientLogCapture } from '../../../client/src/utils/clientLogCapture.js';
|
|
|
|
describe('clientLogCapture', () => {
|
|
let fetchMock;
|
|
let originalConsoleLog;
|
|
let originalConsoleWarn;
|
|
let originalConsoleError;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
|
|
// Preserve original console methods
|
|
originalConsoleLog = console.log;
|
|
originalConsoleWarn = console.warn;
|
|
originalConsoleError = console.error;
|
|
|
|
// Reset console methods to standard ones
|
|
console.log = vi.fn();
|
|
console.warn = vi.fn();
|
|
console.error = vi.fn();
|
|
|
|
// Mock window fetch
|
|
fetchMock = vi.fn();
|
|
global.window.fetch = fetchMock;
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
|
|
// Restore original console methods
|
|
console.log = originalConsoleLog;
|
|
console.warn = originalConsoleWarn;
|
|
console.error = originalConsoleError;
|
|
});
|
|
|
|
it('exits early and does not intercept console if status returns disabled', async () => {
|
|
fetchMock.mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ enabled: false })
|
|
});
|
|
|
|
await initClientLogCapture();
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/debug/status');
|
|
|
|
// Console calls should NOT be queued or overridden (i.e. they should just run vi.fn mocks)
|
|
console.log('Test message');
|
|
expect(console.log).toHaveBeenCalledWith('Test message');
|
|
expect(fetchMock).not.toHaveBeenCalledWith('/api/debug/client-logs', expect.any(Object));
|
|
});
|
|
|
|
it('hooks console and flushes logs periodically when status returns enabled', async () => {
|
|
fetchMock.mockImplementation((url, options) => {
|
|
if (url === '/api/debug/status') {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: async () => ({ enabled: true })
|
|
});
|
|
}
|
|
if (url === '/api/debug/client-logs') {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
json: async () => ({ success: true })
|
|
});
|
|
}
|
|
});
|
|
|
|
await initClientLogCapture();
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/debug/status');
|
|
|
|
// Trigger console logs
|
|
console.log('Booting app', { config: 'loaded' });
|
|
console.warn('Deprecated api call');
|
|
console.error('Failed request', new Error('timeout'));
|
|
|
|
// Move timers forward to trigger flush interval (2000ms)
|
|
await vi.advanceTimersByTimeAsync(2000);
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/debug/client-logs', expect.objectContaining({
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}));
|
|
|
|
const lastCall = fetchMock.mock.calls.find(call => call[0] === '/api/debug/client-logs');
|
|
expect(lastCall).toBeDefined();
|
|
|
|
const loggedEntries = JSON.parse(lastCall[1].body);
|
|
expect(loggedEntries).toHaveLength(4); // Includes the interceptor boot message + 3 logs
|
|
|
|
expect(loggedEntries[1].level).toBe('info');
|
|
expect(loggedEntries[1].message).toContain('Booting app {"config":"loaded"}');
|
|
|
|
expect(loggedEntries[2].level).toBe('warn');
|
|
expect(loggedEntries[2].message).toContain('Deprecated api call');
|
|
|
|
expect(loggedEntries[3].level).toBe('error');
|
|
expect(loggedEntries[3].message).toContain('Failed request');
|
|
});
|
|
});
|