UNPKG

@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
/** * 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'); }); }); });