feat(logging-p0): complete Wave 2 - Auth/Extractor/Cleaner services transformed

This commit is contained in:
Misaka
2026-04-04 10:39:06 +08:00
parent 24d9bfebaf
commit 78a3066904
14 changed files with 3183 additions and 416 deletions

View File

@@ -0,0 +1,432 @@
/**
* RequestContext Unit Tests
*
* Tests for AsyncLocalStorage-based request context management:
* - Context propagation across async/await
* - Concurrent request isolation
* - Nested contexts
* - Non-request scenarios
* - userId and operation fields
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import {
run,
getRequestId,
getContext,
withContext,
LoggerContext
} from '../../src/main/services/logger/request-context'
describe('RequestContext', () => {
beforeEach(() => {
// Clear any existing context before each test
})
afterEach(() => {
// Context is automatically cleaned up when async scope exits
})
describe('run()', () => {
it('should create context with auto-generated requestId', async () => {
let capturedRequestId: string | undefined
await run(async () => {
capturedRequestId = getRequestId()
})
expect(capturedRequestId).toBeDefined()
expect(typeof capturedRequestId).toBe('string')
// UUID v4 format check (basic)
expect(capturedRequestId).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
)
})
it('should include userId when provided', async () => {
let context: LoggerContext | undefined
await run(
async () => {
context = getContext()
},
{ userId: 'user-123' }
)
expect(context).toBeDefined()
expect(context?.userId).toBe('user-123')
expect(context?.requestId).toBeDefined()
})
it('should include operation when provided', async () => {
let context: LoggerContext | undefined
await run(
async () => {
context = getContext()
},
{ operation: 'extract' }
)
expect(context).toBeDefined()
expect(context?.operation).toBe('extract')
expect(context?.requestId).toBeDefined()
})
it('should include both userId and operation when provided', async () => {
let context: LoggerContext | undefined
await run(
async () => {
context = getContext()
},
{ userId: 'user-456', operation: 'clean' }
)
expect(context).toBeDefined()
expect(context?.userId).toBe('user-456')
expect(context?.operation).toBe('clean')
expect(context?.requestId).toBeDefined()
})
it('should work without optional context', async () => {
let context: LoggerContext | undefined
await run(async () => {
context = getContext()
})
expect(context).toBeDefined()
expect(context?.requestId).toBeDefined()
expect(context?.userId).toBeUndefined()
expect(context?.operation).toBeUndefined()
})
})
describe('Context Propagation', () => {
it('should propagate context across async/await', async () => {
const requestIds: (string | undefined)[] = []
async function nestedOperation() {
requestIds.push(getRequestId())
await Promise.resolve() // Simulate async operation
requestIds.push(getRequestId())
}
await run(async () => {
requestIds.push(getRequestId())
await nestedOperation()
requestIds.push(getRequestId())
})
// All should have the same requestId
expect(requestIds).toHaveLength(4)
expect(new Set(requestIds).size).toBe(1)
expect(requestIds[0]).toBeDefined()
})
it('should propagate context through Promise.all', async () => {
const requestIds: (string | undefined)[] = []
await run(async () => {
const outerId = getRequestId()
requestIds.push(outerId)
await Promise.all([
(async () => {
requestIds.push(getRequestId())
await Promise.resolve()
requestIds.push(getRequestId())
})(),
(async () => {
requestIds.push(getRequestId())
await Promise.resolve()
requestIds.push(getRequestId())
})()
])
requestIds.push(getRequestId())
})
// All should have the same requestId (6 total: 1 outer + 2 from each parallel + 1 final)
expect(requestIds).toHaveLength(6)
expect(new Set(requestIds).size).toBe(1)
})
})
describe('Concurrent Request Isolation', () => {
it('should maintain separate contexts for concurrent requests', async () => {
const request1Ids: (string | undefined)[] = []
const request2Ids: (string | undefined)[] = []
const promise1 = run(
async () => {
request1Ids.push(getRequestId())
await new Promise((resolve) => setTimeout(resolve, 10))
request1Ids.push(getRequestId())
},
{ userId: 'user-1', operation: 'extract' }
)
const promise2 = run(
async () => {
request2Ids.push(getRequestId())
await new Promise((resolve) => setTimeout(resolve, 5))
request2Ids.push(getRequestId())
},
{ userId: 'user-2', operation: 'clean' }
)
await Promise.all([promise1, promise2])
// Each request should have consistent internal context
expect(request1Ids).toHaveLength(2)
expect(request2Ids).toHaveLength(2)
// promise1 should be consistent
expect(request1Ids[0]).toBe(request1Ids[1])
// promise2 should be consistent
expect(request2Ids[0]).toBe(request2Ids[1])
// Different requests should have different requestIds
expect(request1Ids[0]).not.toBe(request2Ids[0])
})
it('should not leak context between sequential requests', async () => {
let firstRequestId: string | undefined
let secondRequestId: string | undefined
// First request
await run(
async () => {
firstRequestId = getRequestId()
},
{ userId: 'first-user' }
)
// Second request (should have new context)
await run(
async () => {
secondRequestId = getRequestId()
},
{ userId: 'second-user' }
)
expect(firstRequestId).toBeDefined()
expect(secondRequestId).toBeDefined()
expect(firstRequestId).not.toBe(secondRequestId)
// Outside any context, should be undefined
expect(getRequestId()).toBeUndefined()
})
})
describe('Nested Contexts', () => {
it('should support nesting without affecting outer context', async () => {
const requestIds: { outer: string | undefined; inner: string | undefined } = {
outer: undefined,
inner: undefined
}
await run(
async () => {
requestIds.outer = getRequestId()
await run(async () => {
requestIds.inner = getRequestId()
})
// Outer context should be unchanged after nesting
expect(getRequestId()).toBe(requestIds.outer)
},
{ operation: 'outer' }
)
// Both should be defined but different
expect(requestIds.outer).toBeDefined()
expect(requestIds.inner).toBeDefined()
expect(requestIds.outer).not.toBe(requestIds.inner)
})
it('should restore outer context after nested context exits', async () => {
const capturedIds: (string | undefined)[] = []
await run(async () => {
capturedIds.push(getRequestId())
await run(async () => {
capturedIds.push(getRequestId())
})
capturedIds.push(getRequestId())
})
expect(capturedIds).toHaveLength(3)
expect(capturedIds[0]).toBe(capturedIds[2]) // Before and after should match
expect(capturedIds[0]).not.toBe(capturedIds[1]) // Inner should be different
})
})
describe('Non-Request Scenarios', () => {
it('should return undefined requestId outside context', async () => {
const requestId = getRequestId()
expect(requestId).toBeUndefined()
})
it('should return undefined context outside context', async () => {
const context = getContext()
expect(context).toBeUndefined()
})
it('should work normally without RequestContext wrapper', async () => {
// This simulates existing code that doesn't use request context
expect(getRequestId()).toBeUndefined()
const result = await Promise.resolve('test')
expect(result).toBe('test')
// Still undefined after regular async operation
expect(getRequestId()).toBeUndefined()
})
})
describe('withContext()', () => {
it('should override operation in nested context', async () => {
const operations: (string | undefined)[] = []
await run(
async () => {
operations.push(getContext()?.operation)
await withContext(
async () => {
operations.push(getContext()?.operation)
},
{ operation: 'inner-operation' }
)
operations.push(getContext()?.operation)
},
{ operation: 'outer-operation' }
)
expect(operations).toHaveLength(3)
expect(operations[0]).toBe('outer-operation')
expect(operations[1]).toBe('inner-operation')
expect(operations[2]).toBe('outer-operation')
})
it('should override userId in nested context', async () => {
const userIds: (string | undefined)[] = []
await run(
async () => {
userIds.push(getContext()?.userId)
await withContext(
async () => {
userIds.push(getContext()?.userId)
},
{ userId: 'inner-user' }
)
userIds.push(getContext()?.userId)
},
{ userId: 'outer-user' }
)
expect(userIds).toHaveLength(3)
expect(userIds[0]).toBe('outer-user')
expect(userIds[1]).toBe('inner-user')
expect(userIds[2]).toBe('outer-user')
})
it('should create new requestId if no outer context exists', async () => {
let requestIdInWith: string | undefined
await withContext(
async () => {
requestIdInWith = getRequestId()
},
{ operation: 'standalone' }
)
expect(requestIdInWith).toBeDefined()
expect(getContext()).toBeUndefined() // Back to undefined after exiting
})
it('should preserve requestId when overriding other fields', async () => {
const requestIds: (string | undefined)[] = []
await run(
async () => {
requestIds.push(getRequestId())
await withContext(
async () => {
requestIds.push(getRequestId())
},
{ operation: 'new-operation' }
)
requestIds.push(getRequestId())
},
{ userId: 'test-user' }
)
// requestId should remain the same across all scopes
expect(requestIds).toHaveLength(3)
expect(new Set(requestIds).size).toBe(1)
})
})
describe('Edge Cases', () => {
it('should handle errors within context gracefully', async () => {
let caughtRequestId: string | undefined
try {
await run(async () => {
throw new Error('Test error')
})
} catch (error) {
// Error caught, context should be cleaned up
caughtRequestId = getRequestId()
}
expect(caughtRequestId).toBeUndefined()
})
it('should handle context in try-catch-finally', async () => {
const tryId: string | undefined = undefined
const finallyId: string | undefined = undefined
await run(async () => {
try {
const id = getRequestId()
expect(id).toBeDefined()
throw new Error('Test')
} catch {
const id = getRequestId()
expect(id).toBeDefined()
} finally {
const id = getRequestId()
expect(id).toBeDefined()
}
})
})
it('should handle empty string userId and operation', async () => {
let context: LoggerContext | undefined
await run(
async () => {
context = getContext()
},
{ userId: '', operation: '' }
)
expect(context?.userId).toBe('')
expect(context?.operation).toBe('')
expect(context?.requestId).toBeDefined()
})
})
})