@tiberriver256/mcp-server-azure-devops
Version:
Azure DevOps reference server for the Model Context Protocol (MCP)
445 lines • 19.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const feature_1 = require("./feature");
const azureDevOpsClient = __importStar(require("../../../clients/azure-devops"));
const azure_devops_errors_1 = require("../../../shared/errors/azure-devops-errors");
// Mock the Azure DevOps client
jest.mock('../../../clients/azure-devops');
// Mock the environment utilities to avoid dependency on environment variables
jest.mock('../../../utils/environment', () => ({
defaultOrg: 'azure-devops-mcp-testing',
defaultProject: 'eShopOnWeb',
}));
describe('listWikiPages unit', () => {
// Mock WikiClient
const mockWikiClient = {
listWikiPages: jest.fn(),
};
// Mock getWikiClient function
const mockGetWikiClient = azureDevOpsClient.getWikiClient;
beforeEach(() => {
// Clear mock calls between tests
jest.clearAllMocks();
// Setup default mock implementation
mockGetWikiClient.mockResolvedValue(mockWikiClient);
});
describe('Happy Path Scenarios', () => {
test('should return wiki pages successfully', async () => {
// Mock data
const mockPages = [
{
id: 1,
path: '/Home',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1',
order: 1,
},
{
id: 2,
path: '/Getting-Started',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2',
order: 2,
},
];
// Setup mock responses
mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
// Call the function
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
});
// Assertions
expect(mockGetWikiClient).toHaveBeenCalledWith({
organizationId: 'test-org',
});
expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith('test-project', 'test-wiki', {});
expect(result).toEqual(mockPages);
expect(result.length).toBe(2);
});
test('should handle path filtering parameter', async () => {
const mockPages = [
{
id: 3,
path: '/docs/api',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/3',
order: 1,
},
];
mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
path: '/docs',
});
expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith('test-project', 'test-wiki', { path: '/docs' });
expect(result).toEqual(mockPages);
});
test('should handle recursionLevel parameter', async () => {
const mockPages = [
{
id: 4,
path: '/deep/nested/page',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/4',
order: 1,
},
];
mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
recursionLevel: 10,
});
expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith('test-project', 'test-wiki', { recursionLevel: 10 });
expect(result).toEqual(mockPages);
});
test('should handle both path and recursionLevel parameters', async () => {
const mockPages = [];
mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
path: '/api',
recursionLevel: 5,
});
expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith('test-project', 'test-wiki', { path: '/api', recursionLevel: 5 });
expect(result).toEqual(mockPages);
});
test('should return empty array when no pages found', async () => {
mockWikiClient.listWikiPages.mockResolvedValue([]);
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'empty-wiki',
});
expect(result).toEqual([]);
expect(Array.isArray(result)).toBe(true);
});
test('should use default organization and project when not provided', async () => {
const mockPages = [
{
id: 5,
path: '/default-page',
url: 'https://dev.azure.com/default-org/default-project/_wiki/wikis/wiki1/5',
order: 1,
},
];
mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
const result = await (0, feature_1.listWikiPages)({
wikiId: 'test-wiki',
});
expect(mockGetWikiClient).toHaveBeenCalledWith({
organizationId: 'azure-devops-mcp-testing', // Uses default from environment
});
expect(result).toEqual(mockPages);
});
});
describe('Error Scenarios', () => {
test('should handle network timeout errors', async () => {
const timeoutError = new Error('Network timeout');
timeoutError.name = 'ETIMEDOUT';
mockWikiClient.listWikiPages.mockRejectedValue(timeoutError);
await expect((0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
})).rejects.toThrow(azure_devops_errors_1.AzureDevOpsError);
});
test('should handle connection refused errors', async () => {
const connectionError = new Error('Connection refused');
connectionError.name = 'ECONNREFUSED';
mockWikiClient.listWikiPages.mockRejectedValue(connectionError);
await expect((0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
})).rejects.toThrow(azure_devops_errors_1.AzureDevOpsError);
});
test('should propagate AzureDevOpsResourceNotFoundError from client', async () => {
const notFoundError = new azure_devops_errors_1.AzureDevOpsResourceNotFoundError('Wiki not found: test-wiki');
mockWikiClient.listWikiPages.mockRejectedValue(notFoundError);
await expect((0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'non-existent-wiki',
})).rejects.toThrow(azure_devops_errors_1.AzureDevOpsResourceNotFoundError);
});
test('should propagate AzureDevOpsPermissionError from client', async () => {
const permissionError = new azure_devops_errors_1.AzureDevOpsPermissionError('Permission denied to access wiki');
mockWikiClient.listWikiPages.mockRejectedValue(permissionError);
await expect((0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'restricted-wiki',
})).rejects.toThrow(azure_devops_errors_1.AzureDevOpsPermissionError);
});
test('should wrap unknown errors in AzureDevOpsError', async () => {
const unknownError = new Error('Unknown error occurred');
mockWikiClient.listWikiPages.mockRejectedValue(unknownError);
await expect((0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
})).rejects.toThrow(azure_devops_errors_1.AzureDevOpsError);
try {
await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
});
}
catch (error) {
expect(error).toBeInstanceOf(azure_devops_errors_1.AzureDevOpsError);
expect(error.message).toBe('Failed to list wiki pages');
}
});
test('should handle client creation failure', async () => {
const clientError = new Error('Failed to create client');
mockGetWikiClient.mockRejectedValue(clientError);
await expect((0, feature_1.listWikiPages)({
organizationId: 'invalid-org',
projectId: 'test-project',
wikiId: 'test-wiki',
})).rejects.toThrow(azure_devops_errors_1.AzureDevOpsError);
});
});
describe('Edge Cases and Input Validation', () => {
test('should handle malformed API response gracefully', async () => {
// Mock malformed response (missing required fields)
const malformedPages = [
{
id: 'invalid-id', // Should be number
path: null, // Should be string
url: undefined, // Should be string
},
];
mockWikiClient.listWikiPages.mockResolvedValue(malformedPages);
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
});
// Should still return the data as-is (transformation happens in client)
expect(result).toEqual(malformedPages);
});
test('should handle null/undefined response from client', async () => {
mockWikiClient.listWikiPages.mockResolvedValue(null);
await expect((0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
})).rejects.toThrow(azure_devops_errors_1.AzureDevOpsError);
});
test('should handle very large page collections', async () => {
// Create a large mock dataset
const largeMockPages = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
path: `/page-${i + 1}`,
url: `https://dev.azure.com/org/project/_wiki/wikis/wiki1/${i + 1}`,
order: i + 1,
}));
mockWikiClient.listWikiPages.mockResolvedValue(largeMockPages);
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'large-wiki',
});
expect(result).toEqual(largeMockPages);
expect(result.length).toBe(10000);
});
test('should handle pages with special characters in paths', async () => {
const specialCharPages = [
{
id: 1,
path: '/页面-中文',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1',
order: 1,
},
{
id: 2,
path: '/página-español',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2',
order: 2,
},
{
id: 3,
path: '/page with spaces & symbols!@#$%',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/3',
order: 3,
},
];
mockWikiClient.listWikiPages.mockResolvedValue(specialCharPages);
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'special-wiki',
});
expect(result).toEqual(specialCharPages);
});
test('should handle pages with missing optional order field', async () => {
const pagesWithoutOrder = [
{
id: 1,
path: '/page-1',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1',
// order field is optional and missing
},
{
id: 2,
path: '/page-2',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2',
order: 5,
},
];
mockWikiClient.listWikiPages.mockResolvedValue(pagesWithoutOrder);
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
});
expect(result).toEqual(pagesWithoutOrder);
expect(result[0].order).toBeUndefined();
expect(result[1].order).toBe(5);
});
});
describe('Parameter Validation Edge Cases', () => {
test('should handle boundary recursionLevel values', async () => {
const mockPages = [];
mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
// Test minimum value
await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
recursionLevel: 1,
});
expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith('test-project', 'test-wiki', { recursionLevel: 1 });
// Test maximum value
await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
recursionLevel: 50,
});
expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith('test-project', 'test-wiki', { recursionLevel: 50 });
});
test('should handle empty string parameters', async () => {
const mockPages = [];
mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
await (0, feature_1.listWikiPages)({
organizationId: '',
projectId: '',
wikiId: 'test-wiki',
path: '',
});
expect(mockGetWikiClient).toHaveBeenCalledWith({
organizationId: 'azure-devops-mcp-testing', // Empty string gets overridden by default
});
expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith('eShopOnWeb', // Empty string gets overridden by default project
'test-wiki', { path: '' });
});
test('should handle very long path values', async () => {
const longPath = '/' + 'a'.repeat(1000);
const mockPages = [];
mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
path: longPath,
});
expect(mockWikiClient.listWikiPages).toHaveBeenCalledWith('test-project', 'test-wiki', { path: longPath });
});
});
describe('Data Transformation and Mapping', () => {
test('should preserve all WikiPageSummary fields correctly', async () => {
const mockPages = [
{
id: 42,
path: '/test-page',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/42',
order: 10,
},
];
mockWikiClient.listWikiPages.mockResolvedValue(mockPages);
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
});
expect(result[0]).toEqual({
id: 42,
path: '/test-page',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/42',
order: 10,
});
});
test('should handle mixed data types in response', async () => {
const mixedPages = [
{
id: 1,
path: '/normal-page',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/1',
order: 1,
},
{
id: 2,
path: '/page-without-order',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/2',
// order is undefined
},
{
id: 3,
path: '/page-with-zero-order',
url: 'https://dev.azure.com/org/project/_wiki/wikis/wiki1/3',
order: 0,
},
];
mockWikiClient.listWikiPages.mockResolvedValue(mixedPages);
const result = await (0, feature_1.listWikiPages)({
organizationId: 'test-org',
projectId: 'test-project',
wikiId: 'test-wiki',
});
expect(result).toEqual(mixedPages);
expect(result[1].order).toBeUndefined();
expect(result[2].order).toBe(0);
});
});
});
//# sourceMappingURL=feature.spec.unit.js.map