feat: переработать кастомизацию HttpClient
- добавлен плоский ApiConfig с lifecycle hooks - добавлены ApiError, retry context, timeout и кастомные parser/serializer - обновлены примеры, документация и тесты под новый API
This commit is contained in:
@@ -339,17 +339,23 @@ describe('E2E Generation', () => {
|
||||
const { login } = await import(join(outputPath, 'operations', 'login.ts'));
|
||||
const { get } = await import(join(outputPath, 'operations', 'get.ts'));
|
||||
|
||||
// Создаем HttpClient с securityWorker для добавления Bearer токена
|
||||
let token: string | null = null;
|
||||
|
||||
// Создаем HttpClient с onRequest для добавления Bearer токена
|
||||
const httpClient = new HttpClient({
|
||||
securityWorker: (securityData: string | null) => {
|
||||
if (securityData) {
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${securityData}`
|
||||
}
|
||||
};
|
||||
onRequest: (params) => {
|
||||
if (!params.secure || !token) {
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...params,
|
||||
headers: {
|
||||
...params.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const api = createApiClient(httpClient, {
|
||||
@@ -368,12 +374,100 @@ describe('E2E Generation', () => {
|
||||
});
|
||||
|
||||
// Установка токена
|
||||
httpClient.setSecurityData(loginResult.token);
|
||||
token = loginResult.token;
|
||||
|
||||
// Запрос с токеном
|
||||
const profile = await api.profile.get();
|
||||
|
||||
|
||||
expect(profile.email).toBe('test@example.com');
|
||||
}, 60000);
|
||||
|
||||
test('onError должен поддерживать retry после ApiError', async () => {
|
||||
const outputPath = join(tempDir, 'output');
|
||||
|
||||
await execa('bun', [
|
||||
'run',
|
||||
CLI_PATH,
|
||||
'--input',
|
||||
FIXTURES.MINIMAL,
|
||||
'--output',
|
||||
outputPath,
|
||||
'--mode',
|
||||
'split',
|
||||
]);
|
||||
|
||||
const generatedFile = join(outputPath, 'index.ts');
|
||||
const { ApiError, HttpClient } = await import(generatedFile);
|
||||
let calls = 0;
|
||||
|
||||
const http = new HttpClient({
|
||||
customFetch: async () => {
|
||||
calls += 1;
|
||||
|
||||
if (calls === 1) {
|
||||
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
return Response.json({ ok: true });
|
||||
},
|
||||
onError: (error, context) => {
|
||||
if (error instanceof ApiError && error.status === 401 && context.retryCount === 0) {
|
||||
return context.retry();
|
||||
}
|
||||
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
|
||||
const result = await http.request({
|
||||
path: '/retry',
|
||||
method: 'GET',
|
||||
format: 'json',
|
||||
});
|
||||
|
||||
expect(calls).toBe(2);
|
||||
expect(result).toEqual({ ok: true });
|
||||
}, 60000);
|
||||
|
||||
test('timeout должен отменять зависший запрос', async () => {
|
||||
const outputPath = join(tempDir, 'output');
|
||||
|
||||
await execa('bun', [
|
||||
'run',
|
||||
CLI_PATH,
|
||||
'--input',
|
||||
FIXTURES.MINIMAL,
|
||||
'--output',
|
||||
outputPath,
|
||||
'--mode',
|
||||
'split',
|
||||
]);
|
||||
|
||||
const generatedFile = join(outputPath, 'index.ts');
|
||||
const { HttpClient } = await import(generatedFile);
|
||||
|
||||
const http = new HttpClient({
|
||||
customFetch: (_url, init) => new Promise((resolve, reject) => {
|
||||
init?.signal?.addEventListener('abort', () => {
|
||||
reject(new DOMException('The operation was aborted.', 'AbortError'));
|
||||
});
|
||||
}),
|
||||
onError: (error) => {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
return { aborted: true };
|
||||
}
|
||||
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
|
||||
const result = await http.request({
|
||||
path: '/timeout',
|
||||
method: 'GET',
|
||||
timeout: 1,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ aborted: true });
|
||||
}, 60000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('Generated Client', () => {
|
||||
expect(content).toContain('https://api.example.com');
|
||||
}, 30000);
|
||||
|
||||
test('метод setSecurityData должен работать', async () => {
|
||||
test('HttpClient должен поддерживать lifecycle hooks', async () => {
|
||||
const outputPath = join(tempDir, 'output');
|
||||
const config: GeneratorConfig = {
|
||||
inputPath: FIXTURES.WITH_AUTH,
|
||||
@@ -162,8 +162,12 @@ describe('Generated Client', () => {
|
||||
const generatedFile = join(outputPath, 'http-client.ts');
|
||||
const content = await readTextFile(generatedFile);
|
||||
|
||||
// Проверяем наличие метода для установки токена
|
||||
expect(content).toContain('setSecurityData');
|
||||
// Проверяем наличие hooks для кастомизации запросов
|
||||
expect(content).toContain('onRequest?: RequestInterceptor');
|
||||
expect(content).toContain('onResponse?: ResponseInterceptor');
|
||||
expect(content).toContain('onError?: ErrorInterceptor');
|
||||
expect(content).not.toContain('baseApiParams');
|
||||
expect(content).not.toContain('setSecurityData');
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user