import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { VergeApiClient } from '../../src/client/api';
import { PlumeApiError, PlumeErrorType } from '../../src/client/errors';
describe('VergeApiClient エクスポネンシャルバックオフ', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
describe('デフォルト設定 (ジッターなし)', () => {
it('attempt 0: 200ms', () => {
let callCount = 0;
const fetchMock = vi.fn().mockImplementation(async () => {
callCount++;
if (callCount < 2) {
return {
ok: false,
status: 500,
json: async () => ({ error: 'Server error' }),
};
}
return {
ok: true,
status: 200,
json: async () => ({
token: 'success-token',
user: {
id: 1,
email: 'test@example.com',
username: 'test',
name: 'Test User',
role: 'admin' as const,
password_change_required: false,
created_at: '2025-01-01T00:00:00Z',
},
}),
};
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
jitter: false, // ジッターなし
},
});
const loginPromise = client.login('test@example.com', 'password');
// 1回目のリトライ: 200ms
vi.advanceTimersByTime(200);
loginPromise.then(() => {
expect(fetchMock).toHaveBeenCalledTimes(2);
});
});
it('attempt 1: 400ms (2^1 * 200)', async () => {
let callCount = 0;
const fetchMock = vi.fn().mockImplementation(async () => {
callCount++;
if (callCount < 3) {
return {
ok: false,
status: 500,
json: async () => ({ error: 'Server error' }),
};
}
return {
ok: true,
status: 200,
json: async () => ({
token: 'success-token',
user: {
id: 1,
email: 'test@example.com',
username: 'test',
name: 'Test User',
role: 'admin' as const,
password_change_required: false,
created_at: '2025-01-01T00:00:00Z',
},
}),
};
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
jitter: false,
},
});
const loginPromise = client.login('test@example.com', 'password');
// 1回目: 200ms
await vi.advanceTimersByTimeAsync(200);
// 2回目: 400ms
await vi.advanceTimersByTimeAsync(400);
const result = await loginPromise;
expect(result.token).toBe('success-token');
expect(fetchMock).toHaveBeenCalledTimes(3);
});
it('attempt 2: 800ms (2^2 * 200)', async () => {
let callCount = 0;
const fetchMock = vi.fn().mockImplementation(async () => {
callCount++;
if (callCount < 4) {
return {
ok: false,
status: 500,
json: async () => ({ error: 'Server error' }),
};
}
return {
ok: true,
status: 200,
json: async () => ({
token: 'success-token',
user: {
id: 1,
email: 'test@example.com',
username: 'test',
name: 'Test User',
role: 'admin' as const,
password_change_required: false,
created_at: '2025-01-01T00:00:00Z',
},
}),
};
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
jitter: false,
},
});
const loginPromise = client.login('test@example.com', 'password');
// 1回目: 200ms
await vi.advanceTimersByTimeAsync(200);
// 2回目: 400ms
await vi.advanceTimersByTimeAsync(400);
// 3回目: 800ms
await vi.advanceTimersByTimeAsync(800);
const result = await loginPromise;
expect(result.token).toBe('success-token');
expect(fetchMock).toHaveBeenCalledTimes(4);
});
});
describe('カスタムバックオフ倍率', () => {
it('backoffMultiplier=3 で指数的増加 (200, 600, 1800)', async () => {
let callCount = 0;
const fetchMock = vi.fn().mockImplementation(async () => {
callCount++;
if (callCount < 4) {
return {
ok: false,
status: 500,
json: async () => ({ error: 'Server error' }),
};
}
return {
ok: true,
status: 200,
json: async () => ({
token: 'success-token',
user: {
id: 1,
email: 'test@example.com',
username: 'test',
name: 'Test User',
role: 'admin' as const,
password_change_required: false,
created_at: '2025-01-01T00:00:00Z',
},
}),
};
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
backoffMultiplier: 3,
jitter: false,
},
});
const loginPromise = client.login('test@example.com', 'password');
// 1回目: 200ms
await vi.advanceTimersByTimeAsync(200);
// 2回目: 600ms (3^1 * 200)
await vi.advanceTimersByTimeAsync(600);
// 3回目: 1800ms (3^2 * 200)
await vi.advanceTimersByTimeAsync(1800);
const result = await loginPromise;
expect(result.token).toBe('success-token');
expect(fetchMock).toHaveBeenCalledTimes(4);
});
});
describe('最大遅延時間', () => {
it('maxRetryDelay でキャップされる', async () => {
let callCount = 0;
const fetchMock = vi.fn().mockImplementation(async () => {
callCount++;
if (callCount < 4) {
return {
ok: false,
status: 500,
json: async () => ({ error: 'Server error' }),
};
}
return {
ok: true,
status: 200,
json: async () => ({
token: 'success-token',
user: {
id: 1,
email: 'test@example.com',
username: 'test',
name: 'Test User',
role: 'admin' as const,
password_change_required: false,
created_at: '2025-01-01T00:00:00Z',
},
}),
};
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
retryDelay: 100,
backoffMultiplier: 10, // 大きい倍率
maxRetryDelay: 500, // 最大500ms
jitter: false,
},
});
const loginPromise = client.login('test@example.com', 'password');
// 1回目: 100ms
await vi.advanceTimersByTimeAsync(100);
// 2回目: min(1000, 500) = 500ms
await vi.advanceTimersByTimeAsync(500);
// 3回目: min(10000, 500) = 500ms
await vi.advanceTimersByTimeAsync(500);
const result = await loginPromise;
expect(result.token).toBe('success-token');
expect(fetchMock).toHaveBeenCalledTimes(4);
});
});
describe('ジッター', () => {
it('ジッター有効時、遅延時間が50%〜100%の範囲内', () => {
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
retry: {
retryDelay: 1000,
backoffMultiplier: 1, // 倍率なし
jitter: true,
},
});
// プライベートメソッドにアクセスするために any にキャスト
const clientAny = client as any;
// ジッター有効時、1000ms * 0.5〜1.0 の範囲 (500〜1000ms)
for (let i = 0; i < 100; i++) {
const delay = clientAny.calculateRetryDelay(0);
expect(delay).toBeGreaterThanOrEqual(500);
expect(delay).toBeLessThanOrEqual(1000);
}
});
it('ジッター無効時、固定値', () => {
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
retry: {
retryDelay: 1000,
backoffMultiplier: 1,
jitter: false,
},
});
const clientAny = client as any;
// ジッター無効時、常に1000ms
for (let i = 0; i < 10; i++) {
const delay = clientAny.calculateRetryDelay(0);
expect(delay).toBe(1000);
}
});
});
});