import { describe, it, expect, vi, beforeEach } from 'vitest'; import axios from 'axios'; import { GitHubService } from './github'; import { Repository } from './config'; // Mock axios vi.mock('axios'); const mockedAxios = vi.mocked(axios); describe('GitHubService', () => { let githubService: GitHubService; const mockToken = 'test-token'; const mockRepository: Repository = { owner: 'test-owner', name: 'test-repo' }; beforeEach(() => { githubService = new GitHubService(mockToken, 300); // 300 seconds cache timeout vi.clearAllMocks(); vi.useFakeTimers(); }); describe('constructor', () => { it('should initialize with correct cache timeout in seconds', () => { const service = new GitHubService('token', 600); expect(service.getCacheTimeout()).toBe(600); }); it('should use default cache timeout when not specified', () => { const service = new GitHubService('token'); expect(service.getCacheTimeout()).toBe(300); }); }); describe('getWorkflowRuns', () => { it('should fetch workflow runs successfully', async () => { const mockResponse = { data: { workflow_runs: [ { id: 1, name: 'Test Workflow', display_title: 'Test Run', status: 'completed', conclusion: 'success', created_at: '2024-01-01T10:00:00Z', repository: { id: 1, name: 'test-repo', full_name: 'test-owner/test-repo', owner: { login: 'test-owner', avatar_url: 'https://github.com/test-owner.png' } }, actor: { login: 'test-actor', avatar_url: 'https://github.com/test-actor.png' } } ] }, headers: { 'x-ratelimit-limit': '5000', 'x-ratelimit-remaining': '4999', 'x-ratelimit-reset': '1640995200', 'x-ratelimit-used': '1' } }; mockedAxios.get.mockResolvedValueOnce(mockResponse); const result = await githubService.getWorkflowRuns(mockRepository, 1); expect(mockedAxios.get).toHaveBeenCalledWith( 'https://api.github.com/repos/test-owner/test-repo/actions/runs', { headers: { 'Authorization': 'token test-token', 'Accept': 'application/vnd.github.v3+json', 'X-GitHub-Api-Version': '2022-11-28' }, params: { per_page: 1, page: 1 } } ); expect(result).toEqual(mockResponse.data.workflow_runs); }); it('should handle API errors gracefully', async () => { mockedAxios.get.mockRejectedValueOnce(new Error('API Error')); const result = await githubService.getWorkflowRuns(mockRepository); expect(result).toEqual([]); }); it('should handle rate limit exceeded', async () => { mockedAxios.get.mockRejectedValueOnce({ response: { status: 403, headers: { 'x-ratelimit-remaining': '0' } } }); const result = await githubService.getWorkflowRuns(mockRepository); expect(result).toEqual([]); }); }); describe('caching', () => { it('should cache responses', async () => { const mockResponse = { data: { workflow_runs: [{ id: 1, name: 'Test' }] }, headers: { 'x-ratelimit-remaining': '4999' } }; mockedAxios.get.mockResolvedValueOnce(mockResponse); // First call const result1 = await githubService.getWorkflowRuns(mockRepository, 1); // Second call should use cache const result2 = await githubService.getWorkflowRuns(mockRepository, 1); expect(mockedAxios.get).toHaveBeenCalledTimes(1); expect(result1).toEqual(result2); }); it('should expire cache after TTL', async () => { const mockResponse = { data: { workflow_runs: [{ id: 1, name: 'Test' }] }, headers: { 'x-ratelimit-remaining': '4999' } }; mockedAxios.get.mockResolvedValue(mockResponse); // First call await githubService.getWorkflowRuns(mockRepository, 1); // Advance time beyond cache TTL (300 seconds) vi.advanceTimersByTime(301 * 1000); // Second call should make new request await githubService.getWorkflowRuns(mockRepository, 1); expect(mockedAxios.get).toHaveBeenCalledTimes(2); }); it('should clear cache', async () => { const mockResponse = { data: { workflow_runs: [] }, headers: { 'x-ratelimit-remaining': '4999' } }; mockedAxios.get.mockResolvedValue(mockResponse); await githubService.getWorkflowRuns(mockRepository, 1); const statsBefore = githubService.getCacheStats(); expect(statsBefore.size).toBeGreaterThan(0); githubService.clearCache(); const statsAfter = githubService.getCacheStats(); expect(statsAfter.size).toBe(0); }); }); describe('getLatestMainBranchRuns', () => { it('should fetch runs from multiple repositories in parallel', async () => { const repositories = [ { owner: 'owner1', name: 'repo1' }, { owner: 'owner2', name: 'repo2' } ]; const mockResponse = { data: { workflow_runs: [ { id: 1, name: 'Test', repository: { full_name: 'owner1/repo1', owner: { login: 'owner1' } } } ] }, headers: { 'x-ratelimit-remaining': '4999' } }; mockedAxios.get.mockResolvedValue(mockResponse); const result = await githubService.getLatestMainBranchRuns(repositories); expect(mockedAxios.get).toHaveBeenCalledTimes(2); expect(result).toHaveLength(2); }); it('should handle repositories with no runs', async () => { const repositories = [ { owner: 'owner1', name: 'repo1' } ]; const mockResponse = { data: { workflow_runs: [] }, headers: { 'x-ratelimit-remaining': '4999' } }; mockedAxios.get.mockResolvedValue(mockResponse); const result = await githubService.getLatestMainBranchRuns(repositories); expect(result).toEqual([]); }); }); describe('rate limit handling', () => { it('should update rate limit info from response headers', async () => { const mockResponse = { data: { workflow_runs: [] }, headers: { 'x-ratelimit-limit': '5000', 'x-ratelimit-remaining': '4000', 'x-ratelimit-reset': '1640995200', 'x-ratelimit-used': '1000' } }; mockedAxios.get.mockResolvedValueOnce(mockResponse); await githubService.getWorkflowRuns(mockRepository); const rateLimitInfo = githubService.getRateLimitInfo(); expect(rateLimitInfo.limit).toBe(5000); expect(rateLimitInfo.remaining).toBe(4000); expect(rateLimitInfo.used).toBe(1000); }); }); describe('updateToken', () => { it('should update token without losing cache', () => { const newToken = 'new-token'; const initialCacheSize = githubService.getCacheStats().size; githubService.updateToken(newToken); const finalCacheSize = githubService.getCacheStats().size; expect(finalCacheSize).toBe(initialCacheSize); }); }); describe('getCacheTimeout', () => { it('should return cache timeout in seconds', () => { const service = new GitHubService('token', 600); expect(service.getCacheTimeout()).toBe(600); }); }); });