Datasourceforcryptocurrency / tests /test_apiClient.test.js
Really-amin's picture
Upload 1460 files
96af7c9 verified
/**
* Property-Based Tests for API Client
* Feature: admin-ui-modernization, Property 14: Backend API integration
* Validates: Requirements 15.1, 15.2, 15.4
*/
import fc from 'fast-check';
// Mock fetch for testing
class MockFetch {
constructor() {
this.calls = [];
this.mockResponse = null;
}
reset() {
this.calls = [];
this.mockResponse = null;
}
setMockResponse(response) {
this.mockResponse = response;
}
async fetch(url, options) {
this.calls.push({ url, options });
if (this.mockResponse) {
return this.mockResponse;
}
// Default mock response
return {
ok: true,
status: 200,
headers: {
get: (key) => {
if (key === 'content-type') return 'application/json';
return null;
}
},
json: async () => ({ success: true, data: {} })
};
}
}
// Simple ApiClient implementation for testing
class ApiClient {
constructor(baseURL = 'https://test-backend.example.com') {
this.baseURL = baseURL.replace(/\/$/, '');
this.cache = new Map();
this.requestLogs = [];
this.errorLogs = [];
this.fetchImpl = null;
}
setFetchImpl(fetchImpl) {
this.fetchImpl = fetchImpl;
}
buildUrl(endpoint) {
if (!endpoint.startsWith('/')) {
return `${this.baseURL}/${endpoint}`;
}
return `${this.baseURL}${endpoint}`;
}
async request(method, endpoint, { body, cache = true, ttl = 60000 } = {}) {
const url = this.buildUrl(endpoint);
const cacheKey = `${method}:${url}`;
if (method === 'GET' && cache && this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
if (Date.now() - cached.timestamp < ttl) {
return { ok: true, data: cached.data, cached: true };
}
}
const started = Date.now();
const entry = {
id: `${Date.now()}-${Math.random()}`,
method,
endpoint,
status: 'pending',
duration: 0,
time: new Date().toISOString(),
};
try {
const fetchFn = this.fetchImpl || fetch;
const response = await fetchFn(url, {
method,
headers: {
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
const duration = Date.now() - started;
entry.duration = Math.round(duration);
entry.status = response.status;
const contentType = response.headers.get('content-type') || '';
let data = null;
if (contentType.includes('application/json')) {
data = await response.json();
} else if (contentType.includes('text')) {
data = await response.text();
}
if (!response.ok) {
const error = new Error((data && data.message) || response.statusText || 'Unknown error');
error.status = response.status;
throw error;
}
if (method === 'GET' && cache) {
this.cache.set(cacheKey, { timestamp: Date.now(), data });
}
this.requestLogs.push({ ...entry, success: true });
return { ok: true, data };
} catch (error) {
const duration = Date.now() - started;
entry.duration = Math.round(duration);
entry.status = error.status || 'error';
this.requestLogs.push({ ...entry, success: false, error: error.message });
this.errorLogs.push({
message: error.message,
endpoint,
method,
time: new Date().toISOString(),
});
return { ok: false, error: error.message };
}
}
get(endpoint, options) {
return this.request('GET', endpoint, options);
}
post(endpoint, body, options = {}) {
return this.request('POST', endpoint, { ...options, body });
}
}
// Generators for property-based testing
const httpMethodGen = fc.constantFrom('GET', 'POST');
const endpointGen = fc.oneof(
fc.constant('/api/health'),
fc.constant('/api/market'),
fc.constant('/api/coins'),
fc.webPath().map(p => `/api/${p}`)
);
const baseURLGen = fc.webUrl({ withFragments: false, withQueryParameters: false });
/**
* Property 14: Backend API integration
* For any API request made through apiClient, it should:
* 1. Use the configured baseURL
* 2. Return a standardized response format ({ ok, data } or { ok: false, error })
* 3. Log the request for debugging
*/
console.log('Running Property-Based Tests for API Client...\n');
// Property 1: All requests use the configured baseURL
console.log('Property 1: All requests use the configured baseURL');
fc.assert(
fc.asyncProperty(
baseURLGen,
httpMethodGen,
endpointGen,
async (baseURL, method, endpoint) => {
const client = new ApiClient(baseURL);
const mockFetch = new MockFetch();
client.setFetchImpl(mockFetch.fetch.bind(mockFetch));
await client.request(method, endpoint);
// Check that the URL starts with the baseURL
const expectedBase = baseURL.replace(/\/$/, '');
const actualURL = mockFetch.calls[0].url;
return actualURL.startsWith(expectedBase);
}
),
{ numRuns: 100 }
);
console.log('βœ“ Property 1 passed: All requests use the configured baseURL\n');
// Property 2: All successful responses have standardized format { ok: true, data }
console.log('Property 2: All successful responses have standardized format');
fc.assert(
fc.asyncProperty(
httpMethodGen,
endpointGen,
fc.jsonValue(),
async (method, endpoint, responseData) => {
const client = new ApiClient('https://test.example.com');
const mockFetch = new MockFetch();
mockFetch.setMockResponse({
ok: true,
status: 200,
headers: {
get: (key) => key === 'content-type' ? 'application/json' : null
},
json: async () => responseData
});
client.setFetchImpl(mockFetch.fetch.bind(mockFetch));
const result = await client.request(method, endpoint);
// Check standardized response format
return (
typeof result === 'object' &&
result !== null &&
'ok' in result &&
result.ok === true &&
'data' in result
);
}
),
{ numRuns: 100 }
);
console.log('βœ“ Property 2 passed: All successful responses have standardized format\n');
// Property 3: All error responses have standardized format { ok: false, error }
console.log('Property 3: All error responses have standardized format');
fc.assert(
fc.asyncProperty(
httpMethodGen,
endpointGen,
fc.integer({ min: 400, max: 599 }),
fc.string({ minLength: 1, maxLength: 100 }),
async (method, endpoint, statusCode, errorMessage) => {
const client = new ApiClient('https://test.example.com');
const mockFetch = new MockFetch();
mockFetch.setMockResponse({
ok: false,
status: statusCode,
statusText: errorMessage,
headers: {
get: (key) => key === 'content-type' ? 'application/json' : null
},
json: async () => ({ message: errorMessage })
});
client.setFetchImpl(mockFetch.fetch.bind(mockFetch));
const result = await client.request(method, endpoint);
// Check standardized error response format
return (
typeof result === 'object' &&
result !== null &&
'ok' in result &&
result.ok === false &&
'error' in result &&
typeof result.error === 'string'
);
}
),
{ numRuns: 100 }
);
console.log('βœ“ Property 3 passed: All error responses have standardized format\n');
// Property 4: All requests are logged for debugging
console.log('Property 4: All requests are logged for debugging');
fc.assert(
fc.asyncProperty(
httpMethodGen,
endpointGen,
async (method, endpoint) => {
const client = new ApiClient('https://test.example.com');
const mockFetch = new MockFetch();
client.setFetchImpl(mockFetch.fetch.bind(mockFetch));
const initialLogCount = client.requestLogs.length;
await client.request(method, endpoint);
const finalLogCount = client.requestLogs.length;
// Check that a log entry was added
if (finalLogCount !== initialLogCount + 1) {
return false;
}
// Check that the log entry has required fields
const logEntry = client.requestLogs[client.requestLogs.length - 1];
return (
typeof logEntry === 'object' &&
logEntry !== null &&
'method' in logEntry &&
'endpoint' in logEntry &&
'status' in logEntry &&
'duration' in logEntry &&
'time' in logEntry &&
'success' in logEntry
);
}
),
{ numRuns: 100 }
);
console.log('βœ“ Property 4 passed: All requests are logged for debugging\n');
// Property 5: Error requests are logged in errorLogs
console.log('Property 5: Error requests are logged in errorLogs');
fc.assert(
fc.asyncProperty(
httpMethodGen,
endpointGen,
fc.integer({ min: 400, max: 599 }),
async (method, endpoint, statusCode) => {
const client = new ApiClient('https://test.example.com');
const mockFetch = new MockFetch();
mockFetch.setMockResponse({
ok: false,
status: statusCode,
statusText: 'Error',
headers: {
get: () => 'application/json'
},
json: async () => ({ message: 'Test error' })
});
client.setFetchImpl(mockFetch.fetch.bind(mockFetch));
const initialErrorCount = client.errorLogs.length;
await client.request(method, endpoint);
const finalErrorCount = client.errorLogs.length;
// Check that an error log entry was added
if (finalErrorCount !== initialErrorCount + 1) {
return false;
}
// Check that the error log entry has required fields
const errorEntry = client.errorLogs[client.errorLogs.length - 1];
return (
typeof errorEntry === 'object' &&
errorEntry !== null &&
'message' in errorEntry &&
'endpoint' in errorEntry &&
'method' in errorEntry &&
'time' in errorEntry
);
}
),
{ numRuns: 100 }
);
console.log('βœ“ Property 5 passed: Error requests are logged in errorLogs\n');
console.log('All property-based tests passed! βœ“');