mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
Added http service to tools
This commit is contained in:
120
tools/http-service/__tests__/http.test.ts
Normal file
120
tools/http-service/__tests__/http.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { describe, expect, test, jest, beforeEach } from '@jest/globals';
|
||||
import { HttpService } from '../index';
|
||||
import { HttpRequestConfig } from '../types/http';
|
||||
|
||||
// Setup fetch mock
|
||||
const mockFetch = jest.fn() as jest.MockedFunction<typeof fetch>;
|
||||
global.fetch = mockFetch;
|
||||
|
||||
describe('HttpService', () => {
|
||||
let service: HttpService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
service = HttpService.getInstance();
|
||||
});
|
||||
|
||||
test('should make successful GET request', async () => {
|
||||
const mockResponse = { message: 'Success' };
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: () => Promise.resolve(mockResponse)
|
||||
} as Response);
|
||||
|
||||
const response = await service.get('https://api.example.com/data');
|
||||
expect(response.data).toEqual(mockResponse);
|
||||
expect(response.status).toBe(200);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data',
|
||||
expect.objectContaining({
|
||||
method: 'GET'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should make successful POST request with JSON body', async () => {
|
||||
const requestBody = { key: 'value' };
|
||||
const mockResponse = { id: 1 };
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 201,
|
||||
statusText: 'Created',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: () => Promise.resolve(mockResponse)
|
||||
} as Response);
|
||||
|
||||
const response = await service.post('https://api.example.com/data', requestBody);
|
||||
expect(response.data).toEqual(mockResponse);
|
||||
expect(response.status).toBe(201);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle request with authentication', async () => {
|
||||
const mockResponse = { message: 'Authenticated' };
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: () => Promise.resolve(mockResponse)
|
||||
} as Response);
|
||||
|
||||
const config: Omit<HttpRequestConfig, 'url' | 'method'> = {
|
||||
auth: {
|
||||
type: 'bearer',
|
||||
token: 'test-token'
|
||||
}
|
||||
};
|
||||
|
||||
const response = await service.get('https://api.example.com/protected', config);
|
||||
expect(response.data).toEqual(mockResponse);
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/protected',
|
||||
expect.objectContaining({
|
||||
headers: expect.any(Headers)
|
||||
})
|
||||
);
|
||||
|
||||
const headers = mockFetch.mock.calls[0][1]?.headers as Headers;
|
||||
expect(headers.get('Authorization')).toBe('Bearer test-token');
|
||||
});
|
||||
|
||||
test('should handle request timeout', async () => {
|
||||
mockFetch.mockImplementationOnce(() =>
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('The operation was aborted')), 50);
|
||||
})
|
||||
);
|
||||
|
||||
await expect(
|
||||
service.get('https://api.example.com/data', { timeout: 10 })
|
||||
).rejects.toThrow('The operation was aborted');
|
||||
});
|
||||
|
||||
test('should handle API errors', async () => {
|
||||
const errorResponse = { error: 'Not Found' };
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: new Headers({ 'content-type': 'application/json' }),
|
||||
json: () => Promise.resolve(errorResponse)
|
||||
} as Response);
|
||||
|
||||
const promise = service.get('https://api.example.com/nonexistent');
|
||||
await expect(promise).rejects.toThrow('Not Found');
|
||||
await expect(promise).rejects.toMatchObject({
|
||||
status: 404,
|
||||
data: errorResponse
|
||||
});
|
||||
});
|
||||
});
|
||||
108
tools/http-service/index.ts
Normal file
108
tools/http-service/index.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { HttpRequestConfig, HttpResponse, HttpError } from './types/http';
|
||||
|
||||
export class HttpService {
|
||||
private static instance: HttpService;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): HttpService {
|
||||
if (!HttpService.instance) {
|
||||
HttpService.instance = new HttpService();
|
||||
}
|
||||
return HttpService.instance;
|
||||
}
|
||||
|
||||
private getHeaders(config: HttpRequestConfig): Headers {
|
||||
const headers = new Headers(config.headers);
|
||||
|
||||
if (!headers.has('Content-Type') && config.body) {
|
||||
headers.set('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
if (config.auth) {
|
||||
switch (config.auth.type) {
|
||||
case 'bearer':
|
||||
headers.set('Authorization', `Bearer ${config.auth.token}`);
|
||||
break;
|
||||
case 'basic':
|
||||
const credentials = btoa(`${config.auth.username}:${config.auth.password}`);
|
||||
headers.set('Authorization', `Basic ${credentials}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private async handleResponse<T>(response: Response): Promise<HttpResponse<T>> {
|
||||
const headers: Record<string, string> = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = new Error(response.statusText) as HttpError;
|
||||
error.status = response.status;
|
||||
error.statusText = response.statusText;
|
||||
try {
|
||||
error.data = await response.json();
|
||||
} catch {
|
||||
error.data = await response.text();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let data: T;
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
data = await response.json();
|
||||
} else {
|
||||
data = await response.text() as T;
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers
|
||||
};
|
||||
}
|
||||
|
||||
public async request<T = any>(config: HttpRequestConfig): Promise<HttpResponse<T>> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = config.timeout ? setTimeout(() => controller.abort(), config.timeout) : null;
|
||||
|
||||
try {
|
||||
const response = await fetch(config.url, {
|
||||
method: config.method,
|
||||
headers: this.getHeaders(config),
|
||||
body: config.body ? JSON.stringify(config.body) : undefined,
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
return await this.handleResponse<T>(response);
|
||||
} finally {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
public async get<T = any>(url: string, config: Omit<HttpRequestConfig, 'url' | 'method'> = {}): Promise<HttpResponse<T>> {
|
||||
return this.request<T>({ ...config, url, method: 'GET' });
|
||||
}
|
||||
|
||||
public async post<T = any>(url: string, data?: any, config: Omit<HttpRequestConfig, 'url' | 'method' | 'body'> = {}): Promise<HttpResponse<T>> {
|
||||
return this.request<T>({ ...config, url, method: 'POST', body: data });
|
||||
}
|
||||
|
||||
public async put<T = any>(url: string, data?: any, config: Omit<HttpRequestConfig, 'url' | 'method' | 'body'> = {}): Promise<HttpResponse<T>> {
|
||||
return this.request<T>({ ...config, url, method: 'PUT', body: data });
|
||||
}
|
||||
|
||||
public async delete<T = any>(url: string, config: Omit<HttpRequestConfig, 'url' | 'method'> = {}): Promise<HttpResponse<T>> {
|
||||
return this.request<T>({ ...config, url, method: 'DELETE' });
|
||||
}
|
||||
|
||||
public async patch<T = any>(url: string, data?: any, config: Omit<HttpRequestConfig, 'url' | 'method' | 'body'> = {}): Promise<HttpResponse<T>> {
|
||||
return this.request<T>({ ...config, url, method: 'PATCH', body: data });
|
||||
}
|
||||
}
|
||||
26
tools/http-service/types/http.ts
Normal file
26
tools/http-service/types/http.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export interface HttpRequestConfig {
|
||||
url: string;
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
timeout?: number;
|
||||
auth?: {
|
||||
type: 'basic' | 'bearer';
|
||||
token?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HttpResponse<T = any> {
|
||||
data: T;
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
statusText: string;
|
||||
}
|
||||
|
||||
export interface HttpError extends Error {
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
data?: any;
|
||||
}
|
||||
Reference in New Issue
Block a user