@yeepay/awesome-components-mcp
Version:
MCP server providing access to awesome-components documentation and integration guides with dual-mode operation: direct fetch and GitLab MCP instruction generation
381 lines (309 loc) • 12.4 kB
text/typescript
/**
* Unit tests for GitLab Client Service
*/
import {
fetchGitLabFileContent,
fetchGitLabFileContentSafe,
isValidGitLabUrl,
GitLabError,
GitLabAuthError,
validateAuthenticationConfig
} from '../services/gitlabClient';
import { config } from '../config';
// Mock the global fetch function
const mockFetch = jest.fn();
global.fetch = mockFetch as jest.MockedFunction<typeof fetch>;
describe('GitLab Client Service', () => {
// Store original token value
const originalToken = config.auth.gitlabToken;
beforeEach(() => {
// Reset all mocks before each test
jest.clearAllMocks();
// Clear console.log and console.error mocks
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
// Reset token to original value
(config.auth as any).gitlabToken = originalToken;
});
afterEach(() => {
// Restore console methods
jest.restoreAllMocks();
// Reset token to original value
(config.auth as any).gitlabToken = originalToken;
});
describe('fetchGitLabFileContent', () => {
const testUrl = 'https://gitlab.example.com/repo/-/raw/main/llms.txt';
const testContent = 'This is test content from GitLab';
it('should successfully fetch content from GitLab without token', async () => {
// Ensure no token is set
(config.auth as any).gitlabToken = undefined;
// Mock successful response
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
text: jest.fn().mockResolvedValueOnce(testContent)
} as any);
const result = await fetchGitLabFileContent(testUrl);
expect(result).toBe(testContent);
expect(mockFetch).toHaveBeenCalledWith(testUrl, {
headers: {
'User-Agent': 'awesome-components-mcp/1.0.0'
}
});
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it('should successfully fetch content from GitLab with token', async () => {
// Set a test token
(config.auth as any).gitlabToken = 'test-token-123';
// Mock successful response
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
text: jest.fn().mockResolvedValueOnce(testContent)
} as any);
const result = await fetchGitLabFileContent(testUrl);
expect(result).toBe(testContent);
expect(mockFetch).toHaveBeenCalledWith(testUrl, {
headers: {
'User-Agent': 'awesome-components-mcp/1.0.0',
'Authorization': 'Bearer test-token-123'
}
});
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it('should handle 401 Unauthorized errors', async () => {
// Mock 401 response
mockFetch.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: 'Unauthorized',
text: jest.fn()
} as any);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabAuthError);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('Authentication failed (status: 401)');
});
it('should handle 404 Not Found errors', async () => {
// Mock 404 response
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
text: jest.fn()
} as any);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('File not found (status: 404)');
});
it('should handle 403 Forbidden errors without token', async () => {
// Ensure no token is set
(config.auth as any).gitlabToken = undefined;
// Mock 403 response
mockFetch.mockResolvedValueOnce({
ok: false,
status: 403,
statusText: 'Forbidden',
text: jest.fn()
} as any);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('This repository may be private and require authentication');
});
it('should handle 403 Forbidden errors with token', async () => {
// Set a test token
(config.auth as any).gitlabToken = 'test-token-123';
// Mock 403 response
mockFetch.mockResolvedValueOnce({
ok: false,
status: 403,
statusText: 'Forbidden',
text: jest.fn()
} as any);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('Your token may not have sufficient permissions');
});
it('should handle 5xx server errors', async () => {
// Mock 500 response
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
text: jest.fn()
} as any);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('GitLab server error (status: 500)');
});
it('should handle other HTTP errors', async () => {
// Mock 400 response
mockFetch.mockResolvedValueOnce({
ok: false,
status: 400,
statusText: 'Bad Request',
text: jest.fn()
} as any);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('Failed to fetch content from GitLab (status: 400)');
});
it('should handle network failures', async () => {
// Mock network error
mockFetch.mockRejectedValueOnce(new Error('Network error'));
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow(GitLabError);
await expect(fetchGitLabFileContent(testUrl)).rejects.toThrow('Network error while fetching from GitLab');
});
it('should re-throw GitLabError without wrapping', async () => {
// Mock a response that will trigger a GitLabError
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
text: jest.fn()
} as any);
try {
await fetchGitLabFileContent(testUrl);
} catch (error) {
expect(error).toBeInstanceOf(GitLabError);
expect((error as GitLabError).statusCode).toBe(404);
expect((error as GitLabError).url).toBe(testUrl);
}
});
it('should log successful fetches', async () => {
const consoleSpy = jest.spyOn(console, 'log');
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
text: jest.fn().mockResolvedValueOnce(testContent)
} as any);
await fetchGitLabFileContent(testUrl);
expect(consoleSpy).toHaveBeenCalledWith(`Fetching content from GitLab URL: ${testUrl}`);
expect(consoleSpy).toHaveBeenCalledWith(`Successfully fetched ${testContent.length} characters from ${testUrl}`);
});
it('should log errors for failed requests', async () => {
const consoleSpy = jest.spyOn(console, 'error');
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
text: jest.fn()
} as any);
try {
await fetchGitLabFileContent(testUrl);
} catch {
// Expected to throw
}
expect(consoleSpy).toHaveBeenCalledWith(`GitLab request to ${testUrl} failed: 404 Not Found`);
});
});
describe('isValidGitLabUrl', () => {
it('should validate correct GitLab URLs', () => {
const validUrls = [
'https://gitlab.com/user/repo/-/raw/main/file.txt',
'http://gitlab.example.com/project/-/raw/master/llms.txt',
'https://gitlab.yeepay.com/awesome/awesome-components/-/raw/main/llms.txt'
];
validUrls.forEach(url => {
expect(isValidGitLabUrl(url)).toBe(true);
});
});
it('should reject invalid URLs', () => {
const invalidUrls = [
'not-a-url',
'ftp://gitlab.com/file.txt',
'https://github.com/user/repo/raw/main/file.txt', // GitHub, not GitLab
'https://example.com/file.txt', // No GitLab indicators
'https://gitlab.com/file.txt', // No raw path
'https://notgitlab.com/-/raw/main/file.txt', // No GitLab in hostname
'https://git.company.com/team/project/-/raw/develop/docs.txt' // git but not gitlab
];
invalidUrls.forEach(url => {
expect(isValidGitLabUrl(url)).toBe(false);
});
});
it('should handle malformed URLs gracefully', () => {
const malformedUrls = [
'',
'http://',
'https://',
'not a url at all'
];
malformedUrls.forEach(url => {
expect(isValidGitLabUrl(url)).toBe(false);
});
});
});
describe('validateAuthenticationConfig', () => {
const testUrl = 'http://gitlab.yeepay.com/awesome/awesome-components/-/raw/main/test.txt';
it('should return valid for no token (public access)', () => {
(config.auth as any).gitlabToken = undefined;
const result = validateAuthenticationConfig(testUrl);
expect(result.isValid).toBe(true);
expect(result.message).toContain('public repositories only');
});
it('should return valid for proper token', () => {
(config.auth as any).gitlabToken = 'valid-token-123456';
const result = validateAuthenticationConfig(testUrl);
expect(result.isValid).toBe(true);
expect(result.message).toContain('Authentication token is configured');
});
it('should return invalid for short token', () => {
(config.auth as any).gitlabToken = 'short';
const result = validateAuthenticationConfig(testUrl);
expect(result.isValid).toBe(false);
expect(result.message).toContain('too short');
});
it('should return invalid for empty token', () => {
(config.auth as any).gitlabToken = ' ';
const result = validateAuthenticationConfig(testUrl);
expect(result.isValid).toBe(false);
expect(result.message).toContain('too short');
});
});
describe('fetchGitLabFileContentSafe', () => {
const validUrl = 'https://gitlab.example.com/repo/-/raw/main/llms.txt';
const invalidUrl = 'https://github.com/user/repo/raw/main/file.txt';
const testContent = 'Safe fetch test content';
it('should fetch content for valid GitLab URLs', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
statusText: 'OK',
text: jest.fn().mockResolvedValueOnce(testContent)
} as any);
const result = await fetchGitLabFileContentSafe(validUrl);
expect(result).toBe(testContent);
expect(mockFetch).toHaveBeenCalledWith(validUrl);
});
it('should reject invalid GitLab URLs', async () => {
await expect(fetchGitLabFileContentSafe(invalidUrl)).rejects.toThrow(GitLabError);
await expect(fetchGitLabFileContentSafe(invalidUrl)).rejects.toThrow('Invalid GitLab URL format');
// Should not make any fetch calls for invalid URLs
expect(mockFetch).not.toHaveBeenCalled();
});
it('should propagate fetch errors for valid URLs', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
statusText: 'Not Found',
text: jest.fn()
} as any);
await expect(fetchGitLabFileContentSafe(validUrl)).rejects.toThrow(GitLabError);
await expect(fetchGitLabFileContentSafe(validUrl)).rejects.toThrow('File not found at GitLab URL');
});
});
describe('GitLabError', () => {
it('should create error with all properties', () => {
const error = new GitLabError('Test error', 404, 'https://example.com');
expect(error.message).toBe('Test error');
expect(error.statusCode).toBe(404);
expect(error.url).toBe('https://example.com');
expect(error.name).toBe('GitLabError');
expect(error).toBeInstanceOf(Error);
});
it('should create error with minimal properties', () => {
const error = new GitLabError('Simple error');
expect(error.message).toBe('Simple error');
expect(error.statusCode).toBeUndefined();
expect(error.url).toBeUndefined();
expect(error.name).toBe('GitLabError');
});
});
});