Skip to main content
Glama
issues-client.test.ts17.3 kB
/** * @fileoverview Tests for issues client * This file adds coverage for the previously untested issues-client.ts */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { IssuesClient } from '../../client/issues-client.js'; import type { IssuesClientTestable, MockDeepSourceClient } from '../test-types.js'; import { TestableIssuesClient } from '../utils/test-utils.js'; describe('IssuesClient', () => { let issuesClient: IssuesClient; let mockBaseClient: MockDeepSourceClient; beforeEach(() => { issuesClient = new IssuesClient('test-api-key'); mockBaseClient = issuesClient as unknown as MockDeepSourceClient; // Mock the methods we need mockBaseClient.findProjectByKey = vi.fn(); mockBaseClient.executeGraphQL = vi.fn(); mockBaseClient.createEmptyPaginatedResponse = vi.fn(); mockBaseClient.normalizePaginationParams = vi.fn(); mockBaseClient.logger = { info: vi.fn(), error: vi.fn(), debug: vi.fn(), warn: vi.fn(), }; }); describe('getIssues', () => { it('should fetch issues successfully', async () => { const mockProject = { repository: { login: 'test-org', name: 'test-repo', provider: 'github', }, }; const mockResponse = { data: { repository: { issues: { edges: [ { node: { id: 'issue-1', title: 'Test Issue', shortcode: 'JS-0001', category: 'BUG_RISK', severity: 'HIGH', occurrences: { edges: [ { node: { id: 'occ-1', status: 'ACTIVE', issueText: 'Test issue description', filePath: '/src/test.js', beginLine: 10, tags: ['security'], }, }, ], }, }, }, ], pageInfo: { hasNextPage: false, hasPreviousPage: false, startCursor: null, endCursor: null, }, }, }, }, }; mockBaseClient.findProjectByKey.mockResolvedValue(mockProject); mockBaseClient.normalizePaginationParams.mockReturnValue({ first: 20, after: null, }); mockBaseClient.executeGraphQL.mockResolvedValue(mockResponse); const result = await issuesClient.getIssues('test-project'); expect(mockBaseClient.findProjectByKey).toHaveBeenCalledWith('test-project'); expect(mockBaseClient.executeGraphQL).toHaveBeenCalledWith( expect.stringContaining('query getRepositoryIssues'), expect.objectContaining({ login: 'test-org', name: 'test-repo', provider: 'github', }) ); expect(result.items).toHaveLength(1); expect(result.items[0]?.id).toBe('occ-1'); // Uses occurrence ID as issue ID expect(result.items[0]?.shortcode).toBe('JS-0001'); expect(result.items[0]?.category).toBe('BUG_RISK'); }); it('should return empty response when project not found', async () => { mockBaseClient.findProjectByKey.mockResolvedValue(null); mockBaseClient.createEmptyPaginatedResponse.mockReturnValue({ items: [], pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: 0, }); const result = await issuesClient.getIssues('nonexistent-project'); expect(result.items).toHaveLength(0); expect(result.totalCount).toBe(0); }); it('should handle GraphQL errors', async () => { const mockProject = { repository: { login: 'test-org', name: 'test-repo', provider: 'github', }, }; mockBaseClient.findProjectByKey.mockResolvedValue(mockProject); mockBaseClient.normalizePaginationParams.mockReturnValue({ first: 20, }); mockBaseClient.executeGraphQL.mockRejectedValue(new Error('GraphQL error')); await expect(issuesClient.getIssues('test-project')).rejects.toThrow('GraphQL error'); }); it('should handle missing data in response', async () => { const mockProject = { repository: { login: 'test-org', name: 'test-repo', provider: 'github', }, }; const mockResponse = { data: null, }; mockBaseClient.findProjectByKey.mockResolvedValue(mockProject); mockBaseClient.normalizePaginationParams.mockReturnValue({ first: 20, }); mockBaseClient.executeGraphQL.mockResolvedValue(mockResponse); await expect(issuesClient.getIssues('test-project')).rejects.toThrow( 'No data received from GraphQL API' ); }); it('should handle filter parameters', async () => { const mockProject = { repository: { login: 'test-org', name: 'test-repo', provider: 'github', }, }; const mockResponse = { data: { repository: { issues: { edges: [], pageInfo: { hasNextPage: false, hasPreviousPage: false, startCursor: null, endCursor: null, }, }, }, }, }; mockBaseClient.findProjectByKey.mockResolvedValue(mockProject); mockBaseClient.normalizePaginationParams.mockReturnValue({ analyzerIn: ['javascript'], tags: ['security'], path: '/src/', first: 10, }); mockBaseClient.executeGraphQL.mockResolvedValue(mockResponse); await issuesClient.getIssues('test-project', { analyzerIn: ['javascript'], tags: ['security'], path: '/src/', first: 10, }); // The normalizePaginationParams is now a static method and its behavior // is tested separately in base-client tests }); it('should handle null issuesData in response', async () => { const mockProject = { key: 'test-project', repository: { login: 'test-org', name: 'test-repo', provider: 'github', }, }; // Mock response with no issues data const mockResponse = { data: { repository: null, // This will cause issuesData to be undefined }, }; mockBaseClient.findProjectByKey.mockResolvedValue(mockProject); mockBaseClient.normalizePaginationParams.mockReturnValue({ first: 10 }); mockBaseClient.executeGraphQL.mockResolvedValue(mockResponse); const result = await issuesClient.getIssues('test-project', { first: 10 }); // Should return empty response when repository is null expect(result.items).toEqual([]); expect(result.totalCount).toBe(0); expect(result.pageInfo.hasNextPage).toBe(false); expect(result.pageInfo.hasPreviousPage).toBe(false); }); it('should handle missing pageInfo in issuesData', async () => { const mockProject = { key: 'test-project', repository: { login: 'test-org', name: 'test-repo', provider: 'github', }, }; // Mock response with issues but no pageInfo const mockResponse = { data: { repository: { issues: { edges: [ { node: { id: 'issue-1', title: 'Test Issue', category: 'BUG', shortcode: 'TEST-001', severity: 'HIGH', occurrences: { edges: [ { node: { id: 'occ-1', status: 'ACTIVE', issueText: 'Test issue', filePath: '/test.js', beginLine: 10, tags: [], }, }, ], }, }, }, ], totalCount: 1, // pageInfo is missing - this will trigger the fallback }, }, }, }; mockBaseClient.findProjectByKey.mockResolvedValue(mockProject); mockBaseClient.normalizePaginationParams.mockReturnValue({ first: 10 }); mockBaseClient.executeGraphQL.mockResolvedValue(mockResponse); const result = await issuesClient.getIssues('test-project', { first: 10 }); // Should use default pageInfo when missing expect(result.pageInfo).toEqual({ hasNextPage: false, hasPreviousPage: false, }); expect(result.items).toHaveLength(1); expect(result.totalCount).toBe(1); }); }); describe('buildIssuesQuery', () => { it('should build correct GraphQL query', () => { const query = TestableIssuesClient.testBuildIssuesQuery(); expect(query).toContain('query getRepositoryIssues'); expect(query).toContain('$login: String!'); expect(query).toContain('$name: String!'); expect(query).toContain('$provider: VCSProvider!'); expect(query).toContain('issues('); expect(query).toContain('occurrences('); }); }); describe('extractIssuesFromResponse', () => { it('should extract issues from GraphQL response', () => { const mockResponseData = { repository: { issues: { edges: [ { node: { id: 'issue-1', title: 'Test Issue', shortcode: 'JS-0001', category: 'BUG_RISK', severity: 'HIGH', occurrences: { edges: [ { node: { id: 'occ-1', status: 'ACTIVE', issueText: 'Test issue description', filePath: '/src/test.js', beginLine: 10, tags: ['security'], }, }, ], }, }, }, ], }, }, }; const issues = (issuesClient as IssuesClientTestable).extractIssuesFromResponse( mockResponseData ) as unknown[]; expect(issues).toHaveLength(1); expect(issues[0]?.id).toBe('occ-1'); // Uses occurrence ID as issue ID expect(issues[0]?.shortcode).toBe('JS-0001'); expect(issues[0]?.category).toBe('BUG_RISK'); }); it('should handle missing issues in response', () => { const mockResponseData = { repository: {}, }; const issues = (issuesClient as IssuesClientTestable).extractIssuesFromResponse( mockResponseData ) as unknown[]; expect(issues).toHaveLength(0); }); it('should handle missing occurrences in response', () => { const mockResponseData = { repository: { issues: { edges: [ { node: { id: 'issue-1', title: 'Test Issue', shortcode: 'JS-0001', category: 'BUG_RISK', severity: 'HIGH', occurrences: { edges: [], }, }, }, ], }, }, }; const issues = (issuesClient as IssuesClientTestable).extractIssuesFromResponse( mockResponseData ) as unknown[]; expect(issues).toHaveLength(0); // No occurrences means no issues }); it('should handle null occurrences edges in response', () => { const mockResponseData = { repository: { issues: { edges: [ { node: { id: 'issue-1', title: 'Test Issue', shortcode: 'JS-0001', category: 'BUG_RISK', severity: 'HIGH', occurrences: { edges: null, }, }, }, ], }, }, }; const issues = (issuesClient as IssuesClientTestable).extractIssuesFromResponse( mockResponseData ) as unknown[]; expect(issues).toHaveLength(0); // Null edges means no issues }); it('should handle missing occurrences property entirely', () => { const mockResponseData = { repository: { issues: { edges: [ { node: { id: 'issue-1', title: 'Test Issue', shortcode: 'JS-0001', category: 'BUG_RISK', severity: 'HIGH', // occurrences is missing entirely }, }, ], }, }, }; const issues = (issuesClient as IssuesClientTestable).extractIssuesFromResponse( mockResponseData ) as unknown[]; expect(issues).toHaveLength(0); // Missing occurrences means no issues }); it('should handle error thrown during extraction', () => { const mockResponseData = { repository: { issues: { edges: 'invalid', // This will cause an error when trying to iterate }, }, }; // Should not throw, but handle the error gracefully const issues = (issuesClient as IssuesClientTestable).extractIssuesFromResponse( mockResponseData ) as unknown[]; expect(issues).toHaveLength(0); // Error results in empty array }); }); describe('getIssue', () => { it('should fetch a specific issue by calling getIssues', async () => { const mockIssues = { items: [ { id: 'issue-1', title: 'Test Issue 1', shortcode: 'JS-0001', category: 'BUG_RISK', severity: 'HIGH', status: 'ACTIVE', issue_text: 'Issue description', file_path: '/src/test.js', line_number: 10, tags: ['security'], }, { id: 'issue-2', title: 'Test Issue 2', shortcode: 'JS-0002', category: 'SECURITY', severity: 'CRITICAL', status: 'ACTIVE', issue_text: 'Another issue', file_path: '/src/test2.js', line_number: 20, tags: ['performance'], }, ], pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: 2, }; // Mock getIssues to return our test issues vi.spyOn(issuesClient, 'getIssues').mockResolvedValue(mockIssues); const result = await issuesClient.getIssue('test-project', 'issue-2'); expect(result).toBeDefined(); expect(result?.id).toBe('issue-2'); expect(result?.shortcode).toBe('JS-0002'); expect(result?.category).toBe('SECURITY'); }); it('should return null when issue not found', async () => { const mockIssues = { items: [ { id: 'issue-1', title: 'Test Issue 1', shortcode: 'JS-0001', category: 'BUG_RISK', severity: 'HIGH', status: 'ACTIVE', issue_text: 'Issue description', file_path: '/src/test.js', line_number: 10, tags: ['security'], }, ], pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: 1, }; vi.spyOn(issuesClient, 'getIssues').mockResolvedValue(mockIssues); const result = await issuesClient.getIssue('test-project', 'non-existent-issue'); expect(result).toBeNull(); }); it('should handle errors from getIssues', async () => { vi.spyOn(issuesClient, 'getIssues').mockRejectedValue(new Error('Failed to fetch issues')); await expect(issuesClient.getIssue('test-project', 'issue-1')).rejects.toThrow( 'Failed to fetch issues' ); }); }); describe('handleIssuesError', () => { it('should return empty response for NoneType errors', () => { const error = new Error('NoneType error'); const result = (issuesClient as IssuesClientTestable).handleIssuesError(error); expect(result.items).toHaveLength(0); expect(result.totalCount).toBe(0); expect(result.pageInfo.hasNextPage).toBe(false); }); it('should re-throw non-NoneType errors', () => { const error = new Error('Other error'); expect(() => { (issuesClient as IssuesClientTestable).handleIssuesError(error); }).toThrow('Other error'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server