@spoolcms/nextjs
Version:
The beautiful headless CMS for Next.js developers
262 lines (261 loc) • 12.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const content_1 = require("../utils/content");
// Mock fetch globally
const mockFetch = global.fetch;
// Mock environment to simulate development mode for error logging
jest.mock('../utils/environment', () => ({
detectEnvironment: () => ({
isServer: true,
isClient: false,
isDevelopment: true,
isProduction: false,
isReactStrictMode: false,
}),
getEnvironmentCacheKey: () => 'server-dev',
}));
describe('SpoolCMS Integration Tests', () => {
const mockConfig = {
apiKey: 'test-api-key',
siteId: 'test-site-id',
baseUrl: 'https://test.spoolcms.com',
};
beforeEach(() => {
jest.clearAllMocks();
// Mock console.error
jest.spyOn(console, 'error').mockImplementation(() => { });
});
afterEach(() => {
// Restore console.error
jest.restoreAllMocks();
});
describe('Real-world error scenarios', () => {
it('should handle rate limiting gracefully', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 429,
statusText: 'Too Many Requests',
});
const result = await (0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig });
expect(result).toEqual([]);
expect(console.error).toHaveBeenCalled();
});
it('should handle server errors gracefully', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
});
const result = await (0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig });
expect(result).toEqual([]);
expect(console.error).toHaveBeenCalled();
});
it('should handle authentication errors gracefully', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: 'Unauthorized',
});
const result = await (0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig });
expect(result).toEqual([]);
expect(console.error).toHaveBeenCalledWith('SpoolCMS API error: Authentication failed: Invalid API key or insufficient permissions');
});
it('should handle malformed JSON responses', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => {
throw new SyntaxError('Unexpected token in JSON');
},
});
const result = await (0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig });
expect(result).toEqual([]);
expect(console.error).toHaveBeenCalled();
});
it('should handle network timeouts', async () => {
mockFetch.mockRejectedValueOnce(new Error('Request timeout'));
const result = await (0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig });
expect(result).toEqual([]);
expect(console.error).toHaveBeenCalled();
});
it('should handle DNS resolution failures', async () => {
mockFetch.mockRejectedValueOnce(new Error('getaddrinfo ENOTFOUND'));
const result = await (0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig });
expect(result).toEqual([]);
expect(console.error).toHaveBeenCalled();
});
});
describe('Concurrent request handling', () => {
it('should handle multiple concurrent requests without race conditions', async () => {
// Mock different responses for different endpoints
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => [{ id: '1', title: 'Blog Post' }],
})
.mockResolvedValueOnce({
ok: true,
json: async () => [{ id: '2', title: 'Page' }],
})
.mockResolvedValueOnce({
ok: true,
json: async () => [{ id: '1', name: 'Blog' }, { id: '2', name: 'Pages' }],
});
// Make concurrent requests
const [blogContent, pageContent, collections] = await Promise.all([
(0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig }),
(0, content_1.getSpoolContent)({ collection: 'pages', config: mockConfig }),
(0, content_1.getSpoolCollections)(mockConfig),
]);
expect(blogContent).toEqual([{ id: '1', title: 'Blog Post' }]);
expect(pageContent).toEqual([{ id: '2', title: 'Page' }]);
expect(collections).toEqual([{ id: '1', name: 'Blog' }, { id: '2', name: 'Pages' }]);
expect(mockFetch).toHaveBeenCalledTimes(3);
});
it('should handle mixed success and error responses in concurrent requests', async () => {
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => [{ id: '1', title: 'Success' }],
})
.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
})
.mockRejectedValueOnce(new Error('Network error'));
const [success, notFound, networkError] = await Promise.all([
(0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig }),
(0, content_1.getSpoolContent)({ collection: 'nonexistent', config: mockConfig }),
(0, content_1.getSpoolContent)({ collection: 'error', config: mockConfig }),
]);
expect(success).toEqual([{ id: '1', title: 'Success' }]);
expect(notFound).toEqual([]);
expect(networkError).toEqual([]);
expect(console.error.mock.calls.length).toBeGreaterThanOrEqual(1);
});
});
describe('Edge cases and boundary conditions', () => {
it('should handle empty responses correctly', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => [],
});
const result = await (0, content_1.getSpoolContent)({ collection: 'empty-collection', config: mockConfig });
expect(result).toEqual([]);
});
it('should handle null responses correctly', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => null,
});
const result = await (0, content_1.getSpoolContent)({ collection: 'blog', slug: 'nonexistent-slug', config: mockConfig });
expect(result).toBeNull();
});
it('should handle very large responses without memory issues', async () => {
// Simulate a large response
const largeArray = Array.from({ length: 1000 }, (_, i) => ({
id: i.toString(),
title: `Post ${i}`,
content: 'Lorem ipsum '.repeat(100), // Large content
}));
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => largeArray,
});
const result = await (0, content_1.getSpoolContent)({ collection: 'large-collection', config: mockConfig });
expect(result).toHaveLength(1000);
expect(result[0]).toEqual(expect.objectContaining({
id: '0',
title: 'Post 0',
}));
});
it('should handle responses with special characters and unicode', async () => {
const unicodeData = [
{ id: '1', title: 'Post with émojis 🚀', content: 'Content with 中文 and العربية' },
{ id: '2', title: 'Spëcîål chäractërs', content: 'More unicode: ñáéíóú' },
];
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => unicodeData,
});
const result = await (0, content_1.getSpoolContent)({ collection: 'unicode-collection', config: mockConfig });
expect(result).toEqual(unicodeData);
});
});
describe('Configuration edge cases', () => {
it('should handle missing baseUrl gracefully', async () => {
const configWithoutBaseUrl = {
apiKey: 'test-api-key',
siteId: 'test-site-id',
// baseUrl is optional and should default
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => [{ id: '1', title: 'Post' }],
});
const result = await (0, content_1.getSpoolContent)({ collection: 'blog', config: configWithoutBaseUrl });
expect(result).toEqual([{ id: '1', title: 'Post' }]);
// Should use default baseUrl
expect(mockFetch).toHaveBeenCalledWith('https://www.spoolcms.com/api/spool/test-site-id/content/blog?_format=html&status=published', expect.any(Object));
});
it('should handle empty API key gracefully', async () => {
const configWithEmptyKey = {
apiKey: '',
siteId: 'test-site-id',
baseUrl: 'https://test.spoolcms.com',
};
// With stricter config resolution, this should throw at resolve time
await expect((0, content_1.getSpoolContent)({ collection: 'blog', config: configWithEmptyKey }))
.rejects.toThrow('Spool API key not found');
});
});
describe('Marketing site reliability scenarios', () => {
it('should ensure marketing site loads even when CMS is down', async () => {
// Simulate complete CMS failure
mockFetch.mockRejectedValueOnce(new Error('ECONNREFUSED'));
const [blogPosts, collections] = await Promise.all([
(0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig }),
(0, content_1.getSpoolCollections)(mockConfig),
]);
// Site should still load with empty content
expect(blogPosts).toEqual([]);
expect(collections).toEqual([]);
// Errors should be logged but not thrown
expect(console.error).toHaveBeenCalledTimes(2);
});
it('should handle intermittent failures gracefully', async () => {
// First request fails, second succeeds
mockFetch
.mockRejectedValueOnce(new Error('Temporary failure'))
.mockResolvedValueOnce({
ok: true,
json: async () => [{ id: '1', title: 'Post' }],
});
const firstResult = await (0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig });
const secondResult = await (0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig });
expect(firstResult).toEqual([]);
expect(secondResult).toEqual([{ id: '1', title: 'Post' }]);
});
it('should handle partial API failures without breaking the site', async () => {
// Content succeeds, collections fail
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => [{ id: '1', title: 'Post' }],
})
.mockResolvedValueOnce({
ok: false,
status: 503,
statusText: 'Service Unavailable',
});
const [content, collections] = await Promise.all([
(0, content_1.getSpoolContent)({ collection: 'blog', config: mockConfig }),
(0, content_1.getSpoolCollections)(mockConfig),
]);
expect(content).toEqual([{ id: '1', title: 'Post' }]);
expect(collections).toEqual([]);
// Only one error should be logged
expect(console.error).toHaveBeenCalledTimes(1);
});
});
});