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('500エラー時に3回までリトライする', async () => {
let callCount = 0;
const fetchMock = vi.fn().mockImplementation(async () => {
callCount++;
if (callCount < 4) {
// 最初の3回は500エラー
return {
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: async () => ({ error: 'Server error' }),
};
}
// 4回目は成功
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');
// リトライ間隔を進める (エクスポネンシャルバックオフ: 200, 400, 800)
await vi.advanceTimersByTimeAsync(200);
await vi.advanceTimersByTimeAsync(400);
await vi.advanceTimersByTimeAsync(800);
const result = await loginPromise;
// 4回呼ばれる (初回 + 3回リトライ)
expect(fetchMock).toHaveBeenCalledTimes(4);
expect(result.token).toBe('success-token');
});
it('429 Too Many Requests時にリトライする', async () => {
let callCount = 0;
const fetchMock = vi.fn().mockImplementation(async () => {
callCount++;
if (callCount < 2) {
return {
ok: false,
status: 429,
statusText: 'Too Many Requests',
json: async () => ({ error: 'Rate limit exceeded' }),
};
}
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');
await vi.advanceTimersByTimeAsync(200);
const result = await loginPromise;
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(result.token).toBe('success-token');
});
it('最大リトライ回数を超えたらエラーを投げる', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: async () => ({ error: 'Server error' }),
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
jitter: false, // ジッターを無効化して決定的なテストにする
},
});
const loginPromise = client.login('test@example.com', 'password').catch((err) => err);
// リトライを進める (エクスポネンシャルバックオフ: 200, 400, 800)
await vi.advanceTimersByTimeAsync(200 + 400 + 800);
const error = await loginPromise;
expect(error).toBeInstanceOf(PlumeApiError);
expect(error.message).toBe('Server error');
expect(error.type).toBe(PlumeErrorType.API);
expect(error.statusCode).toBe(500);
expect(fetchMock).toHaveBeenCalledTimes(4); // 初回 + 3回リトライ
});
});
describe('リトライ対象外のステータス', () => {
it('400エラーはリトライしない', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 400,
statusText: 'Bad Request',
json: async () => ({ error: 'Invalid request' }),
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
jitter: false, // ジッターを無効化して決定的なテストにする
},
});
await expect(client.login('test@example.com', 'password')).rejects.toThrow('Invalid request');
expect(fetchMock).toHaveBeenCalledTimes(1); // リトライなし
});
it('401エラーはリトライしない', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 401,
statusText: 'Unauthorized',
json: async () => ({ error: 'Unauthorized' }),
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
jitter: false, // ジッターを無効化して決定的なテストにする
},
});
await expect(client.login('test@example.com', 'password')).rejects.toThrow('Unauthorized');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('404エラーはリトライしない', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
json: async () => ({ error: 'Not found' }),
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
jitter: false, // ジッターを無効化して決定的なテストにする
},
});
const token = 'test-token';
client.setToken(token);
await expect(client.getArticle(1, 999)).rejects.toThrow('Not found');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
});
describe('カスタムリトライ設定', () => {
it('maxRetries=0でリトライを無効化できる', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: async () => ({ error: 'Server error' }),
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
maxRetries: 0,
},
});
await expect(client.login('test@example.com', 'password')).rejects.toThrow('Server error');
expect(fetchMock).toHaveBeenCalledTimes(1); // リトライなし
});
it('カスタムretryDelayを設定できる', async () => {
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: {
retryDelay: 1000, // 1秒
},
});
const loginPromise = client.login('test@example.com', 'password');
await vi.advanceTimersByTimeAsync(1000);
await loginPromise;
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('カスタムretryOn配列を設定できる', async () => {
let callCount = 0;
const fetchMock = vi.fn().mockImplementation(async () => {
callCount++;
if (callCount < 2) {
return {
ok: false,
status: 503,
json: async () => ({ error: 'Service Unavailable' }),
};
}
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: {
retryOn: [503], // 503のみリトライ
},
});
const loginPromise = client.login('test@example.com', 'password');
await vi.advanceTimersByTimeAsync(200);
const result = await loginPromise;
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(result.token).toBe('success-token');
});
it('retryOnに含まれないステータスはリトライしない', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
status: 500,
json: async () => ({ error: 'Server error' }),
});
const client = new VergeApiClient({
baseUrl: 'https://api.example.com',
fetchFn: fetchMock,
retry: {
retryOn: [503], // 503のみリトライ (500は含まない)
},
});
await expect(client.login('test@example.com', 'password')).rejects.toThrow('Server error');
expect(fetchMock).toHaveBeenCalledTimes(1); // リトライなし
});
});
describe('ネットワークエラー時のリトライ', () => {
it('ネットワークエラー時にリトライする', async () => {
let callCount = 0;
const fetchMock = vi.fn().mockImplementation(async () => {
callCount++;
if (callCount < 3) {
throw new Error('Network 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');
await vi.advanceTimersByTimeAsync(200 * 3);
const result = await loginPromise;
expect(fetchMock).toHaveBeenCalledTimes(3);
expect(result.token).toBe('success-token');
});
});
});