UNPKG

@tiberriver256/mcp-server-azure-devops

Version:

Azure DevOps reference server for the Model Context Protocol (MCP)

302 lines 12.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const azure_devops_node_api_1 = require("azure-devops-node-api"); const axios_1 = __importDefault(require("axios")); const feature_1 = require("./feature"); const errors_1 = require("../../../shared/errors"); // Mock axios jest.mock('axios'); const mockedAxios = axios_1.default; // Mock @azure/identity jest.mock('@azure/identity', () => ({ DefaultAzureCredential: jest.fn().mockImplementation(() => ({ getToken: jest .fn() .mockResolvedValue({ token: 'mock-azure-identity-token' }), })), AzureCliCredential: jest.fn(), })); // Mock WebApi jest.mock('azure-devops-node-api'); const MockedWebApi = azure_devops_node_api_1.WebApi; describe('searchWorkItems', () => { let connection; let options; let mockResponse; beforeEach(() => { // Reset mocks jest.clearAllMocks(); // Mock environment variables process.env.AZURE_DEVOPS_AUTH_METHOD = 'pat'; process.env.AZURE_DEVOPS_PAT = 'mock-pat'; // Set up connection mock // Create a mock auth handler that implements IRequestHandler const mockAuthHandler = { prepareRequest: jest.fn(), canHandleAuthentication: jest.fn().mockReturnValue(true), handleAuthentication: jest.fn(), }; connection = new MockedWebApi('https://dev.azure.com/mock-org', mockAuthHandler); connection.serverUrl = 'https://dev.azure.com/mock-org'; connection.getCoreApi.mockResolvedValue({ getProjects: jest.fn().mockResolvedValue([]), }); // Set up options options = { searchText: 'test query', projectId: 'mock-project', top: 50, skip: 0, includeFacets: true, }; // Set up mock response mockResponse = { count: 2, results: [ { project: { id: 'project-id-1', name: 'mock-project', }, fields: { 'system.id': '42', 'system.workitemtype': 'Bug', 'system.title': 'Test Bug', 'system.state': 'Active', 'system.assignedto': 'Test User', }, hits: [ { fieldReferenceName: 'system.title', highlights: ['Test <b>Bug</b>'], }, ], url: 'https://dev.azure.com/mock-org/mock-project/_workitems/edit/42', }, { project: { id: 'project-id-1', name: 'mock-project', }, fields: { 'system.id': '43', 'system.workitemtype': 'Task', 'system.title': 'Test Task', 'system.state': 'New', 'system.assignedto': 'Test User', }, hits: [ { fieldReferenceName: 'system.title', highlights: ['Test <b>Task</b>'], }, ], url: 'https://dev.azure.com/mock-org/mock-project/_workitems/edit/43', }, ], facets: { 'System.WorkItemType': [ { name: 'Bug', id: 'Bug', resultCount: 1, }, { name: 'Task', id: 'Task', resultCount: 1, }, ], }, }; // Mock axios response mockedAxios.post.mockResolvedValue({ data: mockResponse }); }); afterEach(() => { // Clean up environment variables delete process.env.AZURE_DEVOPS_AUTH_METHOD; delete process.env.AZURE_DEVOPS_PAT; }); it('should search work items with the correct parameters', async () => { // Act const result = await (0, feature_1.searchWorkItems)(connection, options); // Assert expect(mockedAxios.post).toHaveBeenCalledWith('https://almsearch.dev.azure.com/mock-org/mock-project/_apis/search/workitemsearchresults?api-version=7.1', { searchText: 'test query', $skip: 0, $top: 50, filters: { 'System.TeamProject': ['mock-project'], }, includeFacets: true, }, expect.objectContaining({ headers: expect.objectContaining({ Authorization: expect.stringContaining('Basic'), 'Content-Type': 'application/json', }), })); expect(result).toEqual(mockResponse); }); it('should include filters when provided', async () => { // Arrange options.filters = { 'System.WorkItemType': ['Bug', 'Task'], 'System.State': ['Active'], }; // Act await (0, feature_1.searchWorkItems)(connection, options); // Assert expect(mockedAxios.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ filters: { 'System.TeamProject': ['mock-project'], 'System.WorkItemType': ['Bug', 'Task'], 'System.State': ['Active'], }, }), expect.any(Object)); }); it('should include orderBy when provided', async () => { // Arrange options.orderBy = [{ field: 'System.CreatedDate', sortOrder: 'ASC' }]; // Act await (0, feature_1.searchWorkItems)(connection, options); // Assert expect(mockedAxios.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ $orderBy: [{ field: 'System.CreatedDate', sortOrder: 'ASC' }], }), expect.any(Object)); }); it('should handle 404 errors correctly', async () => { // Arrange - Mock the implementation to throw the specific error mockedAxios.post.mockImplementation(() => { throw new errors_1.AzureDevOpsResourceNotFoundError('Resource not found: Project not found'); }); // Act & Assert await expect((0, feature_1.searchWorkItems)(connection, options)).rejects.toThrow(errors_1.AzureDevOpsResourceNotFoundError); }); it('should handle 400 errors correctly', async () => { // Arrange - Mock the implementation to throw the specific error mockedAxios.post.mockImplementation(() => { throw new errors_1.AzureDevOpsValidationError('Invalid request: Invalid query'); }); // Act & Assert await expect((0, feature_1.searchWorkItems)(connection, options)).rejects.toThrow(errors_1.AzureDevOpsValidationError); }); it('should handle 401/403 errors correctly', async () => { // Arrange - Mock the implementation to throw the specific error mockedAxios.post.mockImplementation(() => { throw new errors_1.AzureDevOpsPermissionError('Permission denied: Permission denied'); }); // Act & Assert await expect((0, feature_1.searchWorkItems)(connection, options)).rejects.toThrow(errors_1.AzureDevOpsPermissionError); }); it('should handle other axios errors correctly', async () => { // Arrange - Mock the implementation to throw the specific error mockedAxios.post.mockImplementation(() => { throw new errors_1.AzureDevOpsError('Azure DevOps API error: Internal server error'); }); // Act & Assert await expect((0, feature_1.searchWorkItems)(connection, options)).rejects.toThrow(errors_1.AzureDevOpsError); }); it('should handle non-axios errors correctly', async () => { // Arrange mockedAxios.post.mockRejectedValue(new Error('Network error')); // Act & Assert await expect((0, feature_1.searchWorkItems)(connection, options)).rejects.toThrow(errors_1.AzureDevOpsError); }); it('should throw an error if organization cannot be extracted', async () => { // Arrange connection.serverUrl = 'https://invalid-url'; // Act & Assert await expect((0, feature_1.searchWorkItems)(connection, options)).rejects.toThrow(errors_1.AzureDevOpsValidationError); }); it('should use Azure Identity authentication when AZURE_DEVOPS_AUTH_METHOD is azure-identity', async () => { // Mock environment variables const originalEnv = process.env.AZURE_DEVOPS_AUTH_METHOD; process.env.AZURE_DEVOPS_AUTH_METHOD = 'azure-identity'; // Mock the WebApi connection const mockConnection = { serverUrl: 'https://dev.azure.com/testorg', getCoreApi: jest.fn().mockResolvedValue({ getProjects: jest.fn().mockResolvedValue([]), }), }; // Mock axios post const mockResponse = { data: { count: 0, results: [], }, }; axios_1.default.post.mockResolvedValueOnce(mockResponse); // Call the function await (0, feature_1.searchWorkItems)(mockConnection, { projectId: 'testproject', searchText: 'test query', }); // Verify the axios post was called with a Bearer token expect(axios_1.default.post).toHaveBeenCalledWith(expect.any(String), expect.any(Object), { headers: { Authorization: 'Bearer mock-azure-identity-token', 'Content-Type': 'application/json', }, }); // Cleanup process.env.AZURE_DEVOPS_AUTH_METHOD = originalEnv; }); test('should perform organization-wide work item search when projectId is not provided', async () => { // Arrange const mockSearchResponse = { data: { count: 2, results: [ { id: 1, fields: { 'System.Title': 'Test Bug 1', 'System.State': 'Active', 'System.WorkItemType': 'Bug', 'System.TeamProject': 'Project1', }, project: { name: 'Project1', id: 'project-id-1', }, }, { id: 2, fields: { 'System.Title': 'Test Bug 2', 'System.State': 'Active', 'System.WorkItemType': 'Bug', 'System.TeamProject': 'Project2', }, project: { name: 'Project2', id: 'project-id-2', }, }, ], }, }; mockedAxios.post.mockResolvedValueOnce(mockSearchResponse); // Act const result = await (0, feature_1.searchWorkItems)(connection, { searchText: 'bug', }); // Assert expect(result).toBeDefined(); expect(result.count).toBe(2); expect(result.results).toHaveLength(2); expect(result.results[0].fields['System.TeamProject']).toBe('Project1'); expect(result.results[1].fields['System.TeamProject']).toBe('Project2'); expect(mockedAxios.post).toHaveBeenCalledTimes(1); expect(mockedAxios.post).toHaveBeenCalledWith(expect.stringContaining('https://almsearch.dev.azure.com/mock-org/_apis/search/workitemsearchresults'), expect.not.objectContaining({ filters: expect.objectContaining({ 'System.TeamProject': expect.anything(), }), }), expect.any(Object)); }); }); //# sourceMappingURL=feature.spec.unit.js.map