From abad61758c883ee619431812446f39421b7fc582 Mon Sep 17 00:00:00 2001 From: Misaka Date: Mon, 6 Apr 2026 17:40:30 +0800 Subject: [PATCH] refactor(audit): type-safe enums, expanded coverage, and crash-safe logging Replace magic strings with AuditAction/AuditStatus enums across all consumers, add logAuditWithCurrentUser() convenience wrapper, extend audit coverage to data import, result export, app update, and ERP credentials operations, and harden crash handlers with try/catch to prevent audit failures from cascading. Co-Authored-By: Claude Opus 4.6 --- src/main/bootstrap/process-guards.ts | 40 +++++---- src/main/ipc/extractor-handler.ts | 26 +++--- src/main/ipc/settings-handler.ts | 13 ++- .../services/auth/auth-application-service.ts | 19 ++-- .../cleaner/cleaner-application-service.ts | 33 +++---- src/main/services/database/data-importer.ts | 16 ++++ src/main/services/excel/result-exporter.ts | 18 +++- src/main/services/logger/audit-logger.ts | 88 ++++++++++--------- src/main/services/update/update-service.ts | 52 +++++++++-- .../services/user/user-erp-config-service.ts | 28 ++++++ src/main/types/audit.types.ts | 24 +++-- tests/fixtures/factory.ts | 8 +- tests/fixtures/other-factories.test.ts | 2 +- tests/unit/audit-logger.test.ts | 25 +++--- tests/unit/mysql.test.ts | 5 +- tests/unit/postgresql.test.ts | 18 ++-- tests/unit/sql-server.test.ts | 11 ++- 17 files changed, 269 insertions(+), 157 deletions(-) diff --git a/src/main/bootstrap/process-guards.ts b/src/main/bootstrap/process-guards.ts index da0a7da..d561f3f 100644 --- a/src/main/bootstrap/process-guards.ts +++ b/src/main/bootstrap/process-guards.ts @@ -1,32 +1,42 @@ import { app } from 'electron' import logger from '../services/logger/index' import { logAudit, closeAuditLogger } from '../services/logger/audit-logger' +import { AuditAction, AuditStatus } from '../types/audit.types' import { serializeError } from '../services/logger/error-utils' export function setupProcessGuards(): void { process.on('uncaughtException', (err) => { logger.error('Uncaught exception', { error: err }) - logAudit('SYSTEM_CRASH', 'system', { - username: 'system', - computerName: process.env.COMPUTERNAME || 'unknown', - resource: 'main-process', - status: 'failure', - metadata: { error: err.message, stack: err.stack } - }) - setTimeout(() => process.exit(1), 1000) + try { + logAudit(AuditAction.SYSTEM_CRASH, 'system', { + username: 'system', + computerName: process.env.COMPUTERNAME || 'unknown', + resource: 'main-process', + status: AuditStatus.FAILURE, + metadata: { error: err.message, stack: err.stack } + }) + } catch (auditError) { + logger.error('Failed to write crash audit log', { error: auditError }) + } finally { + setTimeout(() => process.exit(1), 1000) + } }) process.on('unhandledRejection', (reason) => { const errorMeta = reason instanceof Error ? { error: serializeError(reason) } : { reason: String(reason) } logger.error('Unhandled Rejection', errorMeta) - logAudit('SYSTEM_ERROR', 'system', { - username: 'system', - computerName: process.env.COMPUTERNAME || 'unknown', - resource: 'main-process', - status: 'failure', - metadata: errorMeta - }) + try { + logAudit(AuditAction.SYSTEM_ERROR, 'system', { + username: 'system', + computerName: process.env.COMPUTERNAME || 'unknown', + resource: 'main-process', + status: AuditStatus.FAILURE, + metadata: errorMeta + }) + } catch (auditError) { + logger.error('Failed to write unhandled rejection audit log', { error: auditError }) + } }) app.on('render-process-gone', (_, webContents, details) => { diff --git a/src/main/ipc/extractor-handler.ts b/src/main/ipc/extractor-handler.ts index f46613d..912c865 100644 --- a/src/main/ipc/extractor-handler.ts +++ b/src/main/ipc/extractor-handler.ts @@ -5,7 +5,8 @@ import { OrderNumberResolver } from '../services/erp/order-resolver' import { create, type IDatabaseService } from '../services/database' import { ExtractorOperationHistoryDAO } from '../services/database/extractor-operation-history-dao' import { createLogger } from '../services/logger' -import { logAudit } from '../services/logger/audit-logger' +import { logAuditWithCurrentUser } from '../services/logger/audit-logger' +import { AuditAction, AuditStatus } from '../types/audit.types' import { SessionManager } from '../services/user/session-manager' import { withErrorHandling, type IpcResult } from './index' import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors' @@ -278,24 +279,17 @@ export function registerExtractorHandlers(): void { } // Audit log: EXTRACT (non-blocking) - const os = await import('os') if (currentUser) { - const auditStatus: 'success' | 'failure' | 'partial' = + const auditStatus: AuditStatus = result.errors.length > 0 && result.recordCount > 0 - ? 'partial' + ? AuditStatus.PARTIAL : result.errors.length > 0 - ? 'failure' - : 'success' - logAudit('EXTRACT', String(currentUser.id), { - username: currentUser.username, - computerName: os.hostname(), - resource: 'MATERIAL_PLAN', - status: auditStatus, - metadata: { - orderCount: validOrderNumbers.length, - recordCount: result.recordCount, - errorCount: result.errors.length - } + ? AuditStatus.FAILURE + : AuditStatus.SUCCESS + logAuditWithCurrentUser(AuditAction.EXTRACT, 'MATERIAL_PLAN', auditStatus, { + orderCount: validOrderNumbers.length, + recordCount: result.recordCount, + errorCount: result.errors.length }) } diff --git a/src/main/ipc/settings-handler.ts b/src/main/ipc/settings-handler.ts index 747d2bb..1e23dd0 100644 --- a/src/main/ipc/settings-handler.ts +++ b/src/main/ipc/settings-handler.ts @@ -5,7 +5,8 @@ import { UserErpConfigService } from '../services/user/user-erp-config-service' import { MySqlService } from '../services/database/mysql' import { SqlServerService } from '../services/database/sql-server' import { createLogger } from '../services/logger' -import { logAudit } from '../services/logger/audit-logger' +import { logAuditWithCurrentUser } from '../services/logger/audit-logger' +import { AuditAction, AuditStatus } from '../types/audit.types' import type { UserType, ConnectionTestResult, SaveSettingsResult } from '../types/settings.types' import { IPC_CHANNELS } from '../../shared/ipc-channels' import { ValidationError } from '../types/errors' @@ -67,13 +68,9 @@ export function registerSettingsHandlers(): void { }) // Audit log: SETTINGS_CHANGE (non-blocking) - const os = await import('os') - logAudit('SETTINGS_CHANGE', String(currentUser.id), { - username: currentUser.username, - computerName: os.hostname(), - resource: 'ERP_CONFIG', - status: 'success', - metadata: { changeType: 'erp_credentials', usernameChanged: !!settings.erp.username } + logAuditWithCurrentUser(AuditAction.SETTINGS_CHANGE, 'ERP_CONFIG', AuditStatus.SUCCESS, { + changeType: 'erp_credentials', + usernameChanged: !!settings.erp.username }) } diff --git a/src/main/services/auth/auth-application-service.ts b/src/main/services/auth/auth-application-service.ts index 8ac6df7..1986b3d 100644 --- a/src/main/services/auth/auth-application-service.ts +++ b/src/main/services/auth/auth-application-service.ts @@ -3,6 +3,7 @@ import { SessionManager } from '../user/session-manager' import { UpdateService } from '../update/update-service' import { createLogger, run, getRequestId, getContext } from '../logger' import { logAudit } from '../logger/audit-logger' +import { AuditAction, AuditStatus } from '../../types/audit.types' import { ValidationError } from '../../types/errors' import type { UserInfo } from '../../types/user.types' import type { @@ -73,11 +74,11 @@ export class AuthApplicationService { userId: userInfo.id }) - this.writeAuditLog('LOGIN', String(userInfo.id), { + this.writeAuditLog(AuditAction.LOGIN, String(userInfo.id), { username: userInfo.username, computerName: hostname(), resource: 'ERP_SYSTEM', - status: 'success', + status: AuditStatus.SUCCESS, metadata: { loginType: 'silent', userType: userInfo.userType } }) @@ -127,11 +128,11 @@ export class AuthApplicationService { const userInfo = this.sessionManager.getUserInfo() if (!success || !userInfo) { - this.writeAuditLog('LOGIN', '0', { + this.writeAuditLog(AuditAction.LOGIN, '0', { username, computerName: hostname(), resource: 'ERP_SYSTEM', - status: 'failure', + status: AuditStatus.FAILURE, metadata: { loginType: 'credentials', reason: 'invalid_credentials' } }) @@ -155,11 +156,11 @@ export class AuthApplicationService { }) await this.updateService.setUserContext(userInfo.userType) - this.writeAuditLog('LOGIN', String(userInfo.id), { + this.writeAuditLog(AuditAction.LOGIN, String(userInfo.id), { username: userInfo.username, computerName: hostname(), resource: 'ERP_SYSTEM', - status: 'success', + status: AuditStatus.SUCCESS, metadata: { loginType: 'credentials', userType: userInfo.userType } }) @@ -204,11 +205,11 @@ export class AuthApplicationService { }) if (userInfo) { - this.writeAuditLog('LOGOUT', String(userInfo.id), { + this.writeAuditLog(AuditAction.LOGOUT, String(userInfo.id), { username: userInfo.username, computerName: hostname(), resource: 'ERP_SYSTEM', - status: 'success', + status: AuditStatus.SUCCESS, metadata: { userType: userInfo.userType } }) } @@ -310,7 +311,7 @@ export class AuthApplicationService { } private writeAuditLog( - action: 'LOGIN' | 'LOGOUT', + action: AuditAction.LOGIN | AuditAction.LOGOUT, actorId: string, payload: Parameters[2] ): void { diff --git a/src/main/services/cleaner/cleaner-application-service.ts b/src/main/services/cleaner/cleaner-application-service.ts index 59de3a3..73d7441 100644 --- a/src/main/services/cleaner/cleaner-application-service.ts +++ b/src/main/services/cleaner/cleaner-application-service.ts @@ -13,7 +13,8 @@ import { RustfsService } from '../rustfs' import { SessionManager } from '../user/session-manager' import { UserErpConfigService } from '../user/user-erp-config-service' import { createLogger } from '../logger' -import { logAudit } from '../logger/audit-logger' +import { logAuditWithCurrentUser } from '../logger/audit-logger' +import { AuditAction, AuditStatus } from '../../types/audit.types' import { IPC_CHANNELS } from '../../../shared/ipc-channels' import { DatabaseQueryError, ErpConnectionError, ValidationError } from '../../types/errors' import type { @@ -278,27 +279,21 @@ export class CleanerApplicationService { return } - const status: 'success' | 'failure' | 'partial' = + const status: AuditStatus = result.errors.length > 0 && result.materialsDeleted > 0 - ? 'partial' + ? AuditStatus.PARTIAL : result.errors.length > 0 - ? 'failure' - : 'success' + ? AuditStatus.FAILURE + : AuditStatus.SUCCESS - logAudit('CLEAN', String(currentUser.id), { - username: currentUser.username, - computerName: (await import('os')).hostname(), - resource: 'MATERIAL_PLAN', - status, - metadata: { - orderCount, - dryRun: input.dryRun ?? false, - queryBatchSize: input.queryBatchSize ?? 100, - processConcurrency: input.processConcurrency ?? 1, - materialsDeleted: result.materialsDeleted, - materialsSkipped: result.materialsSkipped, - errorCount: result.errors.length - } + logAuditWithCurrentUser(AuditAction.CLEAN, 'MATERIAL_PLAN', status, { + orderCount, + dryRun: input.dryRun ?? false, + queryBatchSize: input.queryBatchSize ?? 100, + processConcurrency: input.processConcurrency ?? 1, + materialsDeleted: result.materialsDeleted, + materialsSkipped: result.materialsSkipped, + errorCount: result.errors.length }) } diff --git a/src/main/services/database/data-importer.ts b/src/main/services/database/data-importer.ts index 853f7b5..d6a8dd5 100644 --- a/src/main/services/database/data-importer.ts +++ b/src/main/services/database/data-importer.ts @@ -10,6 +10,8 @@ */ import { createLogger } from '../logger' +import { logAuditWithCurrentUser } from '../logger/audit-logger' +import { AuditAction, AuditStatus } from '../../types/audit.types' import { DiscreteMaterialPlanDAO, type MaterialPlanRecord } from './discrete-material-plan-dao' const log = createLogger('DataImportService') @@ -148,6 +150,20 @@ export class DataImportService { } } + // Audit log: DATA_IMPORT + logAuditWithCurrentUser( + AuditAction.DATA_IMPORT, + 'MATERIAL_PLAN', + result.success ? AuditStatus.SUCCESS : AuditStatus.FAILURE, + { + recordsRead: result.recordsRead, + recordsDeleted: result.recordsDeleted, + recordsImported: result.recordsImported, + uniqueSourceNumbers: result.uniqueSourceNumbers, + errorCount: result.errors.length + } + ) + return result } diff --git a/src/main/services/excel/result-exporter.ts b/src/main/services/excel/result-exporter.ts index e8756b6..21e7866 100644 --- a/src/main/services/excel/result-exporter.ts +++ b/src/main/services/excel/result-exporter.ts @@ -3,6 +3,8 @@ import path from 'path' import { app } from 'electron' import fs from 'fs' import { createLogger } from '../logger' +import { logAuditWithCurrentUser } from '../logger/audit-logger' +import { AuditAction, AuditStatus } from '../../types/audit.types' import type { ExportResultItem, ExportResultResponse } from '../../types/cleaner.types' const log = createLogger('ResultExporter') @@ -37,8 +39,8 @@ export class ResultExporter { * @returns Export result with file path or error */ async exportValidationResults(items: ExportResultItem[]): Promise { + const filePath = path.join(this.exportDir, this.fileName) try { - const filePath = path.join(this.exportDir, this.fileName) log.info('Exporting validation results', { count: items.length, path: filePath }) const workbook = new ExcelJS.Workbook() @@ -97,6 +99,12 @@ export class ResultExporter { await workbook.xlsx.writeFile(filePath) log.info('Export completed', { path: filePath, rows: items.length }) + // Audit log: RESULT_EXPORT success + logAuditWithCurrentUser(AuditAction.RESULT_EXPORT, 'VALIDATION_RESULT', AuditStatus.SUCCESS, { + itemCount: items.length, + filePath + }) + return { success: true, filePath @@ -104,6 +112,14 @@ export class ResultExporter { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) log.error('Export failed', { error: errorMessage }) + + // Audit log: RESULT_EXPORT failure + logAuditWithCurrentUser(AuditAction.RESULT_EXPORT, 'VALIDATION_RESULT', AuditStatus.FAILURE, { + itemCount: items.length, + filePath, + error: errorMessage + }) + return { success: false, error: errorMessage diff --git a/src/main/services/logger/audit-logger.ts b/src/main/services/logger/audit-logger.ts index 4283658..617f8eb 100644 --- a/src/main/services/logger/audit-logger.ts +++ b/src/main/services/logger/audit-logger.ts @@ -6,33 +6,12 @@ import winston from 'winston' import DailyRotateFile from 'winston-daily-rotate-file' import path from 'path' +import { hostname } from 'os' import { app } from 'electron' import { getLogDir } from './shared' - -/** - * Audit log entry structure - * All 8 required fields for comprehensive audit tracking - */ -export interface AuditEntry { - /** ISO 8601 timestamp of the audit event */ - timestamp: string - /** The action that was performed (e.g., 'LOGIN', 'EXTRACT', 'DELETE') */ - action: string - /** User ID who performed the action */ - userId: string - /** Username of the user who performed the action */ - username: string - /** Computer name from which the action was performed */ - computerName: string - /** Application version when the action was performed */ - appVersion: string - /** The resource that was affected (e.g., table name, file path) */ - resource: string - /** Status of the action: 'success' | 'failure' | 'partial' */ - status: 'success' | 'failure' | 'partial' - /** Additional metadata about the audit event */ - metadata: Record -} +import { SessionManager } from '../user/session-manager' +import type { AuditEntry } from '../../types/audit.types' +import { AuditAction, AuditStatus } from '../../types/audit.types' /** * JSONL formatter - outputs one JSON object per line @@ -91,31 +70,58 @@ export function applyAuditConfig(retentionDays: number): void { * @param details - Additional details including username, computerName, resource, status, and optional metadata */ export function logAudit( - action: string, + action: AuditAction, userId: string, details: { username: string computerName: string resource: string - status: 'success' | 'failure' | 'partial' + status: AuditStatus metadata?: Record } ): void { - const entry: AuditEntry = { - timestamp: new Date().toISOString(), - action, - userId, - username: details.username, - computerName: details.computerName, - appVersion: app.getVersion(), - resource: details.resource, - status: details.status, - metadata: details.metadata || {} - } + try { + const entry: AuditEntry = { + timestamp: new Date().toISOString(), + action, + userId, + username: details.username, + computerName: details.computerName, + appVersion: app.getVersion(), + resource: details.resource, + status: details.status, + metadata: details.metadata ?? {} + } - // Write as JSONL - one JSON object per line - // Using info level with the entry stringified as the message - auditLogger.info(JSON.stringify(entry)) + // Write as JSONL - one JSON object per line + // Using info level with the entry stringified as the message + auditLogger.info(JSON.stringify(entry)) + } catch (error) { + console.error('Audit logging failed:', error) + } +} + +/** Cached hostname — invariant for the app lifecycle */ +const cachedHostname = hostname() + +/** + * Audit log shortcut that auto-resolves the current user context. + * Falls back to 'anonymous' if no user is logged in, so the record is always written. + */ +export function logAuditWithCurrentUser( + action: AuditAction, + resource: string, + status: AuditStatus, + metadata?: Record +): void { + const user = SessionManager.getInstance().getUserInfo() + logAudit(action, user ? String(user.id) : 'anonymous', { + username: user?.username ?? 'anonymous', + computerName: cachedHostname, + resource, + status, + metadata: metadata ?? {} + }) } /** diff --git a/src/main/services/update/update-service.ts b/src/main/services/update/update-service.ts index fc6ce4e..5dc47f8 100644 --- a/src/main/services/update/update-service.ts +++ b/src/main/services/update/update-service.ts @@ -1,6 +1,8 @@ import * as fs from 'fs' import { ConfigManager } from '../config/config-manager' import { createLogger } from '../logger' +import { logAuditWithCurrentUser } from '../logger/audit-logger' +import { AuditAction, AuditStatus } from '../../types/audit.types' import type { UpdateConfig } from '../../types/config.schema' import type { UserType } from '../../types/user.types' import type { @@ -281,10 +283,21 @@ export class UpdateService { log.error('Update package hash mismatch', { version: request.version, channel: request.channel, - expectedHash: request.sha256, - actualHash: hash + expectedHash: request.sha256.substring(0, 16), + actualHash: hash.substring(0, 16) }) await fs.promises.rm(downloadPath, { force: true }) + + // Audit log: APP_UPDATE download hash mismatch + logAuditWithCurrentUser(AuditAction.APP_UPDATE, 'UPDATE_PACKAGE', AuditStatus.FAILURE, { + version: request.version, + channel: request.channel, + phase: 'download', + error: 'Hash mismatch', + expectedHash: request.sha256.substring(0, 16), + actualHash: hash.substring(0, 16) + }) + throw new Error('更新包校验失败,文件哈希不匹配') } @@ -294,6 +307,13 @@ export class UpdateService { downloadPath }) + // Audit log: APP_UPDATE download success + logAuditWithCurrentUser(AuditAction.APP_UPDATE, 'UPDATE_PACKAGE', AuditStatus.SUCCESS, { + version: request.version, + channel: request.channel, + phase: 'download' + }) + this.publishStatus({ phase: 'downloaded', progress: 100, @@ -336,11 +356,29 @@ export class UpdateService { error: undefined }) - await this.installer.installDownloadedRelease(downloaded) - log.info('Update installation completed', { - version: downloaded.version, - channel: downloaded.channel - }) + try { + await this.installer.installDownloadedRelease(downloaded) + log.info('Update installation completed', { + version: downloaded.version, + channel: downloaded.channel + }) + + // Audit log: APP_UPDATE install success + logAuditWithCurrentUser(AuditAction.APP_UPDATE, 'UPDATE_PACKAGE', AuditStatus.SUCCESS, { + version: downloaded.version, + channel: downloaded.channel, + phase: 'install' + }) + } catch (installError) { + const msg = installError instanceof Error ? installError.message : String(installError) + logAuditWithCurrentUser(AuditAction.APP_UPDATE, 'UPDATE_PACKAGE', AuditStatus.FAILURE, { + version: downloaded.version, + channel: downloaded.channel, + phase: 'install', + error: msg + }) + throw installError + } } private ensureInitialized(): void { diff --git a/src/main/services/user/user-erp-config-service.ts b/src/main/services/user/user-erp-config-service.ts index 52e20f9..d6ceafb 100644 --- a/src/main/services/user/user-erp-config-service.ts +++ b/src/main/services/user/user-erp-config-service.ts @@ -14,6 +14,8 @@ import { BIPUsersDAO } from './bip-users-dao' import { SessionManager } from './session-manager' import { createLogger } from '../logger' +import { logAuditWithCurrentUser } from '../logger/audit-logger' +import { AuditAction, AuditStatus } from '../../types/audit.types' const log = createLogger('UserErpConfigService') @@ -131,9 +133,22 @@ export class UserErpConfigService { log.error('Failed to update ERP credentials', { username: currentUser.username }) } + logAuditWithCurrentUser( + AuditAction.ERP_CREDENTIALS_UPDATE, + 'ERP_CREDENTIALS', + success ? AuditStatus.SUCCESS : AuditStatus.FAILURE, + { targetUsername: currentUser.username, updateType: 'self' } + ) + return success } catch (error) { log.error('Error updating current user ERP credentials', { error }) + logAuditWithCurrentUser( + AuditAction.ERP_CREDENTIALS_UPDATE, + 'ERP_CREDENTIALS', + AuditStatus.FAILURE, + { targetUsername: 'unknown', updateType: 'self', error: String(error) } + ) return false } } @@ -159,9 +174,22 @@ export class UserErpConfigService { log.error('Failed to update ERP credentials', { username }) } + logAuditWithCurrentUser( + AuditAction.ERP_CREDENTIALS_UPDATE, + 'ERP_CREDENTIALS', + success ? AuditStatus.SUCCESS : AuditStatus.FAILURE, + { targetUsername: username, updateType: 'admin' } + ) + return success } catch (error) { log.error('Error updating user ERP credentials', { error }) + logAuditWithCurrentUser( + AuditAction.ERP_CREDENTIALS_UPDATE, + 'ERP_CREDENTIALS', + AuditStatus.FAILURE, + { targetUsername: username, updateType: 'admin', error: String(error) } + ) return false } } diff --git a/src/main/types/audit.types.ts b/src/main/types/audit.types.ts index 4d8f7c1..098855c 100644 --- a/src/main/types/audit.types.ts +++ b/src/main/types/audit.types.ts @@ -10,23 +10,31 @@ export enum AuditAction { LOGOUT = 'LOGOUT', EXTRACT = 'EXTRACT', CLEAN = 'CLEAN', - SETTINGS_CHANGE = 'SETTINGS_CHANGE' + SETTINGS_CHANGE = 'SETTINGS_CHANGE', + SYSTEM_CRASH = 'SYSTEM_CRASH', + SYSTEM_ERROR = 'SYSTEM_ERROR', + DATA_IMPORT = 'DATA_IMPORT', + RESULT_EXPORT = 'RESULT_EXPORT', + APP_UPDATE = 'APP_UPDATE', + ERP_CREDENTIALS_UPDATE = 'ERP_CREDENTIALS_UPDATE' } /** * Audit status enumeration + * Note: Values are lowercase to match JSON logging conventions */ export enum AuditStatus { - SUCCESS = 'SUCCESS', - FAILURE = 'FAILURE' + SUCCESS = 'success', + FAILURE = 'failure', + PARTIAL = 'partial' } /** * Audit entry interface */ export interface AuditEntry { - /** Timestamp of the action */ - timestamp: Date + /** ISO timestamp of the action */ + timestamp: string /** Action performed */ action: AuditAction /** User ID who performed the action */ @@ -38,9 +46,9 @@ export interface AuditEntry { /** Application version when action was performed */ appVersion: string /** Resource affected by the action */ - resource?: string + resource: string /** Status of the action */ status: AuditStatus - /** Additional metadata in JSON format */ - metadata?: string + /** Additional metadata */ + metadata: Record } diff --git a/tests/fixtures/factory.ts b/tests/fixtures/factory.ts index e6904b3..8414f43 100644 --- a/tests/fixtures/factory.ts +++ b/tests/fixtures/factory.ts @@ -419,11 +419,11 @@ export class AuditLogFactory { * * @example * // Successful login audit - * const entry = AuditLogFactory.createAuditLog('LOGIN', 'SUCCESS') + * const entry = AuditLogFactory.createAuditLog(AuditAction.LOGIN, AuditStatus.SUCCESS) * * @example * // Failed extract audit - * const entry = AuditLogFactory.createAuditLog('EXTRACT', 'FAILURE', { resource: 'Order SC123' }) + * const entry = AuditLogFactory.createAuditLog(AuditAction.EXTRACT, AuditStatus.FAILURE, { resource: 'Order SC123' }) */ static createAuditLog( action: AuditAction = AuditAction.LOGIN, @@ -431,13 +431,15 @@ export class AuditLogFactory { overrides?: Partial ): AuditEntry { return { - timestamp: new Date(), + timestamp: new Date().toISOString(), action, userId: 'USR-001', username: 'test_user', computerName: 'TEST-PC', appVersion: '1.0.0', + resource: 'test-resource', status, + metadata: {}, ...overrides } } diff --git a/tests/fixtures/other-factories.test.ts b/tests/fixtures/other-factories.test.ts index af58d75..d4abdc0 100644 --- a/tests/fixtures/other-factories.test.ts +++ b/tests/fixtures/other-factories.test.ts @@ -91,7 +91,7 @@ describe('AuditLogFactory', () => { it('creates audit log with default values', () => { const entry = AuditLogFactory.createAuditLog() - expect(entry.timestamp).toBeInstanceOf(Date) + expect(typeof entry.timestamp).toBe('string') expect(entry.action).toBe(AuditAction.LOGIN) expect(entry.status).toBe(AuditStatus.SUCCESS) expect(entry.userId).toBe('USR-001') diff --git a/tests/unit/audit-logger.test.ts b/tests/unit/audit-logger.test.ts index 83c452b..56d6e64 100644 --- a/tests/unit/audit-logger.test.ts +++ b/tests/unit/audit-logger.test.ts @@ -8,6 +8,7 @@ */ 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') @@ -32,11 +33,11 @@ describe('Audit Logger', () => { const { logAudit, applyAuditConfig } = auditLoggerModule applyAuditConfig(30) - logAudit('LOGIN', 'user-001', { + logAudit(AuditAction.LOGIN, 'user-001', { username: 'alice', computerName: 'PC-001', resource: 'ERP_SYSTEM', - status: 'success', + status: AuditStatus.SUCCESS, metadata: { sessionId: 'abc' } }) @@ -60,26 +61,26 @@ describe('Audit Logger', () => { applyAuditConfig(30) - logAudit('EXTRACT', 'user1', { + logAudit(AuditAction.EXTRACT, 'user1', { username: 'extractor', computerName: 'PC-001', resource: 'materials', - status: 'success' + status: AuditStatus.SUCCESS }) - logAudit('DELETE', 'user2', { + logAudit(AuditAction.CLEAN, 'user2', { username: 'cleaner', computerName: 'PC-002', resource: 'temp_files', - status: 'failure', + status: AuditStatus.FAILURE, metadata: { error: 'Permission denied' } }) - logAudit('UPDATE', 'user3', { + logAudit(AuditAction.APP_UPDATE, 'user3', { username: 'updater', computerName: 'PC-003', resource: 'config', - status: 'partial', + status: AuditStatus.PARTIAL, metadata: { updated: 5, failed: 2 } }) @@ -95,11 +96,11 @@ describe('Audit Logger', () => { applyAuditConfig(30) - logAudit('PING', 'user-no-meta', { + logAudit(AuditAction.SYSTEM_ERROR, 'user-no-meta', { username: 'tester', computerName: 'PC-001', resource: 'ERP', - status: 'success' + status: AuditStatus.SUCCESS }) const entry = JSON.parse(infoSpy.mock.calls[0][0]) @@ -111,11 +112,11 @@ describe('Audit Logger', () => { applyAuditConfig(30) - logAudit('LOGIN_ATTEMPT', 'user-special', { + logAudit(AuditAction.LOGIN, 'user-special', { username: 'user.name+test@example.com', computerName: 'DESKTOP-特殊字符-001', resource: 'ERP/子系统', - status: 'failure', + status: AuditStatus.FAILURE, metadata: { reason: '密码错误', attempt: 3 } }) diff --git a/tests/unit/mysql.test.ts b/tests/unit/mysql.test.ts index 70afb99..11607b8 100644 --- a/tests/unit/mysql.test.ts +++ b/tests/unit/mysql.test.ts @@ -132,7 +132,10 @@ describe('MySqlService Unit Tests', () => { it('should execute SELECT and return rows with columns', async () => { await service.connect() mockExecute.mockResolvedValue([ - [{ id: 1, name: 'test' }, { id: 2, name: 'foo' }], + [ + { id: 1, name: 'test' }, + { id: 2, name: 'foo' } + ], [{ name: 'id' }, { name: 'name' }] ]) diff --git a/tests/unit/postgresql.test.ts b/tests/unit/postgresql.test.ts index 78ec25e..4f888b6 100644 --- a/tests/unit/postgresql.test.ts +++ b/tests/unit/postgresql.test.ts @@ -168,10 +168,7 @@ describe('PostgreSqlService Unit Tests', () => { await service.connect() mockPgClient.query.mockResolvedValue({ rows: [] }) - await service.transaction([ - { sql: 'SELECT 1', params: [1] }, - { sql: 'SELECT 2' } - ]) + await service.transaction([{ sql: 'SELECT 1', params: [1] }, { sql: 'SELECT 2' }]) // BEGIN + 2 queries + COMMIT expect(mockPgClient.query).toHaveBeenCalledTimes(4) @@ -187,9 +184,9 @@ describe('PostgreSqlService Unit Tests', () => { .mockRejectedValueOnce(new Error('constraint violation')) // query fails .mockResolvedValueOnce({ rows: [] }) // ROLLBACK - await expect( - service.transaction([{ sql: 'SELECT 1', params: [1] }]) - ).rejects.toThrow('PostgreSQL transaction failed') + await expect(service.transaction([{ sql: 'SELECT 1', params: [1] }])).rejects.toThrow( + 'PostgreSQL transaction failed' + ) expect(mockPgClient.query).toHaveBeenCalledWith('ROLLBACK') expect(mockPgClient.release).toHaveBeenCalled() @@ -234,8 +231,7 @@ describe('prepareSql', () => { }) it('should quote column names in INSERT', () => { - const sql = - 'INSERT INTO "dbo"."BIPUsers" (UserName, Password, UserType) VALUES ($1, $2, $3)' + const sql = 'INSERT INTO "dbo"."BIPUsers" (UserName, Password, UserType) VALUES ($1, $2, $3)' const result = prepareSql(sql) expect(result).toBe( 'INSERT INTO "dbo"."BIPUsers" ("UserName", "Password", "UserType") VALUES ($1, $2, $3)' @@ -303,9 +299,7 @@ describe('prepareSql', () => { it('should quote underscore-containing column names', () => { const sql = 'SELECT ERP_URL, ERP_Username, ERP_Password FROM "dbo"."BIPUsers"' const result = prepareSql(sql) - expect(result).toBe( - 'SELECT "ERP_URL", "ERP_Username", "ERP_Password" FROM "dbo"."BIPUsers"' - ) + expect(result).toBe('SELECT "ERP_URL", "ERP_Username", "ERP_Password" FROM "dbo"."BIPUsers"') }) it('should handle ON CONFLICT DO UPDATE SET with EXCLUDED', () => { diff --git a/tests/unit/sql-server.test.ts b/tests/unit/sql-server.test.ts index 5f00ccc..e6e1a62 100644 --- a/tests/unit/sql-server.test.ts +++ b/tests/unit/sql-server.test.ts @@ -149,7 +149,10 @@ describe('SqlServerService Unit Tests', () => { it('should execute SELECT and return rows with columns', async () => { await service.connect() mockRequestQuery.mockResolvedValue({ - recordset: [{ ID: 1, Name: 'test' }, { ID: 2, Name: 'foo' }], + recordset: [ + { ID: 1, Name: 'test' }, + { ID: 2, Name: 'foo' } + ], rowsAffected: [2] }) @@ -209,9 +212,9 @@ describe('SqlServerService Unit Tests', () => { describe('queryWithParams', () => { it('should throw error when not connected', async () => { - await expect( - service.queryWithParams('SELECT @p0', { p0: { value: 1 } }) - ).rejects.toThrow('Not connected to SQL Server') + await expect(service.queryWithParams('SELECT @p0', { p0: { value: 1 } })).rejects.toThrow( + 'Not connected to SQL Server' + ) }) it('should add typed params via request.input', async () => {