/** * Audit Logger Unit Tests * * Tests audit logger behavior: verifies JSONL entry content, * status handling, metadata processing, and special characters. * Uses spy on the module's audit logger instance instead of mocking winston, * to avoid cross-contamination with logger.test.ts under isolate:false. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { AuditAction, AuditStatus } from '../../src/main/types/audit.types' describe('Audit Logger', () => { let auditLoggerModule: typeof import('../../src/main/services/logger/audit-logger') let infoSpy: ReturnType beforeEach(async () => { vi.clearAllMocks() auditLoggerModule = await import('../../src/main/services/logger/audit-logger') // Spy on the audit logger's info method const auditLogger = auditLoggerModule.default infoSpy = vi.fn() auditLogger.info = infoSpy }) afterEach(() => { vi.restoreAllMocks() vi.resetModules() }) it('should produce a valid JSONL entry with all required fields', async () => { const { logAudit, applyAuditConfig } = auditLoggerModule applyAuditConfig(30) logAudit(AuditAction.LOGIN, 'user-001', { username: 'alice', computerName: 'PC-001', resource: 'ERP_SYSTEM', status: AuditStatus.SUCCESS, metadata: { sessionId: 'abc' } }) expect(infoSpy).toHaveBeenCalledTimes(1) const entry = JSON.parse(infoSpy.mock.calls[0][0]) expect(entry.action).toBe('LOGIN') expect(entry.userId).toBe('user-001') expect(entry.username).toBe('alice') expect(entry.computerName).toBe('PC-001') expect(entry.appVersion).toBe('1.9.0-test') expect(entry.resource).toBe('ERP_SYSTEM') expect(entry.status).toBe('success') expect(entry.metadata).toEqual({ sessionId: 'abc' }) // Timestamp should be a valid ISO 8601 string expect(new Date(entry.timestamp).toISOString()).toBe(entry.timestamp) }) it('should accept all status values: success, failure, partial', async () => { const { logAudit, applyAuditConfig } = auditLoggerModule applyAuditConfig(30) logAudit(AuditAction.EXTRACT, 'user1', { username: 'extractor', computerName: 'PC-001', resource: 'materials', status: AuditStatus.SUCCESS }) logAudit(AuditAction.CLEAN, 'user2', { username: 'cleaner', computerName: 'PC-002', resource: 'temp_files', status: AuditStatus.FAILURE, metadata: { error: 'Permission denied' } }) logAudit(AuditAction.APP_UPDATE, 'user3', { username: 'updater', computerName: 'PC-003', resource: 'config', status: AuditStatus.PARTIAL, metadata: { updated: 5, failed: 2 } }) expect(infoSpy).toHaveBeenCalledTimes(3) const entries = infoSpy.mock.calls.map((call: any[]) => JSON.parse(call[0])) expect(entries[0].status).toBe('success') expect(entries[1].status).toBe('failure') expect(entries[2].status).toBe('partial') }) it('should default to empty metadata when not provided', async () => { const { logAudit, applyAuditConfig } = auditLoggerModule applyAuditConfig(30) logAudit(AuditAction.SYSTEM_ERROR, 'user-no-meta', { username: 'tester', computerName: 'PC-001', resource: 'ERP', status: AuditStatus.SUCCESS }) const entry = JSON.parse(infoSpy.mock.calls[0][0]) expect(entry.metadata).toEqual({}) }) it('should handle special characters in fields without error', async () => { const { logAudit, applyAuditConfig } = auditLoggerModule applyAuditConfig(30) logAudit(AuditAction.LOGIN, 'user-special', { username: 'user.name+test@example.com', computerName: 'DESKTOP-特殊字符-001', resource: 'ERP/子系统', status: AuditStatus.FAILURE, metadata: { reason: '密码错误', attempt: 3 } }) expect(infoSpy).toHaveBeenCalledTimes(1) const entry = JSON.parse(infoSpy.mock.calls[0][0]) expect(entry.username).toBe('user.name+test@example.com') expect(entry.computerName).toBe('DESKTOP-特殊字符-001') expect(entry.resource).toBe('ERP/子系统') expect(entry.metadata.reason).toBe('密码错误') }) it('should close audit logger without errors', async () => { const { closeAuditLogger } = auditLoggerModule expect(() => closeAuditLogger()).not.toThrow() }) })