feat(TRUEREF-0021): implement differential tag indexing
This commit is contained in:
committed by
Giancarmine Salucci
parent
e63279fcf6
commit
f4fe8c6043
173
src/lib/server/crawler/github-compare.test.ts
Normal file
173
src/lib/server/crawler/github-compare.test.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Unit tests for GitHub Compare API client (TRUEREF-0021).
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { fetchGitHubChangedFiles } from './github-compare.js';
|
||||
import { GitHubApiError } from './github-tags.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function mockFetch(status: number, body: unknown): void {
|
||||
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
||||
new Response(JSON.stringify(body), { status })
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// fetchGitHubChangedFiles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('fetchGitHubChangedFiles', () => {
|
||||
it('maps added status correctly', async () => {
|
||||
mockFetch(200, {
|
||||
status: 'ahead',
|
||||
files: [{ filename: 'src/new.ts', status: 'added', sha: 'abc123' }]
|
||||
});
|
||||
const result = await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0');
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({ path: 'src/new.ts', status: 'added', sha: 'abc123' });
|
||||
});
|
||||
|
||||
it('maps modified status correctly', async () => {
|
||||
mockFetch(200, {
|
||||
status: 'ahead',
|
||||
files: [{ filename: 'src/index.ts', status: 'modified', sha: 'def456' }]
|
||||
});
|
||||
const result = await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0');
|
||||
expect(result[0]).toMatchObject({ path: 'src/index.ts', status: 'modified' });
|
||||
});
|
||||
|
||||
it('maps removed status correctly and omits sha', async () => {
|
||||
mockFetch(200, {
|
||||
status: 'ahead',
|
||||
files: [{ filename: 'src/old.ts', status: 'removed', sha: '000000' }]
|
||||
});
|
||||
const result = await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0');
|
||||
expect(result[0]).toMatchObject({ path: 'src/old.ts', status: 'removed' });
|
||||
expect(result[0].sha).toBeUndefined();
|
||||
});
|
||||
|
||||
it('maps renamed status and sets previousPath', async () => {
|
||||
mockFetch(200, {
|
||||
status: 'ahead',
|
||||
files: [
|
||||
{
|
||||
filename: 'src/renamed.ts',
|
||||
status: 'renamed',
|
||||
sha: 'ghi789',
|
||||
previous_filename: 'src/original.ts'
|
||||
}
|
||||
]
|
||||
});
|
||||
const result = await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0');
|
||||
expect(result[0]).toMatchObject({
|
||||
path: 'src/renamed.ts',
|
||||
status: 'renamed',
|
||||
previousPath: 'src/original.ts',
|
||||
sha: 'ghi789'
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty array when compare status is identical', async () => {
|
||||
mockFetch(200, { status: 'identical', files: [] });
|
||||
const result = await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.0.0');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when compare status is behind', async () => {
|
||||
mockFetch(200, {
|
||||
status: 'behind',
|
||||
files: [{ filename: 'src/index.ts', status: 'modified', sha: 'abc' }]
|
||||
});
|
||||
const result = await fetchGitHubChangedFiles('owner', 'repo', 'v1.1.0', 'v1.0.0');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('throws GitHubApiError on 401 unauthorized', async () => {
|
||||
mockFetch(401, { message: 'Unauthorized' });
|
||||
await expect(
|
||||
fetchGitHubChangedFiles('owner', 'private-repo', 'v1.0.0', 'v1.1.0')
|
||||
).rejects.toThrow(GitHubApiError);
|
||||
});
|
||||
|
||||
it('throws GitHubApiError on 404 not found', async () => {
|
||||
mockFetch(404, { message: 'Not Found' });
|
||||
await expect(
|
||||
fetchGitHubChangedFiles('owner', 'missing-repo', 'v1.0.0', 'v1.1.0')
|
||||
).rejects.toThrow(GitHubApiError);
|
||||
});
|
||||
|
||||
it('throws GitHubApiError on 422 unprocessable entity', async () => {
|
||||
mockFetch(422, { message: 'Unprocessable Entity' });
|
||||
await expect(
|
||||
fetchGitHubChangedFiles('owner', 'repo', 'bad-ref', 'v1.1.0')
|
||||
).rejects.toThrow(GitHubApiError);
|
||||
});
|
||||
|
||||
it('returns empty array when files property is missing', async () => {
|
||||
mockFetch(200, { status: 'ahead' });
|
||||
const result = await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when files array is empty', async () => {
|
||||
mockFetch(200, { status: 'ahead', files: [] });
|
||||
const result = await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('maps copied status to modified', async () => {
|
||||
mockFetch(200, {
|
||||
status: 'ahead',
|
||||
files: [{ filename: 'src/copy.ts', status: 'copied', sha: 'jkl012' }]
|
||||
});
|
||||
const result = await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0');
|
||||
expect(result[0]).toMatchObject({ path: 'src/copy.ts', status: 'modified' });
|
||||
});
|
||||
|
||||
it('maps changed status to modified', async () => {
|
||||
mockFetch(200, {
|
||||
status: 'ahead',
|
||||
files: [{ filename: 'src/changed.ts', status: 'changed', sha: 'mno345' }]
|
||||
});
|
||||
const result = await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0');
|
||||
expect(result[0]).toMatchObject({ path: 'src/changed.ts', status: 'modified' });
|
||||
});
|
||||
|
||||
it('sends Authorization header when token is provided', async () => {
|
||||
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ status: 'ahead', files: [] }), { status: 200 })
|
||||
);
|
||||
await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0', 'my-token');
|
||||
const callArgs = fetchSpy.mock.calls[0];
|
||||
const headers = (callArgs[1] as RequestInit).headers as Record<string, string>;
|
||||
expect(headers['Authorization']).toBe('Bearer my-token');
|
||||
});
|
||||
|
||||
it('does not send Authorization header when no token provided', async () => {
|
||||
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ status: 'ahead', files: [] }), { status: 200 })
|
||||
);
|
||||
await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0');
|
||||
const callArgs = fetchSpy.mock.calls[0];
|
||||
const headers = (callArgs[1] as RequestInit).headers as Record<string, string>;
|
||||
expect(headers['Authorization']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws GitHubApiError with correct status code', async () => {
|
||||
mockFetch(403, { message: 'Forbidden' });
|
||||
try {
|
||||
await fetchGitHubChangedFiles('owner', 'repo', 'v1.0.0', 'v1.1.0');
|
||||
expect.fail('should have thrown');
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(GitHubApiError);
|
||||
expect((e as GitHubApiError).status).toBe(403);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user