275 lines
7.6 KiB
TypeScript
275 lines
7.6 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}); |