Adds missing server dir

This commit is contained in:
2025-07-11 09:26:17 -04:00
parent c20b9e98f8
commit 2f2f647a9e
7 changed files with 1879 additions and 0 deletions

275
server/github.test.ts Normal file
View File

@@ -0,0 +1,275 @@
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);
});
});
});