feat(logging): Wave 1 - logging infrastructure complete

- Add logging config to config.yaml with level, auditRetention, appRetention
- Add 4 global exception handlers (uncaughtException, unhandledRejection, render-process-gone, child-process-gone)
- Create audit-logger.ts with JSONL format and 30-day rotation
- Define IPC logger channels (LOGGER_FORWARD) and preload API
- Define audit types (AuditAction enum, AuditEntry interface, AuditStatus enum)
- Add unit tests for audit logger

All typechecks passing. Wave 1 complete.
This commit is contained in:
test
2026-03-08 13:49:31 +08:00
parent f45d3df385
commit 5e6898fb40
9 changed files with 450 additions and 18 deletions

View File

@@ -0,0 +1,167 @@
/**
* Audit Logger Unit Tests
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import fs from 'fs'
import path from 'path'
// Mock fs for file operations
vi.mock('fs', () => ({
default: {
existsSync: vi.fn(() => false),
mkdirSync: vi.fn()
},
existsSync: vi.fn(() => false),
mkdirSync: vi.fn()
}))
// Mock electron app
vi.mock('electron', () => ({
app: {
isReady: vi.fn(() => false),
getPath: vi.fn(() => './logs'),
isPackaged: false
}
}))
// Track calls to winston
interface WinstonCall {
level: string
message: string
}
const winstonCalls: WinstonCall[] = []
// Mock winston
vi.mock('winston', () => ({
default: {
createLogger: vi.fn(() => ({
info: vi.fn((message) => {
winstonCalls.push({ level: 'info', message })
}),
close: vi.fn()
})),
format: {
combine: vi.fn((...args) => args),
timestamp: vi.fn(() => ({ type: 'timestamp' })),
printf: vi.fn((fn) => fn)
}
}
}))
// Mock winston-daily-rotate-file
vi.mock('winston-daily-rotate-file', () => ({
default: vi.fn()
}))
describe('Audit Logger', () => {
beforeEach(() => {
vi.clearAllMocks()
winstonCalls.length = 0
})
afterEach(() => {
vi.resetModules()
})
it('should export logAudit function', async () => {
const { logAudit } = await import('../../src/main/services/logger/audit-logger')
expect(logAudit).toBeDefined()
expect(typeof logAudit).toBe('function')
})
it('should log audit entry with all required fields', async () => {
const { logAudit } = await import('../../src/main/services/logger/audit-logger')
await logAudit('LOGIN', 'user123', {
username: 'john.doe',
computerName: 'DESKTOP-001',
resource: 'ERP_SYSTEM',
status: 'success',
metadata: { sessionId: 'abc-123' }
})
expect(winstonCalls.length).toBe(1)
const call = winstonCalls[0]
expect(call.level).toBe('info')
// Parse the JSONL message
const entry = JSON.parse(call.message as string)
expect(entry.action).toBe('LOGIN')
expect(entry.userId).toBe('user123')
expect(entry.username).toBe('john.doe')
expect(entry.computerName).toBe('DESKTOP-001')
expect(entry.resource).toBe('ERP_SYSTEM')
expect(entry.status).toBe('success')
expect(entry.metadata).toEqual({ sessionId: 'abc-123' })
expect(entry.timestamp).toBeDefined()
})
it('should log audit entry without optional metadata', async () => {
const { logAudit } = await import('../../src/main/services/logger/audit-logger')
await logAudit('LOGOUT', 'user456', {
username: 'jane.smith',
computerName: 'DESKTOP-002',
resource: 'ERP_SYSTEM',
status: 'success'
})
expect(winstonCalls.length).toBe(1)
const call = winstonCalls[0]
const entry = JSON.parse(call.message as string)
expect(entry.action).toBe('LOGOUT')
expect(entry.userId).toBe('user456')
expect(entry.username).toBe('jane.smith')
expect(entry.computerName).toBe('DESKTOP-002')
expect(entry.resource).toBe('ERP_SYSTEM')
expect(entry.status).toBe('success')
expect(entry.metadata).toEqual({}) // Empty object when not provided
})
it('should handle different status values', async () => {
const { logAudit } = await import('../../src/main/services/logger/audit-logger')
// Test failure status
await logAudit('EXTRACT', 'user789', {
username: 'test.user',
computerName: 'DESKTOP-003',
resource: 'materials_table',
status: 'failure',
metadata: { error: 'Connection timeout' }
})
const call = winstonCalls[0]
const entry = JSON.parse(call.message as string)
expect(entry.status).toBe('failure')
})
it('should log with partial status', async () => {
const { logAudit } = await import('../../src/main/services/logger/audit-logger')
await logAudit('DELETE', 'user999', {
username: 'admin',
computerName: 'DESKTOP-004',
resource: 'temp_files',
status: 'partial',
metadata: { deleted: 5, failed: 2 }
})
const call = winstonCalls[0]
const entry = JSON.parse(call.message as string)
expect(entry.status).toBe('partial')
})
it('should export closeAuditLogger function', async () => {
const { closeAuditLogger } = await import('../../src/main/services/logger/audit-logger')
expect(closeAuditLogger).toBeDefined()
expect(typeof closeAuditLogger).toBe('function')
})
it('should close audit logger without errors', async () => {
const { closeAuditLogger } = await import('../../src/main/services/logger/audit-logger')
await expect(closeAuditLogger()).resolves.toBeUndefined()
})
})