Skip to content

Commit 92f10a2

Browse files
test: add comprehensive E2E tests for domain filtering and read-only mode
Add 8 new E2E test cases covering: - Domain filtering (single domain, multiple domains, comma-separated) - Read-only mode (filters write operations) - Combined filtering (domain + read-only mode) - Helper function to create clients with custom CLI args Test coverage: - Default behavior (43 tools) - Single domain filtering (5 tools) - Multiple domain filtering (9 tools) - Comma-separated domain syntax - Read-only mode (~22 tools) - Combined filtering (2 tools, 95% reduction) - Multiple domains with read-only mode Tests validate: - Correct tool counts for each configuration - Presence of expected read-only tools - Absence of write operation tools in read-only mode - Absence of tools from non-selected domains Co-authored-by: Tiberriver256 <6989492+Tiberriver256@users.noreply.github.com>
1 parent 8252773 commit 92f10a2

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed

src/server.spec.e2e.ts

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,41 @@ import fs from 'fs';
99
// Load environment variables from .env file
1010
dotenv.config();
1111

12+
/**
13+
* Helper function to create a client with custom CLI arguments
14+
*/
15+
async function createClientWithArgs(
16+
args: string[] = [],
17+
): Promise<{ client: Client; transport: StdioClientTransport }> {
18+
const serverPath = join(process.cwd(), 'dist', 'index.js');
19+
const tempEnvFile = join(process.cwd(), '.env.e2e-test');
20+
21+
const transport = new StdioClientTransport({
22+
command: 'node',
23+
args: ['-r', 'dotenv/config', serverPath, ...args],
24+
env: {
25+
...process.env,
26+
NODE_ENV: 'test',
27+
DOTENV_CONFIG_PATH: tempEnvFile,
28+
},
29+
});
30+
31+
const client = new Client(
32+
{
33+
name: 'e2e-test-client',
34+
version: '1.0.0',
35+
},
36+
{
37+
capabilities: {
38+
tools: {},
39+
},
40+
},
41+
);
42+
43+
await client.connect(transport);
44+
return { client, transport };
45+
}
46+
1247
describe('Azure DevOps MCP Server E2E Tests', () => {
1348
let client: Client;
1449
let serverProcess: ReturnType<typeof spawn>;
@@ -134,6 +169,218 @@ AZURE_DEVOPS_AUTH_METHOD=${authMethod}
134169
});
135170
});
136171

172+
describe('Domain Filtering', () => {
173+
test('should load all 43 tools by default (no filtering)', async () => {
174+
// Arrange - use default client without any domain args
175+
const tools = await client.listTools();
176+
177+
// Assert
178+
expect(tools.tools).toBeDefined();
179+
expect(tools.tools.length).toBe(43);
180+
});
181+
182+
test('should load only 5 work-items tools when --domains work-items', async () => {
183+
// Arrange
184+
const { client: filteredClient, transport: filteredTransport } =
185+
await createClientWithArgs(['--domains', 'work-items']);
186+
187+
try {
188+
// Act
189+
const tools = await filteredClient.listTools();
190+
191+
// Assert
192+
expect(tools.tools).toBeDefined();
193+
expect(tools.tools.length).toBe(5);
194+
195+
const toolNames = tools.tools.map((t) => t.name);
196+
expect(toolNames).toContain('list_work_items');
197+
expect(toolNames).toContain('get_work_item');
198+
expect(toolNames).toContain('create_work_item');
199+
expect(toolNames).toContain('update_work_item');
200+
expect(toolNames).toContain('manage_work_item_link');
201+
202+
// Should not contain tools from other domains
203+
expect(toolNames).not.toContain('list_repositories');
204+
expect(toolNames).not.toContain('list_projects');
205+
expect(toolNames).not.toContain('list_pipelines');
206+
} finally {
207+
await filteredClient.close();
208+
await filteredTransport.close();
209+
}
210+
});
211+
212+
test('should load 9 tools when --domains core work-items', async () => {
213+
// Arrange
214+
const { client: filteredClient, transport: filteredTransport } =
215+
await createClientWithArgs(['--domains', 'core', 'work-items']);
216+
217+
try {
218+
// Act
219+
const tools = await filteredClient.listTools();
220+
221+
// Assert
222+
expect(tools.tools).toBeDefined();
223+
expect(tools.tools.length).toBe(9);
224+
225+
const toolNames = tools.tools.map((t) => t.name);
226+
227+
// Core domain tools (4)
228+
expect(toolNames).toContain('list_organizations');
229+
expect(toolNames).toContain('list_projects');
230+
expect(toolNames).toContain('get_project');
231+
expect(toolNames).toContain('get_me');
232+
233+
// Work items domain tools (5)
234+
expect(toolNames).toContain('list_work_items');
235+
expect(toolNames).toContain('get_work_item');
236+
237+
// Should not contain tools from other domains
238+
expect(toolNames).not.toContain('list_repositories');
239+
expect(toolNames).not.toContain('list_pipelines');
240+
} finally {
241+
await filteredClient.close();
242+
await filteredTransport.close();
243+
}
244+
});
245+
246+
test('should support comma-separated domains', async () => {
247+
// Arrange
248+
const { client: filteredClient, transport: filteredTransport } =
249+
await createClientWithArgs(['--domains', 'core,work-items']);
250+
251+
try {
252+
// Act
253+
const tools = await filteredClient.listTools();
254+
255+
// Assert - should be same as space-separated
256+
expect(tools.tools).toBeDefined();
257+
expect(tools.tools.length).toBe(9);
258+
} finally {
259+
await filteredClient.close();
260+
await filteredTransport.close();
261+
}
262+
});
263+
});
264+
265+
describe('Read-Only Mode', () => {
266+
test('should filter out write operations when --read-only', async () => {
267+
// Arrange
268+
const { client: readOnlyClient, transport: readOnlyTransport } =
269+
await createClientWithArgs(['--read-only']);
270+
271+
try {
272+
// Act
273+
const tools = await readOnlyClient.listTools();
274+
275+
// Assert
276+
const toolNames = tools.tools.map((t) => t.name);
277+
278+
// Should contain read-only tools
279+
expect(toolNames).toContain('list_work_items');
280+
expect(toolNames).toContain('get_work_item');
281+
expect(toolNames).toContain('list_repositories');
282+
expect(toolNames).toContain('get_repository');
283+
expect(toolNames).toContain('list_projects');
284+
expect(toolNames).toContain('get_project');
285+
286+
// Should NOT contain write operations
287+
expect(toolNames).not.toContain('create_work_item');
288+
expect(toolNames).not.toContain('update_work_item');
289+
expect(toolNames).not.toContain('create_branch');
290+
expect(toolNames).not.toContain('create_commit');
291+
expect(toolNames).not.toContain('create_pull_request');
292+
expect(toolNames).not.toContain('update_pull_request');
293+
expect(toolNames).not.toContain('trigger_pipeline');
294+
expect(toolNames).not.toContain('create_wiki');
295+
expect(toolNames).not.toContain('update_wiki_page');
296+
297+
// Verify we filtered out approximately half the tools
298+
expect(tools.tools.length).toBeGreaterThanOrEqual(20);
299+
expect(tools.tools.length).toBeLessThanOrEqual(25);
300+
} finally {
301+
await readOnlyClient.close();
302+
await readOnlyTransport.close();
303+
}
304+
});
305+
});
306+
307+
describe('Combined Filtering (Domain + Read-Only)', () => {
308+
test('should load only 2 tools when --domains work-items --read-only', async () => {
309+
// Arrange
310+
const { client: filteredClient, transport: filteredTransport } =
311+
await createClientWithArgs([
312+
'--domains',
313+
'work-items',
314+
'--read-only',
315+
]);
316+
317+
try {
318+
// Act
319+
const tools = await filteredClient.listTools();
320+
321+
// Assert - 95% reduction! (43 -> 2)
322+
expect(tools.tools).toBeDefined();
323+
expect(tools.tools.length).toBe(2);
324+
325+
const toolNames = tools.tools.map((t) => t.name);
326+
expect(toolNames).toContain('list_work_items');
327+
expect(toolNames).toContain('get_work_item');
328+
329+
// Should not contain write operations
330+
expect(toolNames).not.toContain('create_work_item');
331+
expect(toolNames).not.toContain('update_work_item');
332+
333+
// Should not contain tools from other domains
334+
expect(toolNames).not.toContain('list_repositories');
335+
expect(toolNames).not.toContain('list_projects');
336+
} finally {
337+
await filteredClient.close();
338+
await filteredTransport.close();
339+
}
340+
});
341+
342+
test('should handle multiple domains with read-only mode', async () => {
343+
// Arrange
344+
const { client: filteredClient, transport: filteredTransport } =
345+
await createClientWithArgs([
346+
'--domains',
347+
'core',
348+
'repositories',
349+
'--read-only',
350+
]);
351+
352+
try {
353+
// Act
354+
const tools = await filteredClient.listTools();
355+
356+
// Assert
357+
const toolNames = tools.tools.map((t) => t.name);
358+
359+
// Core domain read-only tools
360+
expect(toolNames).toContain('list_organizations');
361+
expect(toolNames).toContain('list_projects');
362+
expect(toolNames).toContain('get_project');
363+
expect(toolNames).toContain('get_me');
364+
365+
// Repositories domain read-only tools
366+
expect(toolNames).toContain('list_repositories');
367+
expect(toolNames).toContain('get_repository');
368+
expect(toolNames).toContain('get_file_content');
369+
370+
// Should not contain write operations
371+
expect(toolNames).not.toContain('create_branch');
372+
expect(toolNames).not.toContain('create_commit');
373+
374+
// Should not contain tools from other domains
375+
expect(toolNames).not.toContain('list_work_items');
376+
expect(toolNames).not.toContain('list_pipelines');
377+
} finally {
378+
await filteredClient.close();
379+
await filteredTransport.close();
380+
}
381+
});
382+
});
383+
137384
describe('Organizations', () => {
138385
test('should list organizations', async () => {
139386
// Arrange

0 commit comments

Comments
 (0)