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 <noreply@anthropic.com>
This commit is contained in:
8
tests/fixtures/factory.ts
vendored
8
tests/fixtures/factory.ts
vendored
@@ -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>
|
||||
): 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
|
||||
}
|
||||
}
|
||||
|
||||
2
tests/fixtures/other-factories.test.ts
vendored
2
tests/fixtures/other-factories.test.ts
vendored
@@ -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')
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
|
||||
|
||||
@@ -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' }]
|
||||
])
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user