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:
@@ -1,32 +1,42 @@
|
|||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import logger from '../services/logger/index'
|
import logger from '../services/logger/index'
|
||||||
import { logAudit, closeAuditLogger } from '../services/logger/audit-logger'
|
import { logAudit, closeAuditLogger } from '../services/logger/audit-logger'
|
||||||
|
import { AuditAction, AuditStatus } from '../types/audit.types'
|
||||||
import { serializeError } from '../services/logger/error-utils'
|
import { serializeError } from '../services/logger/error-utils'
|
||||||
|
|
||||||
export function setupProcessGuards(): void {
|
export function setupProcessGuards(): void {
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', (err) => {
|
||||||
logger.error('Uncaught exception', { error: err })
|
logger.error('Uncaught exception', { error: err })
|
||||||
logAudit('SYSTEM_CRASH', 'system', {
|
try {
|
||||||
username: 'system',
|
logAudit(AuditAction.SYSTEM_CRASH, 'system', {
|
||||||
computerName: process.env.COMPUTERNAME || 'unknown',
|
username: 'system',
|
||||||
resource: 'main-process',
|
computerName: process.env.COMPUTERNAME || 'unknown',
|
||||||
status: 'failure',
|
resource: 'main-process',
|
||||||
metadata: { error: err.message, stack: err.stack }
|
status: AuditStatus.FAILURE,
|
||||||
})
|
metadata: { error: err.message, stack: err.stack }
|
||||||
setTimeout(() => process.exit(1), 1000)
|
})
|
||||||
|
} catch (auditError) {
|
||||||
|
logger.error('Failed to write crash audit log', { error: auditError })
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => process.exit(1), 1000)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason) => {
|
process.on('unhandledRejection', (reason) => {
|
||||||
const errorMeta =
|
const errorMeta =
|
||||||
reason instanceof Error ? { error: serializeError(reason) } : { reason: String(reason) }
|
reason instanceof Error ? { error: serializeError(reason) } : { reason: String(reason) }
|
||||||
logger.error('Unhandled Rejection', errorMeta)
|
logger.error('Unhandled Rejection', errorMeta)
|
||||||
logAudit('SYSTEM_ERROR', 'system', {
|
try {
|
||||||
username: 'system',
|
logAudit(AuditAction.SYSTEM_ERROR, 'system', {
|
||||||
computerName: process.env.COMPUTERNAME || 'unknown',
|
username: 'system',
|
||||||
resource: 'main-process',
|
computerName: process.env.COMPUTERNAME || 'unknown',
|
||||||
status: 'failure',
|
resource: 'main-process',
|
||||||
metadata: errorMeta
|
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) => {
|
app.on('render-process-gone', (_, webContents, details) => {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { OrderNumberResolver } from '../services/erp/order-resolver'
|
|||||||
import { create, type IDatabaseService } from '../services/database'
|
import { create, type IDatabaseService } from '../services/database'
|
||||||
import { ExtractorOperationHistoryDAO } from '../services/database/extractor-operation-history-dao'
|
import { ExtractorOperationHistoryDAO } from '../services/database/extractor-operation-history-dao'
|
||||||
import { createLogger } from '../services/logger'
|
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 { SessionManager } from '../services/user/session-manager'
|
||||||
import { withErrorHandling, type IpcResult } from './index'
|
import { withErrorHandling, type IpcResult } from './index'
|
||||||
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
|
import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../types/errors'
|
||||||
@@ -278,24 +279,17 @@ export function registerExtractorHandlers(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Audit log: EXTRACT (non-blocking)
|
// Audit log: EXTRACT (non-blocking)
|
||||||
const os = await import('os')
|
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
const auditStatus: 'success' | 'failure' | 'partial' =
|
const auditStatus: AuditStatus =
|
||||||
result.errors.length > 0 && result.recordCount > 0
|
result.errors.length > 0 && result.recordCount > 0
|
||||||
? 'partial'
|
? AuditStatus.PARTIAL
|
||||||
: result.errors.length > 0
|
: result.errors.length > 0
|
||||||
? 'failure'
|
? AuditStatus.FAILURE
|
||||||
: 'success'
|
: AuditStatus.SUCCESS
|
||||||
logAudit('EXTRACT', String(currentUser.id), {
|
logAuditWithCurrentUser(AuditAction.EXTRACT, 'MATERIAL_PLAN', auditStatus, {
|
||||||
username: currentUser.username,
|
orderCount: validOrderNumbers.length,
|
||||||
computerName: os.hostname(),
|
recordCount: result.recordCount,
|
||||||
resource: 'MATERIAL_PLAN',
|
errorCount: result.errors.length
|
||||||
status: auditStatus,
|
|
||||||
metadata: {
|
|
||||||
orderCount: validOrderNumbers.length,
|
|
||||||
recordCount: result.recordCount,
|
|
||||||
errorCount: result.errors.length
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { UserErpConfigService } from '../services/user/user-erp-config-service'
|
|||||||
import { MySqlService } from '../services/database/mysql'
|
import { MySqlService } from '../services/database/mysql'
|
||||||
import { SqlServerService } from '../services/database/sql-server'
|
import { SqlServerService } from '../services/database/sql-server'
|
||||||
import { createLogger } from '../services/logger'
|
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 type { UserType, ConnectionTestResult, SaveSettingsResult } from '../types/settings.types'
|
||||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||||
import { ValidationError } from '../types/errors'
|
import { ValidationError } from '../types/errors'
|
||||||
@@ -67,13 +68,9 @@ export function registerSettingsHandlers(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Audit log: SETTINGS_CHANGE (non-blocking)
|
// Audit log: SETTINGS_CHANGE (non-blocking)
|
||||||
const os = await import('os')
|
logAuditWithCurrentUser(AuditAction.SETTINGS_CHANGE, 'ERP_CONFIG', AuditStatus.SUCCESS, {
|
||||||
logAudit('SETTINGS_CHANGE', String(currentUser.id), {
|
changeType: 'erp_credentials',
|
||||||
username: currentUser.username,
|
usernameChanged: !!settings.erp.username
|
||||||
computerName: os.hostname(),
|
|
||||||
resource: 'ERP_CONFIG',
|
|
||||||
status: 'success',
|
|
||||||
metadata: { changeType: 'erp_credentials', usernameChanged: !!settings.erp.username }
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { SessionManager } from '../user/session-manager'
|
|||||||
import { UpdateService } from '../update/update-service'
|
import { UpdateService } from '../update/update-service'
|
||||||
import { createLogger, run, getRequestId, getContext } from '../logger'
|
import { createLogger, run, getRequestId, getContext } from '../logger'
|
||||||
import { logAudit } from '../logger/audit-logger'
|
import { logAudit } from '../logger/audit-logger'
|
||||||
|
import { AuditAction, AuditStatus } from '../../types/audit.types'
|
||||||
import { ValidationError } from '../../types/errors'
|
import { ValidationError } from '../../types/errors'
|
||||||
import type { UserInfo } from '../../types/user.types'
|
import type { UserInfo } from '../../types/user.types'
|
||||||
import type {
|
import type {
|
||||||
@@ -73,11 +74,11 @@ export class AuthApplicationService {
|
|||||||
userId: userInfo.id
|
userId: userInfo.id
|
||||||
})
|
})
|
||||||
|
|
||||||
this.writeAuditLog('LOGIN', String(userInfo.id), {
|
this.writeAuditLog(AuditAction.LOGIN, String(userInfo.id), {
|
||||||
username: userInfo.username,
|
username: userInfo.username,
|
||||||
computerName: hostname(),
|
computerName: hostname(),
|
||||||
resource: 'ERP_SYSTEM',
|
resource: 'ERP_SYSTEM',
|
||||||
status: 'success',
|
status: AuditStatus.SUCCESS,
|
||||||
metadata: { loginType: 'silent', userType: userInfo.userType }
|
metadata: { loginType: 'silent', userType: userInfo.userType }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -127,11 +128,11 @@ export class AuthApplicationService {
|
|||||||
const userInfo = this.sessionManager.getUserInfo()
|
const userInfo = this.sessionManager.getUserInfo()
|
||||||
|
|
||||||
if (!success || !userInfo) {
|
if (!success || !userInfo) {
|
||||||
this.writeAuditLog('LOGIN', '0', {
|
this.writeAuditLog(AuditAction.LOGIN, '0', {
|
||||||
username,
|
username,
|
||||||
computerName: hostname(),
|
computerName: hostname(),
|
||||||
resource: 'ERP_SYSTEM',
|
resource: 'ERP_SYSTEM',
|
||||||
status: 'failure',
|
status: AuditStatus.FAILURE,
|
||||||
metadata: { loginType: 'credentials', reason: 'invalid_credentials' }
|
metadata: { loginType: 'credentials', reason: 'invalid_credentials' }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -155,11 +156,11 @@ export class AuthApplicationService {
|
|||||||
})
|
})
|
||||||
await this.updateService.setUserContext(userInfo.userType)
|
await this.updateService.setUserContext(userInfo.userType)
|
||||||
|
|
||||||
this.writeAuditLog('LOGIN', String(userInfo.id), {
|
this.writeAuditLog(AuditAction.LOGIN, String(userInfo.id), {
|
||||||
username: userInfo.username,
|
username: userInfo.username,
|
||||||
computerName: hostname(),
|
computerName: hostname(),
|
||||||
resource: 'ERP_SYSTEM',
|
resource: 'ERP_SYSTEM',
|
||||||
status: 'success',
|
status: AuditStatus.SUCCESS,
|
||||||
metadata: { loginType: 'credentials', userType: userInfo.userType }
|
metadata: { loginType: 'credentials', userType: userInfo.userType }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -204,11 +205,11 @@ export class AuthApplicationService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (userInfo) {
|
if (userInfo) {
|
||||||
this.writeAuditLog('LOGOUT', String(userInfo.id), {
|
this.writeAuditLog(AuditAction.LOGOUT, String(userInfo.id), {
|
||||||
username: userInfo.username,
|
username: userInfo.username,
|
||||||
computerName: hostname(),
|
computerName: hostname(),
|
||||||
resource: 'ERP_SYSTEM',
|
resource: 'ERP_SYSTEM',
|
||||||
status: 'success',
|
status: AuditStatus.SUCCESS,
|
||||||
metadata: { userType: userInfo.userType }
|
metadata: { userType: userInfo.userType }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -310,7 +311,7 @@ export class AuthApplicationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private writeAuditLog(
|
private writeAuditLog(
|
||||||
action: 'LOGIN' | 'LOGOUT',
|
action: AuditAction.LOGIN | AuditAction.LOGOUT,
|
||||||
actorId: string,
|
actorId: string,
|
||||||
payload: Parameters<typeof logAudit>[2]
|
payload: Parameters<typeof logAudit>[2]
|
||||||
): void {
|
): void {
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { RustfsService } from '../rustfs'
|
|||||||
import { SessionManager } from '../user/session-manager'
|
import { SessionManager } from '../user/session-manager'
|
||||||
import { UserErpConfigService } from '../user/user-erp-config-service'
|
import { UserErpConfigService } from '../user/user-erp-config-service'
|
||||||
import { createLogger } from '../logger'
|
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 { IPC_CHANNELS } from '../../../shared/ipc-channels'
|
||||||
import { DatabaseQueryError, ErpConnectionError, ValidationError } from '../../types/errors'
|
import { DatabaseQueryError, ErpConnectionError, ValidationError } from '../../types/errors'
|
||||||
import type {
|
import type {
|
||||||
@@ -278,27 +279,21 @@ export class CleanerApplicationService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const status: 'success' | 'failure' | 'partial' =
|
const status: AuditStatus =
|
||||||
result.errors.length > 0 && result.materialsDeleted > 0
|
result.errors.length > 0 && result.materialsDeleted > 0
|
||||||
? 'partial'
|
? AuditStatus.PARTIAL
|
||||||
: result.errors.length > 0
|
: result.errors.length > 0
|
||||||
? 'failure'
|
? AuditStatus.FAILURE
|
||||||
: 'success'
|
: AuditStatus.SUCCESS
|
||||||
|
|
||||||
logAudit('CLEAN', String(currentUser.id), {
|
logAuditWithCurrentUser(AuditAction.CLEAN, 'MATERIAL_PLAN', status, {
|
||||||
username: currentUser.username,
|
orderCount,
|
||||||
computerName: (await import('os')).hostname(),
|
dryRun: input.dryRun ?? false,
|
||||||
resource: 'MATERIAL_PLAN',
|
queryBatchSize: input.queryBatchSize ?? 100,
|
||||||
status,
|
processConcurrency: input.processConcurrency ?? 1,
|
||||||
metadata: {
|
materialsDeleted: result.materialsDeleted,
|
||||||
orderCount,
|
materialsSkipped: result.materialsSkipped,
|
||||||
dryRun: input.dryRun ?? false,
|
errorCount: result.errors.length
|
||||||
queryBatchSize: input.queryBatchSize ?? 100,
|
|
||||||
processConcurrency: input.processConcurrency ?? 1,
|
|
||||||
materialsDeleted: result.materialsDeleted,
|
|
||||||
materialsSkipped: result.materialsSkipped,
|
|
||||||
errorCount: result.errors.length
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from '../logger'
|
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'
|
import { DiscreteMaterialPlanDAO, type MaterialPlanRecord } from './discrete-material-plan-dao'
|
||||||
|
|
||||||
const log = createLogger('DataImportService')
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import path from 'path'
|
|||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { createLogger } from '../logger'
|
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'
|
import type { ExportResultItem, ExportResultResponse } from '../../types/cleaner.types'
|
||||||
|
|
||||||
const log = createLogger('ResultExporter')
|
const log = createLogger('ResultExporter')
|
||||||
@@ -37,8 +39,8 @@ export class ResultExporter {
|
|||||||
* @returns Export result with file path or error
|
* @returns Export result with file path or error
|
||||||
*/
|
*/
|
||||||
async exportValidationResults(items: ExportResultItem[]): Promise<ExportResultResponse> {
|
async exportValidationResults(items: ExportResultItem[]): Promise<ExportResultResponse> {
|
||||||
|
const filePath = path.join(this.exportDir, this.fileName)
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(this.exportDir, this.fileName)
|
|
||||||
log.info('Exporting validation results', { count: items.length, path: filePath })
|
log.info('Exporting validation results', { count: items.length, path: filePath })
|
||||||
|
|
||||||
const workbook = new ExcelJS.Workbook()
|
const workbook = new ExcelJS.Workbook()
|
||||||
@@ -97,6 +99,12 @@ export class ResultExporter {
|
|||||||
await workbook.xlsx.writeFile(filePath)
|
await workbook.xlsx.writeFile(filePath)
|
||||||
log.info('Export completed', { path: filePath, rows: items.length })
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
filePath
|
filePath
|
||||||
@@ -104,6 +112,14 @@ export class ResultExporter {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
log.error('Export failed', { error: errorMessage })
|
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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorMessage
|
error: errorMessage
|
||||||
|
|||||||
@@ -6,33 +6,12 @@
|
|||||||
import winston from 'winston'
|
import winston from 'winston'
|
||||||
import DailyRotateFile from 'winston-daily-rotate-file'
|
import DailyRotateFile from 'winston-daily-rotate-file'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { hostname } from 'os'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import { getLogDir } from './shared'
|
import { getLogDir } from './shared'
|
||||||
|
import { SessionManager } from '../user/session-manager'
|
||||||
/**
|
import type { AuditEntry } from '../../types/audit.types'
|
||||||
* Audit log entry structure
|
import { AuditAction, AuditStatus } from '../../types/audit.types'
|
||||||
* 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<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSONL formatter - outputs one JSON object per line
|
* 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
|
* @param details - Additional details including username, computerName, resource, status, and optional metadata
|
||||||
*/
|
*/
|
||||||
export function logAudit(
|
export function logAudit(
|
||||||
action: string,
|
action: AuditAction,
|
||||||
userId: string,
|
userId: string,
|
||||||
details: {
|
details: {
|
||||||
username: string
|
username: string
|
||||||
computerName: string
|
computerName: string
|
||||||
resource: string
|
resource: string
|
||||||
status: 'success' | 'failure' | 'partial'
|
status: AuditStatus
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
): void {
|
): void {
|
||||||
const entry: AuditEntry = {
|
try {
|
||||||
timestamp: new Date().toISOString(),
|
const entry: AuditEntry = {
|
||||||
action,
|
timestamp: new Date().toISOString(),
|
||||||
userId,
|
action,
|
||||||
username: details.username,
|
userId,
|
||||||
computerName: details.computerName,
|
username: details.username,
|
||||||
appVersion: app.getVersion(),
|
computerName: details.computerName,
|
||||||
resource: details.resource,
|
appVersion: app.getVersion(),
|
||||||
status: details.status,
|
resource: details.resource,
|
||||||
metadata: details.metadata || {}
|
status: details.status,
|
||||||
}
|
metadata: details.metadata ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
// Write as JSONL - one JSON object per line
|
// Write as JSONL - one JSON object per line
|
||||||
// Using info level with the entry stringified as the message
|
// Using info level with the entry stringified as the message
|
||||||
auditLogger.info(JSON.stringify(entry))
|
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<string, unknown>
|
||||||
|
): void {
|
||||||
|
const user = SessionManager.getInstance().getUserInfo()
|
||||||
|
logAudit(action, user ? String(user.id) : 'anonymous', {
|
||||||
|
username: user?.username ?? 'anonymous',
|
||||||
|
computerName: cachedHostname,
|
||||||
|
resource,
|
||||||
|
status,
|
||||||
|
metadata: metadata ?? {}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import { ConfigManager } from '../config/config-manager'
|
import { ConfigManager } from '../config/config-manager'
|
||||||
import { createLogger } from '../logger'
|
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 { UpdateConfig } from '../../types/config.schema'
|
||||||
import type { UserType } from '../../types/user.types'
|
import type { UserType } from '../../types/user.types'
|
||||||
import type {
|
import type {
|
||||||
@@ -281,10 +283,21 @@ export class UpdateService {
|
|||||||
log.error('Update package hash mismatch', {
|
log.error('Update package hash mismatch', {
|
||||||
version: request.version,
|
version: request.version,
|
||||||
channel: request.channel,
|
channel: request.channel,
|
||||||
expectedHash: request.sha256,
|
expectedHash: request.sha256.substring(0, 16),
|
||||||
actualHash: hash
|
actualHash: hash.substring(0, 16)
|
||||||
})
|
})
|
||||||
await fs.promises.rm(downloadPath, { force: true })
|
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('更新包校验失败,文件哈希不匹配')
|
throw new Error('更新包校验失败,文件哈希不匹配')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +307,13 @@ export class UpdateService {
|
|||||||
downloadPath
|
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({
|
this.publishStatus({
|
||||||
phase: 'downloaded',
|
phase: 'downloaded',
|
||||||
progress: 100,
|
progress: 100,
|
||||||
@@ -336,11 +356,29 @@ export class UpdateService {
|
|||||||
error: undefined
|
error: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.installer.installDownloadedRelease(downloaded)
|
try {
|
||||||
log.info('Update installation completed', {
|
await this.installer.installDownloadedRelease(downloaded)
|
||||||
version: downloaded.version,
|
log.info('Update installation completed', {
|
||||||
channel: downloaded.channel
|
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 {
|
private ensureInitialized(): void {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
import { BIPUsersDAO } from './bip-users-dao'
|
import { BIPUsersDAO } from './bip-users-dao'
|
||||||
import { SessionManager } from './session-manager'
|
import { SessionManager } from './session-manager'
|
||||||
import { createLogger } from '../logger'
|
import { createLogger } from '../logger'
|
||||||
|
import { logAuditWithCurrentUser } from '../logger/audit-logger'
|
||||||
|
import { AuditAction, AuditStatus } from '../../types/audit.types'
|
||||||
|
|
||||||
const log = createLogger('UserErpConfigService')
|
const log = createLogger('UserErpConfigService')
|
||||||
|
|
||||||
@@ -131,9 +133,22 @@ export class UserErpConfigService {
|
|||||||
log.error('Failed to update ERP credentials', { username: currentUser.username })
|
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
|
return success
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error updating current user ERP credentials', { 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
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,9 +174,22 @@ export class UserErpConfigService {
|
|||||||
log.error('Failed to update ERP credentials', { username })
|
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
|
return success
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error updating user ERP credentials', { 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
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,23 +10,31 @@ export enum AuditAction {
|
|||||||
LOGOUT = 'LOGOUT',
|
LOGOUT = 'LOGOUT',
|
||||||
EXTRACT = 'EXTRACT',
|
EXTRACT = 'EXTRACT',
|
||||||
CLEAN = 'CLEAN',
|
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
|
* Audit status enumeration
|
||||||
|
* Note: Values are lowercase to match JSON logging conventions
|
||||||
*/
|
*/
|
||||||
export enum AuditStatus {
|
export enum AuditStatus {
|
||||||
SUCCESS = 'SUCCESS',
|
SUCCESS = 'success',
|
||||||
FAILURE = 'FAILURE'
|
FAILURE = 'failure',
|
||||||
|
PARTIAL = 'partial'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audit entry interface
|
* Audit entry interface
|
||||||
*/
|
*/
|
||||||
export interface AuditEntry {
|
export interface AuditEntry {
|
||||||
/** Timestamp of the action */
|
/** ISO timestamp of the action */
|
||||||
timestamp: Date
|
timestamp: string
|
||||||
/** Action performed */
|
/** Action performed */
|
||||||
action: AuditAction
|
action: AuditAction
|
||||||
/** User ID who performed the action */
|
/** User ID who performed the action */
|
||||||
@@ -38,9 +46,9 @@ export interface AuditEntry {
|
|||||||
/** Application version when action was performed */
|
/** Application version when action was performed */
|
||||||
appVersion: string
|
appVersion: string
|
||||||
/** Resource affected by the action */
|
/** Resource affected by the action */
|
||||||
resource?: string
|
resource: string
|
||||||
/** Status of the action */
|
/** Status of the action */
|
||||||
status: AuditStatus
|
status: AuditStatus
|
||||||
/** Additional metadata in JSON format */
|
/** Additional metadata */
|
||||||
metadata?: string
|
metadata: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|||||||
8
tests/fixtures/factory.ts
vendored
8
tests/fixtures/factory.ts
vendored
@@ -419,11 +419,11 @@ export class AuditLogFactory {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // Successful login audit
|
* // Successful login audit
|
||||||
* const entry = AuditLogFactory.createAuditLog('LOGIN', 'SUCCESS')
|
* const entry = AuditLogFactory.createAuditLog(AuditAction.LOGIN, AuditStatus.SUCCESS)
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // Failed extract audit
|
* // 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(
|
static createAuditLog(
|
||||||
action: AuditAction = AuditAction.LOGIN,
|
action: AuditAction = AuditAction.LOGIN,
|
||||||
@@ -431,13 +431,15 @@ export class AuditLogFactory {
|
|||||||
overrides?: Partial<AuditEntry>
|
overrides?: Partial<AuditEntry>
|
||||||
): AuditEntry {
|
): AuditEntry {
|
||||||
return {
|
return {
|
||||||
timestamp: new Date(),
|
timestamp: new Date().toISOString(),
|
||||||
action,
|
action,
|
||||||
userId: 'USR-001',
|
userId: 'USR-001',
|
||||||
username: 'test_user',
|
username: 'test_user',
|
||||||
computerName: 'TEST-PC',
|
computerName: 'TEST-PC',
|
||||||
appVersion: '1.0.0',
|
appVersion: '1.0.0',
|
||||||
|
resource: 'test-resource',
|
||||||
status,
|
status,
|
||||||
|
metadata: {},
|
||||||
...overrides
|
...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', () => {
|
it('creates audit log with default values', () => {
|
||||||
const entry = AuditLogFactory.createAuditLog()
|
const entry = AuditLogFactory.createAuditLog()
|
||||||
|
|
||||||
expect(entry.timestamp).toBeInstanceOf(Date)
|
expect(typeof entry.timestamp).toBe('string')
|
||||||
expect(entry.action).toBe(AuditAction.LOGIN)
|
expect(entry.action).toBe(AuditAction.LOGIN)
|
||||||
expect(entry.status).toBe(AuditStatus.SUCCESS)
|
expect(entry.status).toBe(AuditStatus.SUCCESS)
|
||||||
expect(entry.userId).toBe('USR-001')
|
expect(entry.userId).toBe('USR-001')
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { AuditAction, AuditStatus } from '../../src/main/types/audit.types'
|
||||||
|
|
||||||
describe('Audit Logger', () => {
|
describe('Audit Logger', () => {
|
||||||
let auditLoggerModule: typeof import('../../src/main/services/logger/audit-logger')
|
let auditLoggerModule: typeof import('../../src/main/services/logger/audit-logger')
|
||||||
@@ -32,11 +33,11 @@ describe('Audit Logger', () => {
|
|||||||
const { logAudit, applyAuditConfig } = auditLoggerModule
|
const { logAudit, applyAuditConfig } = auditLoggerModule
|
||||||
|
|
||||||
applyAuditConfig(30)
|
applyAuditConfig(30)
|
||||||
logAudit('LOGIN', 'user-001', {
|
logAudit(AuditAction.LOGIN, 'user-001', {
|
||||||
username: 'alice',
|
username: 'alice',
|
||||||
computerName: 'PC-001',
|
computerName: 'PC-001',
|
||||||
resource: 'ERP_SYSTEM',
|
resource: 'ERP_SYSTEM',
|
||||||
status: 'success',
|
status: AuditStatus.SUCCESS,
|
||||||
metadata: { sessionId: 'abc' }
|
metadata: { sessionId: 'abc' }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -60,26 +61,26 @@ describe('Audit Logger', () => {
|
|||||||
|
|
||||||
applyAuditConfig(30)
|
applyAuditConfig(30)
|
||||||
|
|
||||||
logAudit('EXTRACT', 'user1', {
|
logAudit(AuditAction.EXTRACT, 'user1', {
|
||||||
username: 'extractor',
|
username: 'extractor',
|
||||||
computerName: 'PC-001',
|
computerName: 'PC-001',
|
||||||
resource: 'materials',
|
resource: 'materials',
|
||||||
status: 'success'
|
status: AuditStatus.SUCCESS
|
||||||
})
|
})
|
||||||
|
|
||||||
logAudit('DELETE', 'user2', {
|
logAudit(AuditAction.CLEAN, 'user2', {
|
||||||
username: 'cleaner',
|
username: 'cleaner',
|
||||||
computerName: 'PC-002',
|
computerName: 'PC-002',
|
||||||
resource: 'temp_files',
|
resource: 'temp_files',
|
||||||
status: 'failure',
|
status: AuditStatus.FAILURE,
|
||||||
metadata: { error: 'Permission denied' }
|
metadata: { error: 'Permission denied' }
|
||||||
})
|
})
|
||||||
|
|
||||||
logAudit('UPDATE', 'user3', {
|
logAudit(AuditAction.APP_UPDATE, 'user3', {
|
||||||
username: 'updater',
|
username: 'updater',
|
||||||
computerName: 'PC-003',
|
computerName: 'PC-003',
|
||||||
resource: 'config',
|
resource: 'config',
|
||||||
status: 'partial',
|
status: AuditStatus.PARTIAL,
|
||||||
metadata: { updated: 5, failed: 2 }
|
metadata: { updated: 5, failed: 2 }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -95,11 +96,11 @@ describe('Audit Logger', () => {
|
|||||||
|
|
||||||
applyAuditConfig(30)
|
applyAuditConfig(30)
|
||||||
|
|
||||||
logAudit('PING', 'user-no-meta', {
|
logAudit(AuditAction.SYSTEM_ERROR, 'user-no-meta', {
|
||||||
username: 'tester',
|
username: 'tester',
|
||||||
computerName: 'PC-001',
|
computerName: 'PC-001',
|
||||||
resource: 'ERP',
|
resource: 'ERP',
|
||||||
status: 'success'
|
status: AuditStatus.SUCCESS
|
||||||
})
|
})
|
||||||
|
|
||||||
const entry = JSON.parse(infoSpy.mock.calls[0][0])
|
const entry = JSON.parse(infoSpy.mock.calls[0][0])
|
||||||
@@ -111,11 +112,11 @@ describe('Audit Logger', () => {
|
|||||||
|
|
||||||
applyAuditConfig(30)
|
applyAuditConfig(30)
|
||||||
|
|
||||||
logAudit('LOGIN_ATTEMPT', 'user-special', {
|
logAudit(AuditAction.LOGIN, 'user-special', {
|
||||||
username: 'user.name+test@example.com',
|
username: 'user.name+test@example.com',
|
||||||
computerName: 'DESKTOP-特殊字符-001',
|
computerName: 'DESKTOP-特殊字符-001',
|
||||||
resource: 'ERP/子系统',
|
resource: 'ERP/子系统',
|
||||||
status: 'failure',
|
status: AuditStatus.FAILURE,
|
||||||
metadata: { reason: '密码错误', attempt: 3 }
|
metadata: { reason: '密码错误', attempt: 3 }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,10 @@ describe('MySqlService Unit Tests', () => {
|
|||||||
it('should execute SELECT and return rows with columns', async () => {
|
it('should execute SELECT and return rows with columns', async () => {
|
||||||
await service.connect()
|
await service.connect()
|
||||||
mockExecute.mockResolvedValue([
|
mockExecute.mockResolvedValue([
|
||||||
[{ id: 1, name: 'test' }, { id: 2, name: 'foo' }],
|
[
|
||||||
|
{ id: 1, name: 'test' },
|
||||||
|
{ id: 2, name: 'foo' }
|
||||||
|
],
|
||||||
[{ name: 'id' }, { name: 'name' }]
|
[{ name: 'id' }, { name: 'name' }]
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -168,10 +168,7 @@ describe('PostgreSqlService Unit Tests', () => {
|
|||||||
await service.connect()
|
await service.connect()
|
||||||
mockPgClient.query.mockResolvedValue({ rows: [] })
|
mockPgClient.query.mockResolvedValue({ rows: [] })
|
||||||
|
|
||||||
await service.transaction([
|
await service.transaction([{ sql: 'SELECT 1', params: [1] }, { sql: 'SELECT 2' }])
|
||||||
{ sql: 'SELECT 1', params: [1] },
|
|
||||||
{ sql: 'SELECT 2' }
|
|
||||||
])
|
|
||||||
|
|
||||||
// BEGIN + 2 queries + COMMIT
|
// BEGIN + 2 queries + COMMIT
|
||||||
expect(mockPgClient.query).toHaveBeenCalledTimes(4)
|
expect(mockPgClient.query).toHaveBeenCalledTimes(4)
|
||||||
@@ -187,9 +184,9 @@ describe('PostgreSqlService Unit Tests', () => {
|
|||||||
.mockRejectedValueOnce(new Error('constraint violation')) // query fails
|
.mockRejectedValueOnce(new Error('constraint violation')) // query fails
|
||||||
.mockResolvedValueOnce({ rows: [] }) // ROLLBACK
|
.mockResolvedValueOnce({ rows: [] }) // ROLLBACK
|
||||||
|
|
||||||
await expect(
|
await expect(service.transaction([{ sql: 'SELECT 1', params: [1] }])).rejects.toThrow(
|
||||||
service.transaction([{ sql: 'SELECT 1', params: [1] }])
|
'PostgreSQL transaction failed'
|
||||||
).rejects.toThrow('PostgreSQL transaction failed')
|
)
|
||||||
|
|
||||||
expect(mockPgClient.query).toHaveBeenCalledWith('ROLLBACK')
|
expect(mockPgClient.query).toHaveBeenCalledWith('ROLLBACK')
|
||||||
expect(mockPgClient.release).toHaveBeenCalled()
|
expect(mockPgClient.release).toHaveBeenCalled()
|
||||||
@@ -234,8 +231,7 @@ describe('prepareSql', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should quote column names in INSERT', () => {
|
it('should quote column names in INSERT', () => {
|
||||||
const sql =
|
const sql = 'INSERT INTO "dbo"."BIPUsers" (UserName, Password, UserType) VALUES ($1, $2, $3)'
|
||||||
'INSERT INTO "dbo"."BIPUsers" (UserName, Password, UserType) VALUES ($1, $2, $3)'
|
|
||||||
const result = prepareSql(sql)
|
const result = prepareSql(sql)
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
'INSERT INTO "dbo"."BIPUsers" ("UserName", "Password", "UserType") VALUES ($1, $2, $3)'
|
'INSERT INTO "dbo"."BIPUsers" ("UserName", "Password", "UserType") VALUES ($1, $2, $3)'
|
||||||
@@ -303,9 +299,7 @@ describe('prepareSql', () => {
|
|||||||
it('should quote underscore-containing column names', () => {
|
it('should quote underscore-containing column names', () => {
|
||||||
const sql = 'SELECT ERP_URL, ERP_Username, ERP_Password FROM "dbo"."BIPUsers"'
|
const sql = 'SELECT ERP_URL, ERP_Username, ERP_Password FROM "dbo"."BIPUsers"'
|
||||||
const result = prepareSql(sql)
|
const result = prepareSql(sql)
|
||||||
expect(result).toBe(
|
expect(result).toBe('SELECT "ERP_URL", "ERP_Username", "ERP_Password" FROM "dbo"."BIPUsers"')
|
||||||
'SELECT "ERP_URL", "ERP_Username", "ERP_Password" FROM "dbo"."BIPUsers"'
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle ON CONFLICT DO UPDATE SET with EXCLUDED', () => {
|
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 () => {
|
it('should execute SELECT and return rows with columns', async () => {
|
||||||
await service.connect()
|
await service.connect()
|
||||||
mockRequestQuery.mockResolvedValue({
|
mockRequestQuery.mockResolvedValue({
|
||||||
recordset: [{ ID: 1, Name: 'test' }, { ID: 2, Name: 'foo' }],
|
recordset: [
|
||||||
|
{ ID: 1, Name: 'test' },
|
||||||
|
{ ID: 2, Name: 'foo' }
|
||||||
|
],
|
||||||
rowsAffected: [2]
|
rowsAffected: [2]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -209,9 +212,9 @@ describe('SqlServerService Unit Tests', () => {
|
|||||||
|
|
||||||
describe('queryWithParams', () => {
|
describe('queryWithParams', () => {
|
||||||
it('should throw error when not connected', async () => {
|
it('should throw error when not connected', async () => {
|
||||||
await expect(
|
await expect(service.queryWithParams('SELECT @p0', { p0: { value: 1 } })).rejects.toThrow(
|
||||||
service.queryWithParams('SELECT @p0', { p0: { value: 1 } })
|
'Not connected to SQL Server'
|
||||||
).rejects.toThrow('Not connected to SQL Server')
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should add typed params via request.input', async () => {
|
it('should add typed params via request.input', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user