Advertisement

← vibesdk

sdk/test/http.test.ts

3.6 KB Download file
import { describe, expect, it } from 'bun:test';
import { HttpClient } from '../src/http';
import type { VibeClientOptions } from '../src/types';

import { createFetchMock } from './fakes';

describe('HttpClient', () => {
	it('retries on 5xx errors and succeeds', async () => {
		let attempts = 0;
		const { fetchFn } = createFetchMock(async () => {
			attempts += 1;
			if (attempts < 3) {
				return new Response('Internal Server Error', { status: 500 });
			}
			return new Response(JSON.stringify({ result: 'ok' }), {
				status: 200,
				headers: { 'Content-Type': 'application/json' },
			});
		});

		const opts: VibeClientOptions = {
			baseUrl: 'http://localhost:5173',
			fetchFn,
			retry: { initialDelayMs: 1, maxDelayMs: 10 },
		};
		const http = new HttpClient(opts);
		const result = await http.fetchJson<{ result: string }>('/test');

		expect(result).toEqual({ result: 'ok' });
		expect(attempts).toBe(3);
	});

	it('throws after max retries on persistent 5xx errors', async () => {
		let attempts = 0;
		const { fetchFn } = createFetchMock(async () => {
			attempts += 1;
			return new Response('Service Unavailable', { status: 503 });
		});

		const opts: VibeClientOptions = {
			baseUrl: 'http://localhost:5173',
			fetchFn,
			retry: { maxRetries: 2, initialDelayMs: 1, maxDelayMs: 10 },
		};
		const http = new HttpClient(opts);

		await expect(http.fetchJson('/test')).rejects.toThrow(/HTTP 503/);
		expect(attempts).toBe(3);
	});

	it('does not retry on 4xx errors', async () => {
		let attempts = 0;
		const { fetchFn } = createFetchMock(async () => {
			attempts += 1;
			return new Response('Not Found', { status: 404 });
		});

		const opts: VibeClientOptions = {
			baseUrl: 'http://localhost:5173',
			fetchFn,
			retry: { maxRetries: 3, initialDelayMs: 1 },
		};
		const http = new HttpClient(opts);

		await expect(http.fetchJson('/test')).rejects.toThrow(/HTTP 404/);
		expect(attempts).toBe(1);
	});

	it('respects retry.enabled = false', async () => {
		let attempts = 0;
		const { fetchFn } = createFetchMock(async () => {
			attempts += 1;
			return new Response('Internal Server Error', { status: 500 });
		});

		const opts: VibeClientOptions = {
			baseUrl: 'http://localhost:5173',
			fetchFn,
			retry: { enabled: false },
		};
		const http = new HttpClient(opts);

		await expect(http.fetchJson('/test')).rejects.toThrow(/HTTP 500/);
		expect(attempts).toBe(1);
	});

	it('exchanges apiKey for access token and caches it', async () => {
		let exchangeCalls = 0;
		const { fetchFn, calls } = createFetchMock(async ({ url }) => {
			if (url.endsWith('/api/auth/exchange-api-key')) {
				exchangeCalls += 1;
				return new Response(
					JSON.stringify({
						success: true,
						data: {
							accessToken: 'ACCESS_TOKEN',
							expiresIn: 900,
							expiresAt: new Date(Date.now() + 900_000).toISOString(),
							apiKeyId: 'k_123',
							user: { id: 'u_1' },
						},
					}),
					{ status: 200, headers: { 'Content-Type': 'application/json' } },
				);
			}
			return new Response('not found', { status: 404 });
		});

		const opts: VibeClientOptions = {
			baseUrl: 'http://localhost:5173',
			apiKey: 'API_KEY',
			fetchFn,
		};
		const http = new HttpClient(opts);

		const h1 = await http.headers();
		expect(h1.get('Authorization')).toBe('Bearer ACCESS_TOKEN');

		const h2 = await http.headers({ 'X-Test': '1' });
		expect(h2.get('Authorization')).toBe('Bearer ACCESS_TOKEN');
		expect(h2.get('X-Test')).toBe('1');

		expect(exchangeCalls).toBe(1);
		expect(calls.some((c) => c.url.endsWith('/api/auth/exchange-api-key'))).toBe(true);
	});
});