- 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.
168 lines
4.6 KiB
TypeScript
168 lines
4.6 KiB
TypeScript
/**
|
|
* 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()
|
|
})
|
|
})
|