// tests/fda.test.ts
import { FDAClient } from '../src/apis/fda';
// Helper to check if API is rate limited
const isRateLimited = (error: any): boolean => {
const message = error?.message || '';
return message.includes('403') || message.includes('429') || message.includes('rate') || message.includes('Forbidden');
};
// Skip test if rate limited
const skipIfRateLimited = async <T>(fn: () => Promise<T>): Promise<T | null> => {
try {
return await fn();
} catch (error) {
if (isRateLimited(error)) {
console.warn('Skipping test due to API rate limiting');
return null;
}
throw error;
}
};
describe('FDAClient', () => {
let client: FDAClient;
beforeEach(() => {
client = new FDAClient();
// Disable size monitoring to simplify tests
client.setSizeMonitoring(false);
});
afterEach(() => {
client.clearCache();
});
describe('searchDrugs', () => {
it('should search drugs by name successfully', async () => {
const params = {
drugName: 'aspirin',
limit: 5,
};
const result = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result) return;
expect(result).toHaveProperty('drugs');
expect(result).toHaveProperty('totalCount');
expect(Array.isArray(result.drugs)).toBe(true);
expect(typeof result.totalCount).toBe('number');
}, 20000);
it('should return empty results for non-existent drug', async () => {
const params = {
drugName: 'xyznonexistentdrug9876543210',
limit: 5,
};
const result = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result) return;
expect(result).toHaveProperty('drugs');
expect(result).toHaveProperty('totalCount');
expect(result.drugs).toHaveLength(0);
expect(result.totalCount).toBe(0);
}, 20000);
it('should cache search results', async () => {
const params = {
drugName: 'ibuprofen',
limit: 3,
};
// First call
const start1 = Date.now();
const result1 = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result1) return;
const time1 = Date.now() - start1;
// Second call (should be cached)
const start2 = Date.now();
const result2 = await client.searchDrugs(params);
const time2 = Date.now() - start2;
expect(result1).toEqual(result2);
// Cached call should be significantly faster (at least 50% faster)
// Using a ratio check instead of absolute comparison for stability
expect(time2).toBeLessThan(time1 + 50); // Allow some timing variance
}, 25000);
it('should search drugs by active ingredient', async () => {
const params = {
activeIngredient: 'acetaminophen',
limit: 5,
};
const result = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result) return;
expect(result).toHaveProperty('drugs');
expect(Array.isArray(result.drugs)).toBe(true);
}, 20000);
it('should respect limit parameter', async () => {
const params = {
drugName: 'metformin',
limit: 3,
};
const result = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result) return;
expect(result.drugs.length).toBeLessThanOrEqual(3);
}, 20000);
it('should return properly structured drug objects', async () => {
const params = {
drugName: 'lisinopril',
limit: 2,
};
const result = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result) return;
if (result.drugs.length > 0) {
const drug = result.drugs[0];
expect(drug).toHaveProperty('applicationNumber');
expect(drug).toHaveProperty('sponsorName');
expect(drug).toHaveProperty('activeIngredients');
expect(drug).toHaveProperty('approvalDate');
expect(drug).toHaveProperty('approvalStatus');
expect(drug).toHaveProperty('dosageForm');
expect(drug).toHaveProperty('route');
expect(Array.isArray(drug.activeIngredients)).toBe(true);
}
}, 20000);
it('should handle wildcard search', async () => {
const params = {
limit: 5,
};
const result = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result) return;
// Wildcard search should return results
expect(result).toHaveProperty('drugs');
}, 20000);
});
describe('getAdverseEvents', () => {
it('should get adverse events for a drug', async () => {
const params = {
drugName: 'aspirin',
limit: 10,
};
const result = await skipIfRateLimited(() => client.getAdverseEvents(params));
if (!result) return;
expect(result).toHaveProperty('events');
expect(result).toHaveProperty('totalCount');
expect(result).toHaveProperty('summary');
expect(Array.isArray(result.events)).toBe(true);
expect(typeof result.totalCount).toBe('number');
}, 20000);
it('should return proper summary structure', async () => {
const params = {
drugName: 'ibuprofen',
limit: 20,
};
const result = await skipIfRateLimited(() => client.getAdverseEvents(params));
if (!result) return;
expect(result.summary).toHaveProperty('total');
expect(result.summary).toHaveProperty('serious');
expect(result.summary).toHaveProperty('hospitalizations');
expect(result.summary).toHaveProperty('deaths');
expect(result.summary).toHaveProperty('topEvents');
expect(typeof result.summary.total).toBe('number');
expect(typeof result.summary.serious).toBe('number');
expect(Array.isArray(result.summary.topEvents)).toBe(true);
}, 20000);
it('should return properly structured adverse event objects', async () => {
const params = {
drugName: 'metformin',
limit: 5,
};
const result = await skipIfRateLimited(() => client.getAdverseEvents(params));
if (!result) return;
if (result.events.length > 0) {
const event = result.events[0];
expect(event).toHaveProperty('reportId');
expect(event).toHaveProperty('drugName');
expect(event).toHaveProperty('eventDate');
expect(event).toHaveProperty('eventDescription');
expect(event).toHaveProperty('seriousness');
expect(event).toHaveProperty('outcome');
expect(event).toHaveProperty('reporterType');
// Check seriousness value
expect(['Serious', 'Non-serious']).toContain(event.seriousness);
}
}, 20000);
it('should filter by date range', async () => {
const params = {
drugName: 'aspirin',
dateRange: {
from: '2020-01-01',
to: '2023-12-31',
},
limit: 10,
};
const result = await skipIfRateLimited(() => client.getAdverseEvents(params));
if (!result) return;
expect(result).toHaveProperty('events');
expect(Array.isArray(result.events)).toBe(true);
}, 20000);
it('should return empty results for non-existent drug', async () => {
const params = {
drugName: 'xyznonexistentdrug9876543210',
limit: 5,
};
const result = await skipIfRateLimited(() => client.getAdverseEvents(params));
if (!result) return;
expect(result.events).toHaveLength(0);
expect(result.totalCount).toBe(0);
expect(result.summary.total).toBe(0);
}, 20000);
it('should cache adverse event results', async () => {
const params = {
drugName: 'lisinopril',
limit: 5,
};
// First call
const start1 = Date.now();
const result1 = await skipIfRateLimited(() => client.getAdverseEvents(params));
if (!result1) return;
const time1 = Date.now() - start1;
// Second call (should be cached)
const start2 = Date.now();
const result2 = await client.getAdverseEvents(params);
const time2 = Date.now() - start2;
expect(result1).toEqual(result2);
expect(time2).toBeLessThan(time1);
}, 25000);
it('should generate top events summary', async () => {
const params = {
drugName: 'acetaminophen',
limit: 50,
};
const result = await skipIfRateLimited(() => client.getAdverseEvents(params));
if (!result) return;
if (result.events.length > 0) {
expect(result.summary.topEvents.length).toBeGreaterThan(0);
expect(result.summary.topEvents.length).toBeLessThanOrEqual(10);
result.summary.topEvents.forEach(topEvent => {
expect(topEvent).toHaveProperty('event');
expect(topEvent).toHaveProperty('count');
expect(typeof topEvent.event).toBe('string');
expect(typeof topEvent.count).toBe('number');
});
}
}, 20000);
});
describe('caching', () => {
it('should clear cache successfully', () => {
expect(() => client.clearCache()).not.toThrow();
});
it('should return fresh results after cache clear', async () => {
const params = {
drugName: 'omeprazole',
limit: 2,
};
// First call
const result1 = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result1) return;
// Clear cache
client.clearCache();
// Second call (should make new API request)
const start = Date.now();
const result2 = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result2) return;
const time = Date.now() - start;
// Should take longer than a cached call
expect(time).toBeGreaterThan(10);
}, 25000);
});
describe('size monitoring', () => {
it('should enable and disable size monitoring', () => {
client.setSizeMonitoring(true);
expect(() => client.getSizeMonitoringConfig()).not.toThrow();
client.setSizeMonitoring(false);
expect(() => client.getSizeMonitoringConfig()).not.toThrow();
});
it('should get size monitoring config', () => {
const config = client.getSizeMonitoringConfig();
expect(config).toHaveProperty('maxResponseSize');
expect(config).toHaveProperty('warningThreshold');
expect(config).toHaveProperty('truncationMode');
expect(config).toHaveProperty('enableSizeTracking');
});
it('should update size monitoring config', () => {
client.updateSizeMonitoringConfig({ maxResponseSize: 2000000 });
const config = client.getSizeMonitoringConfig();
expect(config.maxResponseSize).toBe(2000000);
});
});
describe('error handling', () => {
it('should handle API errors gracefully for drugs search', async () => {
const params = {
drugName: 'valid drug name',
limit: 5,
};
// Should not throw for valid params (unless rate limited)
const result = await skipIfRateLimited(() => client.searchDrugs(params));
if (result) {
expect(result).toBeDefined();
}
}, 20000);
it('should handle API errors gracefully for adverse events', async () => {
const params = {
drugName: 'valid drug name',
limit: 5,
};
// Should not throw for valid params (unless rate limited)
const result = await skipIfRateLimited(() => client.getAdverseEvents(params));
if (result) {
expect(result).toBeDefined();
}
}, 20000);
});
describe('alternative search strategies', () => {
it('should try alternative strategies when primary search fails', async () => {
// Use a drug name that might require alternative search strategies
const params = {
drugName: 'Tylenol', // Brand name that may require different field searches
limit: 5,
};
const result = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result) return;
// Should either find results or return empty (not throw)
expect(result).toHaveProperty('drugs');
expect(result).toHaveProperty('totalCount');
}, 30000);
it('should handle combined drug name and active ingredient search', async () => {
const params = {
drugName: 'Advil',
activeIngredient: 'ibuprofen',
limit: 5,
};
const result = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result) return;
expect(result).toHaveProperty('drugs');
expect(Array.isArray(result.drugs)).toBe(true);
}, 25000);
});
describe('data transformation', () => {
it('should transform drug data with active ingredients', async () => {
const params = {
drugName: 'metformin',
limit: 3,
};
const result = await skipIfRateLimited(() => client.searchDrugs(params));
if (!result) return;
if (result.drugs.length > 0) {
const drug = result.drugs[0];
if (drug.activeIngredients.length > 0) {
const ingredient = drug.activeIngredients[0];
expect(ingredient).toHaveProperty('name');
expect(ingredient).toHaveProperty('strength');
}
}
}, 20000);
it('should transform adverse event with patient data', async () => {
const params = {
drugName: 'warfarin',
limit: 10,
};
const result = await skipIfRateLimited(() => client.getAdverseEvents(params));
if (!result) return;
if (result.events.length > 0) {
const event = result.events[0];
// Patient sex should be properly transformed
if (event.patientSex) {
expect(['Male', 'Female', 'Unknown']).toContain(event.patientSex);
}
// Patient age should be a number if present
if (event.patientAge !== undefined) {
expect(typeof event.patientAge).toBe('number');
}
}
}, 20000);
});
});