storyblok-js-client
Version:
Universal JavaScript SDK for Storyblok's API
264 lines (226 loc) • 9.2 kB
text/typescript
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { ISbFetch } from './sbFetch';
import SbFetch from './sbFetch';
import { headersToObject } from '../tests/utils';
describe('sbFetch', () => {
let sbFetch: SbFetch;
const mockFetch = vi.fn();
afterEach(() => {
vi.restoreAllMocks();
});
it('should initialize', () => {
sbFetch = new SbFetch({} as ISbFetch);
expect(sbFetch).toBeInstanceOf(SbFetch);
});
describe('get', () => {
it('should correctly construct URLs for GET requests', async () => {
sbFetch = new SbFetch({
baseURL: 'https://api.storyblok.com/v2/',
fetch: mockFetch,
} as ISbFetch);
const response = new Response(JSON.stringify({ data: 'test' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
mockFetch.mockResolvedValue(response);
await sbFetch.get('test', {
is_startpage: false,
search_term: 'test',
});
expect(mockFetch).toHaveBeenCalledWith(
'https://api.storyblok.com/v2/test?is_startpage=false&search_term=test',
expect.anything(),
);
});
});
describe('post', () => {
it('should handle POST requests correctly', async () => {
const testPayload = { title: 'New Story' };
const response = new Response(JSON.stringify({ data: 'test' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
mockFetch.mockResolvedValue(response);
await sbFetch.post('stories', testPayload);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.storyblok.com/v2/stories',
{
method: 'post',
body: JSON.stringify(testPayload),
headers: expect.any(Headers),
signal: expect.any(AbortSignal),
},
);
});
it('should set specific headers for POST requests', async () => {
sbFetch = new SbFetch({
baseURL: 'https://api.storyblok.com/v2/',
headers: new Headers({
'Content-Type': 'application/json',
}),
fetch: mockFetch,
} as ISbFetch);
const testPayload = { title: 'New Story' };
const response = new Response(JSON.stringify({ data: 'test' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
mockFetch.mockResolvedValue(response);
await sbFetch.post('stories', testPayload);
// Get the last call to fetch and extract the headers
const lastCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1];
const actualHeaders = headersToObject(lastCall[1].headers);
expect(actualHeaders['content-type']).toBe('application/json');
});
});
describe('put', () => {
it('should handle PUT requests correctly', async () => {
const testPayload = { title: 'Updated Story' };
const response = new Response(JSON.stringify({ data: 'test' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
mockFetch.mockResolvedValue(response);
await sbFetch.put('stories/1', testPayload);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.storyblok.com/v2/stories/1',
{
method: 'put',
body: JSON.stringify(testPayload),
headers: expect.any(Headers),
signal: expect.any(AbortSignal),
},
);
});
});
describe('delete', () => {
it('should handle DELETE requests correctly', async () => {
const response = new Response(null, {
status: 204, // Typically, DELETE operations might not return content
});
mockFetch.mockResolvedValue(response);
await sbFetch.delete('stories/1');
expect(mockFetch).toHaveBeenCalledWith(
'https://api.storyblok.com/v2/stories/1',
{
method: 'delete',
body: '{}', // Ensuring no body is sent
headers: expect.any(Headers),
signal: expect.any(AbortSignal),
},
);
});
});
it('should handle network errors gracefully', async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error('Network Failure'));
const sbFetch = new SbFetch({
baseURL: 'https://api.example.com',
headers: new Headers(),
fetch: mockFetch,
});
// Assuming your implementation wraps the error message inside an object under `message`.
const result = await sbFetch.get('/test', {});
// Check if the error object format matches your implementation.
expect(result).toEqual({
message: expect.any(String), // Checks if `message` is a string
});
// If you want to be more specific and check the message of the error:
expect(result.message).toEqual('Network Failure'); // This path needs to match the structure you actually use.
});
describe('timeout behavior', () => {
// Helper to create mock fetch with configurable delay
const createMockFetch = (delayMs: number) => {
return vi.fn((_url: string, options?: any): Promise<Response> => {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
resolve(new Response(JSON.stringify({ data: 'test' }), { status: 200 }));
}, delayMs);
options?.signal?.addEventListener('abort', () => {
clearTimeout(timeoutId);
const error = new Error('The operation was aborted');
error.name = 'AbortError';
reject(error);
});
});
});
};
// Helper to create SbFetch instance with timeout
const createSbFetchWithTimeout = (timeoutSeconds: number, mockFetch: any) => {
return new SbFetch({
baseURL: 'https://api.storyblok.com/v2',
timeout: timeoutSeconds,
headers: new Headers(),
fetch: mockFetch as any,
});
};
it('should timeout after configured timeout period', async () => {
const mockFetch = createMockFetch(5000);
const sbFetch = createSbFetchWithTimeout(1, mockFetch);
const result = await sbFetch.get('cdn/stories', {});
expect(result).toEqual({
message: 'Request timeout: The request was aborted due to timeout',
});
}, 3000);
it('should timeout after 2 seconds when configured with 2s timeout', async () => {
vi.useFakeTimers();
const mockFetch = createMockFetch(5000);
const sbFetch = createSbFetchWithTimeout(2, mockFetch);
const requestPromise = sbFetch.get('cdn/stories', {});
// After 1.9 seconds, request should still be pending
await vi.advanceTimersByTimeAsync(1900);
expect(mockFetch).toHaveBeenCalledTimes(1);
// After 2+ seconds, should timeout
await vi.advanceTimersByTimeAsync(200);
const result = await requestPromise;
expect(result).toEqual({
message: 'Request timeout: The request was aborted due to timeout',
});
vi.useRealTimers();
}, 3000);
it('should timeout after 0.5 seconds when configured with 0.5s timeout', async () => {
vi.useFakeTimers();
const mockFetch = createMockFetch(5000);
const sbFetch = createSbFetchWithTimeout(0.5, mockFetch);
const requestPromise = sbFetch.get('cdn/stories', {});
// After 0.5+ seconds, should timeout
await vi.advanceTimersByTimeAsync(600);
const result = await requestPromise;
expect(result).toEqual({
message: 'Request timeout: The request was aborted due to timeout',
});
vi.useRealTimers();
}, 3000);
it('should complete successfully if response arrives before timeout', async () => {
const mockFetch = createMockFetch(500); // 500ms delay
const sbFetch = createSbFetchWithTimeout(2, mockFetch);
const result = await sbFetch.get('cdn/stories', {});
expect(result).toHaveProperty('data');
expect(result).toHaveProperty('status', 200);
}, 3000);
it('should not timeout when timeout is set to 0 (disabled)', async () => {
const mockFetch = vi.fn().mockResolvedValue(
new Response(JSON.stringify({ data: 'test' }), { status: 200 }),
);
const sbFetch = createSbFetchWithTimeout(0, mockFetch);
const result = await sbFetch.get('cdn/stories', {});
expect(result).toHaveProperty('data');
expect(result).toHaveProperty('status', 200);
}, 3000);
it('should return clear error message for timeout', async () => {
const mockFetch = vi.fn((_url: string, options?: any): Promise<Response> => {
return new Promise((_resolve, reject) => {
options?.signal?.addEventListener('abort', () => {
const error = new Error('The operation was aborted');
error.name = 'AbortError';
reject(error);
});
// Immediately abort to test the error message
setTimeout(() => options?.signal?.dispatchEvent(new Event('abort')), 10);
});
});
const sbFetch = createSbFetchWithTimeout(1, mockFetch);
const result = await sbFetch.get('cdn/stories', {});
expect((result as any).message).toBe('Request timeout: The request was aborted due to timeout');
});
});
});