Skip to main content
Glama
component-service.test.ts19.4 kB
import { ComponentService, createComponentService } from '../../../src/services/component-service.js'; import { MockDataAccessLayer } from '../../fixtures/mocks/mock-data-access.js'; import { MockCacheService } from '../../fixtures/mocks/mock-cache.js'; import { NotFoundError } from '../../../src/utils/errors.js'; describe('ComponentService', () => { let service: ComponentService; let mockDataAccess: MockDataAccessLayer; let mockCache: MockCacheService; beforeEach(() => { mockDataAccess = new MockDataAccessLayer(); mockCache = new MockCacheService(); service = new ComponentService(mockDataAccess as any, mockCache as any); }); afterEach(() => { jest.restoreAllMocks(); }); describe('getAllComponents', () => { it('should retrieve all components successfully', async () => { const components = await service.getAllComponents(); expect(components).toBeDefined(); expect(Array.isArray(components)).toBe(true); expect(components.length).toBe(4); expect(mockDataAccess.getCallCount('getComponents')).toBe(1); }); it('should use cached data when available', async () => { // First call const components1 = await service.getAllComponents(); expect(mockDataAccess.getCallCount('getComponents')).toBe(1); // Second call - should use cache const components2 = await service.getAllComponents(); expect(mockDataAccess.getCallCount('getComponents')).toBe(1); // Still 1 expect(components2).toEqual(components1); }); it('should transform components with correct structure', async () => { const components = await service.getAllComponents(); const component = components[0]; expect(component).toHaveProperty('id'); expect(component).toHaveProperty('name'); expect(component).toHaveProperty('status'); expect(component).toHaveProperty('position'); expect(component).toHaveProperty('onlyShowIfDegraded'); }); it('should map component statuses correctly', async () => { const components = await service.getAllComponents(); // component-1: operational -> none expect(components[0].status).toBe('none'); // component-2: degraded_performance -> minor expect(components[1].status).toBe('minor'); }); }); describe('getComponentById', () => { it('should retrieve component by ID successfully', async () => { const component = await service.getComponentById('component-1'); expect(component).toBeDefined(); expect(component.id).toBe('component-1'); expect(component.name).toBe('Distributed Cloud Services - API Gateway'); }); it('should throw NotFoundError for non-existent ID', async () => { await expect(service.getComponentById('non-existent-id')) .rejects .toThrow(NotFoundError); }); it('should throw NotFoundError with descriptive message', async () => { await expect(service.getComponentById('missing')) .rejects .toThrow('Component not found: missing'); }); }); describe('getComponentByName', () => { it('should retrieve component by exact name match', async () => { const component = await service.getComponentByName('XC Observability - Metrics'); expect(component).toBeDefined(); expect(component.id).toBe('component-2'); expect(component.name).toBe('XC Observability - Metrics'); }); it('should handle case-insensitive name matching', async () => { const component = await service.getComponentByName('xc observability - metrics'); expect(component).toBeDefined(); expect(component.id).toBe('component-2'); }); it('should throw NotFoundError for non-existent name', async () => { await expect(service.getComponentByName('Non-Existent Component')) .rejects .toThrow(NotFoundError); }); it('should throw NotFoundError with descriptive message', async () => { await expect(service.getComponentByName('Missing')) .rejects .toThrow('Component not found: Missing'); }); }); describe('getComponentsByStatus', () => { it('should filter components by "none" status', async () => { const components = await service.getComponentsByStatus('none'); expect(components.length).toBe(3); components.forEach(c => expect(c.status).toBe('none')); }); it('should filter components by "minor" status', async () => { const components = await service.getComponentsByStatus('minor'); expect(components.length).toBe(1); expect(components[0].id).toBe('component-2'); expect(components[0].status).toBe('minor'); }); it('should return empty array when no matches', async () => { const components = await service.getComponentsByStatus('critical'); expect(components).toEqual([]); }); it('should filter components by "major" status', async () => { const components = await service.getComponentsByStatus('major'); expect(components.length).toBe(0); }); }); describe('getComponentsByGroup', () => { it('should filter components by group name', async () => { const components = await service.getComponentsByGroup('group-1'); expect(components.length).toBe(3); components.forEach(c => expect(c.group).toBe('group-1')); }); it('should filter components by different group', async () => { const components = await service.getComponentsByGroup('group-2'); expect(components.length).toBe(1); expect(components[0].id).toBe('component-4'); }); it('should return empty array for non-existent group', async () => { const components = await service.getComponentsByGroup('non-existent-group'); expect(components).toEqual([]); }); }); describe('getDegradedComponents', () => { it('should return only non-operational components', async () => { const components = await service.getDegradedComponents(); expect(components.length).toBe(1); expect(components[0].id).toBe('component-2'); expect(components[0].status).not.toBe('none'); }); it('should exclude operational components', async () => { const components = await service.getDegradedComponents(); const operationalFound = components.some(c => c.status === 'none'); expect(operationalFound).toBe(false); }); }); describe('getOperationalComponents', () => { it('should return only operational components', async () => { const components = await service.getOperationalComponents(); expect(components.length).toBe(3); components.forEach(c => expect(c.status).toBe('none')); }); it('should exclude degraded components', async () => { const components = await service.getOperationalComponents(); const degradedFound = components.some(c => c.status !== 'none'); expect(degradedFound).toBe(false); }); }); describe('getComponentGroups', () => { it('should group components correctly', async () => { const groups = await service.getComponentGroups(); expect(groups).toBeDefined(); expect(Array.isArray(groups)).toBe(true); expect(groups.length).toBe(2); // group-1 and group-2 }); it('should include component counts per group', async () => { const groups = await service.getComponentGroups(); const group1 = groups.find(g => g.name === 'group-1'); const group2 = groups.find(g => g.name === 'group-2'); expect(group1?.components.length).toBe(3); expect(group2?.components.length).toBe(1); }); it('should include all components in groups', async () => { const groups = await service.getComponentGroups(); const totalComponents = groups.reduce((sum, g) => sum + g.components.length, 0); expect(totalComponents).toBe(4); }); it('should assign group IDs and positions', async () => { const groups = await service.getComponentGroups(); groups.forEach(group => { expect(group).toHaveProperty('id'); expect(group).toHaveProperty('position'); expect(group.id).toMatch(/^group-\d+$/); }); }); }); describe('searchComponents', () => { it('should search by partial name match', async () => { const components = await service.searchComponents('Observability'); expect(components.length).toBe(2); expect(components[0].name).toContain('Observability'); expect(components[1].name).toContain('Observability'); }); it('should be case-insensitive', async () => { const components = await service.searchComponents('observability'); expect(components.length).toBe(2); }); it('should support regex patterns', async () => { const components = await service.searchComponents('XC.*Metrics'); expect(components.length).toBe(1); expect(components[0].id).toBe('component-2'); }); it('should return empty array when no matches', async () => { const components = await service.searchComponents('NonExistentPattern'); expect(components).toEqual([]); }); it('should search across all component names', async () => { const components = await service.searchComponents('Cloud|WAF'); expect(components.length).toBeGreaterThan(0); }); }); describe('getComponentCountByStatus', () => { it('should count components by each status', async () => { const counts = await service.getComponentCountByStatus(); expect(counts).toEqual({ none: 3, minor: 1, major: 0, critical: 0 }); }); it('should include all status levels', async () => { const counts = await service.getComponentCountByStatus(); expect(counts).toHaveProperty('none'); expect(counts).toHaveProperty('minor'); expect(counts).toHaveProperty('major'); expect(counts).toHaveProperty('critical'); }); it('should sum to total component count', async () => { const counts = await service.getComponentCountByStatus(); const total = Object.values(counts).reduce((sum, count) => sum + count, 0); expect(total).toBe(4); }); }); describe('isComponentOperational', () => { it('should return true for operational component', async () => { const isOperational = await service.isComponentOperational('component-1'); expect(isOperational).toBe(true); }); it('should return false for degraded component', async () => { const isOperational = await service.isComponentOperational('component-2'); expect(isOperational).toBe(false); }); it('should throw NotFoundError for non-existent component', async () => { await expect(service.isComponentOperational('non-existent')) .rejects .toThrow(NotFoundError); }); }); describe('invalidateCache', () => { it('should clear components cache', async () => { // Populate cache await service.getAllComponents(); expect(mockCache.has('all-components')).toBe(true); // Invalidate service.invalidateCache(); expect(mockCache.has('all-components')).toBe(false); }); it('should force fresh data fetch after invalidation', async () => { // First call await service.getAllComponents(); expect(mockDataAccess.getCallCount('getComponents')).toBe(1); // Invalidate and call again service.invalidateCache(); await service.getAllComponents(); expect(mockDataAccess.getCallCount('getComponents')).toBe(2); }); }); describe('edge cases', () => { it('should handle empty component list gracefully', async () => { // Mock empty response mockDataAccess.setApiClient({ fetchSummary: jest.fn().mockResolvedValue({ page: { id: 'test', name: 'Test', url: '', time_zone: '', updated_at: '' }, components: [], incidents: [], scheduled_maintenances: [], status: { indicator: 'none', description: 'OK' } }) } as any); const components = await service.getAllComponents(); expect(components).toEqual([]); const groups = await service.getComponentGroups(); expect(groups).toEqual([]); }); it('should handle components without groups', async () => { const groups = await service.getComponentGroups(); // All our test components have groups, but the method handles ungrouped expect(groups.length).toBeGreaterThan(0); }); it('should handle concurrent requests efficiently', async () => { const promises = [ service.getAllComponents(), service.getAllComponents(), service.getAllComponents() ]; const results = await Promise.all(promises); // All should return same data expect(results[0]).toEqual(results[1]); expect(results[1]).toEqual(results[2]); }); it('should handle components with partial_outage status', async () => { mockDataAccess.setApiClient({ fetchSummary: jest.fn().mockResolvedValue({ page: { id: 'test', name: 'Test', url: '', time_zone: '', updated_at: '' }, components: [{ id: 'comp-outage', name: 'Outage Component', status: 'partial_outage', position: 1, group_id: null, only_show_if_degraded: false }], incidents: [], scheduled_maintenances: [], status: { indicator: 'major', description: 'Partial Outage' } }) } as any); const components = await service.getAllComponents(); expect(components[0].status).toBe('major'); }); it('should handle components with major_outage status', async () => { mockDataAccess.setApiClient({ fetchSummary: jest.fn().mockResolvedValue({ page: { id: 'test', name: 'Test', url: '', time_zone: '', updated_at: '' }, components: [{ id: 'comp-critical', name: 'Critical Component', status: 'major_outage', position: 1, group_id: null, only_show_if_degraded: false }], incidents: [], scheduled_maintenances: [], status: { indicator: 'critical', description: 'Major Outage' } }) } as any); const components = await service.getAllComponents(); expect(components[0].status).toBe('critical'); }); it('should handle components with unknown status', async () => { mockDataAccess.setApiClient({ fetchSummary: jest.fn().mockResolvedValue({ page: { id: 'test', name: 'Test', url: '', time_zone: '', updated_at: '' }, components: [{ id: 'comp-unknown', name: 'Unknown Component', status: 'unknown_status', position: 1, group_id: null, only_show_if_degraded: false }], incidents: [], scheduled_maintenances: [], status: { indicator: 'none', description: 'OK' } }) } as any); const components = await service.getAllComponents(); expect(components[0].status).toBe('none'); // defaults to 'none' }); it('should handle already transformed components (from scraper)', async () => { // Test the scraper fallback path where components are pre-transformed // by mocking getComponents to return an array directly const scraperComponents = [ { id: 'scraper-comp-1', name: 'Scraper Component', status: 'minor' as const, description: 'From scraper', group: 'scraper-group', position: 1, onlyShowIfDegraded: false } ]; // Override the getComponents method to simulate scraper returning pre-transformed data jest.spyOn(mockDataAccess, 'getComponents').mockResolvedValue(scraperComponents as any); // Clear cache to force fresh fetch mockCache.clear(); const components = await service.getAllComponents(); expect(components).toEqual(scraperComponents); expect(components[0].id).toBe('scraper-comp-1'); }); it('should handle "Ungrouped" components in getComponentGroups', async () => { // Test components without group assignment const ungroupedComponents = [ { id: 'ungrouped-1', name: 'Ungrouped Component 1', status: 'none' as const, position: 1, onlyShowIfDegraded: false }, { id: 'ungrouped-2', name: 'Ungrouped Component 2', status: 'none' as const, position: 2, onlyShowIfDegraded: false } ]; jest.spyOn(mockDataAccess, 'getComponents').mockResolvedValue(ungroupedComponents as any); mockCache.clear(); const groups = await service.getComponentGroups(); // Should create an "Ungrouped" group for components without group field const ungroupedGroup = groups.find(g => g.name === 'Ungrouped'); expect(ungroupedGroup).toBeDefined(); expect(ungroupedGroup?.components.length).toBe(2); }); it('should handle components without description', async () => { mockDataAccess.setApiClient({ fetchSummary: jest.fn().mockResolvedValue({ page: { id: 'test', name: 'Test', url: '', time_zone: '', updated_at: '' }, components: [{ id: 'comp-no-desc', name: 'No Description Component', status: 'operational', position: 1, group_id: null, description: null, only_show_if_degraded: false }], incidents: [], scheduled_maintenances: [], status: { indicator: 'none', description: 'OK' } }) } as any); mockCache.clear(); const components = await service.getAllComponents(); expect(components[0].description).toBeUndefined(); }); it('should handle components without group_id', async () => { mockDataAccess.setApiClient({ fetchSummary: jest.fn().mockResolvedValue({ page: { id: 'test', name: 'Test', url: '', time_zone: '', updated_at: '' }, components: [{ id: 'comp-no-group', name: 'No Group Component', status: 'operational', position: 1, group_id: null, only_show_if_degraded: false }], incidents: [], scheduled_maintenances: [], status: { indicator: 'none', description: 'OK' } }) } as any); mockCache.clear(); const components = await service.getAllComponents(); expect(components[0].group).toBeUndefined(); }); }); describe('createComponentService factory', () => { it('should create ComponentService instance', () => { const instance = createComponentService(mockDataAccess as any, mockCache as any); expect(instance).toBeInstanceOf(ComponentService); }); it('should create functional service instance', async () => { const instance = createComponentService(mockDataAccess as any, mockCache as any); const components = await instance.getAllComponents(); expect(components).toBeDefined(); expect(Array.isArray(components)).toBe(true); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/robinmordasiewicz/f5cloudstatus-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server