test: add comprehensive unit tests for utils and security
- Add security.test.ts: 36 tests for webhook validation, rate limiting - Add circuit-breaker.test.ts: 31 tests for state transitions - Add retry.test.ts: 25 tests for exponential backoff - Add api-helper.test.ts: 25 tests for API abstraction - Add optimistic-lock.test.ts: 11 tests for concurrency control - Add summary-service.test.ts: 29 tests for profile system Total: 157 new test cases (222 passing overall) - Fix setup.ts D1 schema initialization for Miniflare - Update vitest.config.ts to exclude demo files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
753
tests/circuit-breaker.test.ts
Normal file
753
tests/circuit-breaker.test.ts
Normal file
@@ -0,0 +1,753 @@
|
||||
/**
|
||||
* Comprehensive Unit Tests for circuit-breaker.ts
|
||||
*
|
||||
* 테스트 범위:
|
||||
* 1. 초기 상태 검증
|
||||
* 2. CLOSED → OPEN 전환 (failureThreshold)
|
||||
* 3. OPEN → HALF_OPEN 전환 (resetTimeout)
|
||||
* 4. HALF_OPEN → CLOSED 전환 (성공 시)
|
||||
* 5. HALF_OPEN → OPEN 전환 (실패 시)
|
||||
* 6. 메서드 동작 검증
|
||||
* 7. Monitoring window 기반 failure cleanup
|
||||
* 8. 통계 및 메트릭 검증
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import {
|
||||
CircuitBreaker,
|
||||
CircuitState,
|
||||
CircuitBreakerError,
|
||||
CircuitBreakerOptions,
|
||||
} from '../src/utils/circuit-breaker';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../src/utils/metrics', () => ({
|
||||
metrics: {
|
||||
record: vi.fn(),
|
||||
increment: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../src/services/notification', () => ({
|
||||
notifyAdmin: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('../src/utils/logger', () => ({
|
||||
createLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('CircuitBreaker', () => {
|
||||
let breaker: CircuitBreaker;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('should start in CLOSED state', () => {
|
||||
breaker = new CircuitBreaker();
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
});
|
||||
|
||||
it('should allow execution in CLOSED state', async () => {
|
||||
breaker = new CircuitBreaker();
|
||||
|
||||
const mockFn = vi.fn().mockResolvedValue('success');
|
||||
const result = await breaker.execute(mockFn);
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should initialize with default options', () => {
|
||||
breaker = new CircuitBreaker();
|
||||
|
||||
const stats = breaker.getStats();
|
||||
expect(stats.config).toEqual({
|
||||
failureThreshold: 5,
|
||||
resetTimeoutMs: 60000,
|
||||
monitoringWindowMs: 120000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialize with custom options', () => {
|
||||
breaker = new CircuitBreaker({
|
||||
failureThreshold: 3,
|
||||
resetTimeoutMs: 30000,
|
||||
monitoringWindowMs: 60000,
|
||||
serviceName: 'test-service',
|
||||
});
|
||||
|
||||
const stats = breaker.getStats();
|
||||
expect(stats.config).toEqual({
|
||||
failureThreshold: 3,
|
||||
resetTimeoutMs: 30000,
|
||||
monitoringWindowMs: 60000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CLOSED → OPEN Transition', () => {
|
||||
beforeEach(() => {
|
||||
breaker = new CircuitBreaker({
|
||||
failureThreshold: 3,
|
||||
serviceName: 'test-service',
|
||||
});
|
||||
});
|
||||
|
||||
it('should open circuit after threshold failures', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// 3번 실패
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected failure
|
||||
}
|
||||
}
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.OPEN);
|
||||
expect(mockFn).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should block requests when circuit is OPEN', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Trigger opening
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.OPEN);
|
||||
|
||||
// Next request should be blocked
|
||||
await expect(
|
||||
breaker.execute(mockFn)
|
||||
).rejects.toThrow(CircuitBreakerError);
|
||||
|
||||
// mockFn should not be called (circuit is open)
|
||||
expect(mockFn).toHaveBeenCalledTimes(3); // Only from previous calls
|
||||
});
|
||||
|
||||
it('should throw CircuitBreakerError with correct state', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Open the circuit
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
expect.fail('Should have thrown CircuitBreakerError');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(CircuitBreakerError);
|
||||
expect((error as CircuitBreakerError).state).toBe(CircuitState.OPEN);
|
||||
expect((error as CircuitBreakerError).message).toContain('Circuit breaker is open');
|
||||
}
|
||||
});
|
||||
|
||||
it('should record failures within threshold without opening', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// 2번 실패 (threshold는 3)
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
|
||||
const stats = breaker.getStats();
|
||||
expect(stats.failures).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OPEN → HALF_OPEN Transition', () => {
|
||||
beforeEach(() => {
|
||||
breaker = new CircuitBreaker({
|
||||
failureThreshold: 2,
|
||||
resetTimeoutMs: 10000, // 10초
|
||||
serviceName: 'test-service',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transition to HALF_OPEN after resetTimeout', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.OPEN);
|
||||
|
||||
// Advance time by resetTimeout
|
||||
vi.advanceTimersByTime(10000);
|
||||
|
||||
// Next execute() should check reset timeout
|
||||
mockFn.mockResolvedValueOnce('success');
|
||||
await breaker.execute(mockFn);
|
||||
|
||||
// Should have transitioned to HALF_OPEN, then CLOSED after success
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
});
|
||||
|
||||
it('should allow one test request in HALF_OPEN state', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for reset timeout
|
||||
vi.advanceTimersByTime(10000);
|
||||
|
||||
// Test request should succeed
|
||||
mockFn.mockResolvedValueOnce('success');
|
||||
const result = await breaker.execute(mockFn);
|
||||
|
||||
expect(result).toBe('success');
|
||||
});
|
||||
|
||||
it('should not transition before resetTimeout elapsed', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.OPEN);
|
||||
|
||||
// Advance time but not enough
|
||||
vi.advanceTimersByTime(5000); // Only 5 seconds
|
||||
|
||||
// Should still be OPEN
|
||||
await expect(
|
||||
breaker.execute(mockFn)
|
||||
).rejects.toThrow(CircuitBreakerError);
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.OPEN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HALF_OPEN → CLOSED Transition', () => {
|
||||
beforeEach(() => {
|
||||
breaker = new CircuitBreaker({
|
||||
failureThreshold: 2,
|
||||
resetTimeoutMs: 10000,
|
||||
serviceName: 'test-service',
|
||||
});
|
||||
});
|
||||
|
||||
it('should close circuit on successful test request', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
// Wait and succeed
|
||||
vi.advanceTimersByTime(10000);
|
||||
mockFn.mockResolvedValueOnce('success');
|
||||
|
||||
await breaker.execute(mockFn);
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
});
|
||||
|
||||
it('should clear failures after closing', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
expect(breaker.getStats().failures).toBe(2);
|
||||
|
||||
// Wait and succeed
|
||||
vi.advanceTimersByTime(10000);
|
||||
mockFn.mockResolvedValueOnce('success');
|
||||
|
||||
await breaker.execute(mockFn);
|
||||
|
||||
// Failures should be cleared
|
||||
expect(breaker.getStats().failures).toBe(0);
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HALF_OPEN → OPEN Transition', () => {
|
||||
beforeEach(() => {
|
||||
breaker = new CircuitBreaker({
|
||||
failureThreshold: 2,
|
||||
resetTimeoutMs: 10000,
|
||||
serviceName: 'test-service',
|
||||
});
|
||||
});
|
||||
|
||||
it('should reopen circuit on failed test request', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for reset timeout
|
||||
vi.advanceTimersByTime(10000);
|
||||
|
||||
// Test request fails
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
// Should be back to OPEN
|
||||
expect(breaker.getState()).toBe(CircuitState.OPEN);
|
||||
});
|
||||
|
||||
it('should block subsequent requests after reopening', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
// Wait and fail test
|
||||
vi.advanceTimersByTime(10000);
|
||||
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
// Next request should be blocked
|
||||
await expect(
|
||||
breaker.execute(mockFn)
|
||||
).rejects.toThrow(CircuitBreakerError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset() Method', () => {
|
||||
it('should manually reset circuit to CLOSED', async () => {
|
||||
breaker = new CircuitBreaker({ failureThreshold: 2 });
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.OPEN);
|
||||
|
||||
// Manual reset
|
||||
breaker.reset();
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
});
|
||||
|
||||
it('should clear all failures and stats on reset', async () => {
|
||||
breaker = new CircuitBreaker({ failureThreshold: 2 });
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Record some failures
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
const statsBefore = breaker.getStats();
|
||||
expect(statsBefore.failures).toBe(2);
|
||||
expect(statsBefore.stats.totalFailures).toBe(2);
|
||||
|
||||
breaker.reset();
|
||||
|
||||
const statsAfter = breaker.getStats();
|
||||
expect(statsAfter.failures).toBe(0);
|
||||
expect(statsAfter.stats.totalFailures).toBe(0);
|
||||
expect(statsAfter.stats.totalSuccesses).toBe(0);
|
||||
});
|
||||
|
||||
it('should allow execution after reset', async () => {
|
||||
breaker = new CircuitBreaker({ failureThreshold: 2 });
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
breaker.reset();
|
||||
|
||||
// Should allow execution
|
||||
mockFn.mockResolvedValueOnce('success');
|
||||
const result = await breaker.execute(mockFn);
|
||||
|
||||
expect(result).toBe('success');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Monitoring Window Cleanup', () => {
|
||||
it('should clean up old failures outside monitoring window', async () => {
|
||||
breaker = new CircuitBreaker({
|
||||
failureThreshold: 5,
|
||||
monitoringWindowMs: 10000, // 10초
|
||||
});
|
||||
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Record 2 failures
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
expect(breaker.getStats().failures).toBe(2);
|
||||
|
||||
// Advance time beyond monitoring window
|
||||
vi.advanceTimersByTime(11000);
|
||||
|
||||
// Record another failure (triggers cleanup)
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
// Old failures should be cleaned up
|
||||
const stats = breaker.getStats();
|
||||
expect(stats.failures).toBe(1); // Only the latest failure
|
||||
});
|
||||
|
||||
it('should not open circuit if old failures are cleaned up', async () => {
|
||||
breaker = new CircuitBreaker({
|
||||
failureThreshold: 3,
|
||||
monitoringWindowMs: 10000,
|
||||
});
|
||||
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
// Record 2 failures
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
// Advance time to clean up failures
|
||||
vi.advanceTimersByTime(11000);
|
||||
|
||||
// Record one more failure (total recent: 1)
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
// Should still be CLOSED (only 1 recent failure)
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Statistics and Metrics', () => {
|
||||
beforeEach(() => {
|
||||
breaker = new CircuitBreaker({
|
||||
failureThreshold: 3,
|
||||
serviceName: 'test-service',
|
||||
});
|
||||
});
|
||||
|
||||
it('should track success count', async () => {
|
||||
const mockFn = vi.fn().mockResolvedValue('success');
|
||||
|
||||
await breaker.execute(mockFn);
|
||||
await breaker.execute(mockFn);
|
||||
|
||||
const stats = breaker.getStats();
|
||||
expect(stats.stats.totalSuccesses).toBe(2);
|
||||
expect(stats.stats.totalRequests).toBe(2);
|
||||
});
|
||||
|
||||
it('should track failure count', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
const stats = breaker.getStats();
|
||||
expect(stats.stats.totalFailures).toBe(2);
|
||||
expect(stats.failures).toBe(2);
|
||||
});
|
||||
|
||||
it('should track last failure time', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
const beforeTime = Date.now();
|
||||
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
const afterTime = Date.now();
|
||||
|
||||
const stats = breaker.getStats();
|
||||
expect(stats.lastFailureTime).toBeDefined();
|
||||
|
||||
const failureTime = stats.lastFailureTime!.getTime();
|
||||
expect(failureTime).toBeGreaterThanOrEqual(beforeTime);
|
||||
expect(failureTime).toBeLessThanOrEqual(afterTime);
|
||||
});
|
||||
|
||||
it('should return correct state in stats', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Service error'));
|
||||
|
||||
const initialStats = breaker.getStats();
|
||||
expect(initialStats.state).toBe(CircuitState.CLOSED);
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
const openStats = breaker.getStats();
|
||||
expect(openStats.state).toBe(CircuitState.OPEN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle synchronous errors', async () => {
|
||||
breaker = new CircuitBreaker({ failureThreshold: 2 });
|
||||
|
||||
const mockFn = vi.fn().mockImplementation(() => {
|
||||
throw new Error('Sync error');
|
||||
});
|
||||
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
}
|
||||
|
||||
expect(breaker.getStats().failures).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle non-Error throws', async () => {
|
||||
breaker = new CircuitBreaker({ failureThreshold: 2 });
|
||||
|
||||
const mockFn = vi.fn().mockRejectedValue('string error');
|
||||
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
}
|
||||
|
||||
expect(breaker.getStats().failures).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle multiple state transitions', async () => {
|
||||
breaker = new CircuitBreaker({
|
||||
failureThreshold: 2,
|
||||
resetTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
const mockFn = vi.fn();
|
||||
|
||||
// CLOSED → OPEN
|
||||
mockFn.mockRejectedValue(new Error('Error'));
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
expect(breaker.getState()).toBe(CircuitState.OPEN);
|
||||
|
||||
// OPEN → HALF_OPEN → CLOSED
|
||||
vi.advanceTimersByTime(5000);
|
||||
mockFn.mockResolvedValueOnce('success');
|
||||
await breaker.execute(mockFn);
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
|
||||
// CLOSED → OPEN again
|
||||
mockFn.mockRejectedValue(new Error('Error'));
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
expect(breaker.getState()).toBe(CircuitState.OPEN);
|
||||
});
|
||||
|
||||
it('should handle rapid successive calls', async () => {
|
||||
breaker = new CircuitBreaker({ failureThreshold: 5 });
|
||||
|
||||
const mockFn = vi.fn().mockResolvedValue('success');
|
||||
|
||||
// Execute 10 times rapidly
|
||||
const promises = Array.from({ length: 10 }, () =>
|
||||
breaker.execute(mockFn)
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
expect(mockFn).toHaveBeenCalledTimes(10);
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
|
||||
const stats = breaker.getStats();
|
||||
expect(stats.stats.totalSuccesses).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration Options', () => {
|
||||
it('should respect custom failureThreshold', async () => {
|
||||
breaker = new CircuitBreaker({ failureThreshold: 10 });
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Error'));
|
||||
|
||||
// 9 failures should not open circuit
|
||||
for (let i = 0; i < 9; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
|
||||
// 10th failure should open it
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.OPEN);
|
||||
});
|
||||
|
||||
it('should respect custom resetTimeoutMs', async () => {
|
||||
breaker = new CircuitBreaker({
|
||||
failureThreshold: 2,
|
||||
resetTimeoutMs: 20000, // 20초
|
||||
});
|
||||
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Error'));
|
||||
|
||||
// Open circuit
|
||||
for (let i = 0; i < 2; i++) {
|
||||
try {
|
||||
await breaker.execute(mockFn);
|
||||
} catch (error) {
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
// 10초는 부족
|
||||
vi.advanceTimersByTime(10000);
|
||||
await expect(breaker.execute(mockFn)).rejects.toThrow(CircuitBreakerError);
|
||||
|
||||
// 20초면 충분
|
||||
vi.advanceTimersByTime(10000);
|
||||
mockFn.mockResolvedValueOnce('success');
|
||||
await breaker.execute(mockFn);
|
||||
|
||||
expect(breaker.getState()).toBe(CircuitState.CLOSED);
|
||||
});
|
||||
|
||||
it('should use custom serviceName in stats', () => {
|
||||
breaker = new CircuitBreaker({ serviceName: 'custom-service' });
|
||||
|
||||
// serviceName은 내부적으로만 사용되므로 logger mock으로 확인
|
||||
// 실제로는 getStats()에 serviceName이 포함되지 않지만,
|
||||
// 초기화 시 logger.info에 전달되었는지 확인 가능
|
||||
expect(breaker).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user