Skip to content

Commit 0468c1c

Browse files
authored
add wiki tests to improve code coverage (#469)
Improve WorkItems code coverage from; ```sh ----------------|---------|----------|---------|---------|------------------------------------------------------ File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ----------------|---------|----------|---------|---------|------------------------------------------------------ wiki.ts | 90.06 | 79.59 | 100 | 90.41 | 170-182,193,360,381-383 ----------------|---------|----------|---------|---------|------------------------------------------------------ ``` to ```sh ----------------|---------|----------|---------|---------|------------------------------------------------------ File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ----------------|---------|----------|---------|---------|------------------------------------------------------ wiki.ts | 98.67 | 93.87 | 100 | 99.31 | 193 ----------------|---------|----------|---------|---------|------------------------------------------------------ ``` ## GitHub issue number None ## **Associated Risks** None ## ✅ **PR Checklist** - [x] **I have read the [contribution guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CONTRIBUTING.md)** - [x] **I have read the [code of conduct guidelines](https://github.com/microsoft/azure-devops-mcp/blob/main/CODE_OF_CONDUCT.md)** - [x] Title of the pull request is clear and informative. - [x] 👌 Code hygiene - [x] 🔭 Telemetry added, updated, or N/A - [x] 📄 Documentation added, updated, or N/A - [x] 🛡️ Automated tests added, or N/A ## 🧪 **How did you test it?** Run `npm run test` command
1 parent cdd7758 commit 0468c1c

File tree

1 file changed

+123
-5
lines changed

1 file changed

+123
-5
lines changed

test/src/tools/wiki.test.ts

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ describe("configureWikiTools", () => {
1717
let server: McpServer;
1818
let tokenProvider: TokenProviderMock;
1919
let connectionProvider: ConnectionProviderMock;
20-
let mockConnection: { getWikiApi: jest.Mock };
20+
let mockConnection: {
21+
getWikiApi: jest.Mock;
22+
serverUrl: string;
23+
};
2124
let mockWikiApi: WikiApiMock;
2225

2326
beforeEach(() => {
@@ -31,6 +34,7 @@ describe("configureWikiTools", () => {
3134
};
3235
mockConnection = {
3336
getWikiApi: jest.fn().mockResolvedValue(mockWikiApi),
37+
serverUrl: "https://dev.azure.com/testorg",
3438
};
3539
connectionProvider = jest.fn().mockResolvedValue(mockConnection);
3640
});
@@ -502,7 +506,7 @@ describe("configureWikiTools", () => {
502506

503507
// Mock fetch for REST page by id returning content
504508
const mockFetch = jest.fn();
505-
global.fetch = mockFetch as any;
509+
global.fetch = mockFetch as typeof fetch;
506510
mockFetch.mockResolvedValueOnce({
507511
ok: true,
508512
json: jest.fn().mockResolvedValue({ content: "# Page Title\nBody" }),
@@ -512,7 +516,7 @@ describe("configureWikiTools", () => {
512516
const result = await handler({ url });
513517

514518
// Current implementation may fallback to root path stream retrieval
515-
expect(mockWikiApi.getPageText).toHaveBeenCalledWith("project", "myWiki", "/", undefined, undefined, true);
519+
expect(mockWikiApi.getPageText).not.toHaveBeenCalled();
516520
// Content either direct or from stream JSON string wrapping
517521
expect(result.content[0].text).toContain("Page Title");
518522
});
@@ -525,7 +529,7 @@ describe("configureWikiTools", () => {
525529
(tokenProvider as jest.Mock).mockResolvedValueOnce({ token: "abc", expiresOnTimestamp: Date.now() + 10000 });
526530

527531
const mockFetch = jest.fn();
528-
global.fetch = mockFetch as any;
532+
global.fetch = mockFetch as typeof fetch;
529533
mockFetch.mockResolvedValueOnce({
530534
ok: true,
531535
json: jest.fn().mockResolvedValue({ path: "/Some/Page" }),
@@ -545,7 +549,7 @@ describe("configureWikiTools", () => {
545549
const result = await handler({ url });
546550

547551
// Implementation currently falls back to root path if path not resolved prior to fallback
548-
expect(mockWikiApi.getPageText).toHaveBeenCalledWith("project", "myWiki", "/", undefined, undefined, true);
552+
expect(mockWikiApi.getPageText).toHaveBeenCalledWith("project", "myWiki", "/Some/Page", undefined, undefined, true);
549553
expect(result.content[0].text).toBe('"fallback content"');
550554
});
551555

@@ -579,6 +583,120 @@ describe("configureWikiTools", () => {
579583
expect(result.isError).toBe(true);
580584
expect(result.content[0].text).toContain("Error fetching wiki page content: URL does not match expected wiki pattern");
581585
});
586+
587+
it("should handle invalid URL format", async () => {
588+
configureWikiTools(server, tokenProvider, connectionProvider);
589+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wiki_get_page_content");
590+
if (!call) throw new Error("wiki_get_page_content tool not registered");
591+
const [, , , handler] = call;
592+
593+
const result = await handler({ url: "not-a-valid-url" });
594+
expect(result.isError).toBe(true);
595+
expect(result.content[0].text).toContain("Error fetching wiki page content: Invalid URL format");
596+
});
597+
598+
it("should handle URL with pageId that returns 404", async () => {
599+
configureWikiTools(server, tokenProvider, connectionProvider);
600+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wiki_get_page_content");
601+
if (!call) throw new Error("wiki_get_page_content tool not registered");
602+
const [, , , handler] = call;
603+
604+
(tokenProvider as jest.Mock).mockResolvedValueOnce({ token: "abc", expiresOnTimestamp: Date.now() + 10000 });
605+
606+
const mockFetch = jest.fn();
607+
global.fetch = mockFetch as unknown as typeof fetch;
608+
mockFetch.mockResolvedValueOnce({
609+
ok: false,
610+
status: 404,
611+
});
612+
613+
const url = "https://dev.azure.com/org/project/_wiki/wikis/myWiki/999/NonExistent-Page";
614+
const result = await handler({ url });
615+
616+
expect(result.isError).toBe(true);
617+
expect(result.content[0].text).toContain("Error fetching wiki page content: Page with id 999 not found");
618+
});
619+
620+
it("should handle URL that resolves but project/wiki end up undefined", async () => {
621+
configureWikiTools(server, tokenProvider, connectionProvider);
622+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wiki_get_page_content");
623+
if (!call) throw new Error("wiki_get_page_content tool not registered");
624+
const [, , , handler] = call;
625+
626+
const url = "https://dev.azure.com/org//_wiki/wikis/?pagePath=%2FHome";
627+
const result = await handler({ url });
628+
629+
expect(result.isError).toBe(true);
630+
expect(result.content[0].text).toContain("Error fetching wiki page content: Could not extract project or wikiIdentifier from URL");
631+
});
632+
633+
it("should handle URL with non-numeric pageId", async () => {
634+
configureWikiTools(server, tokenProvider, connectionProvider);
635+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wiki_get_page_content");
636+
if (!call) throw new Error("wiki_get_page_content tool not registered");
637+
const [, , , handler] = call;
638+
639+
const mockStream = {
640+
setEncoding: jest.fn(),
641+
on: function (event: string, cb: (chunk?: unknown) => void) {
642+
if (event === "data") setImmediate(() => cb("content for non-numeric path"));
643+
if (event === "end") setImmediate(() => cb());
644+
return this;
645+
},
646+
};
647+
mockWikiApi.getPageText.mockResolvedValue(mockStream as unknown);
648+
649+
const url = "https://dev.azure.com/org/project/_wiki/wikis/myWiki/not-a-number/Some-Page";
650+
const result = await handler({ url });
651+
652+
expect(mockWikiApi.getPageText).toHaveBeenCalledWith("project", "myWiki", "/", undefined, undefined, true);
653+
expect(result.content[0].text).toBe('"content for non-numeric path"');
654+
});
655+
656+
it("should use default root path when resolvedPath is undefined", async () => {
657+
configureWikiTools(server, tokenProvider, connectionProvider);
658+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wiki_get_page_content");
659+
if (!call) throw new Error("wiki_get_page_content tool not registered");
660+
const [, , , handler] = call;
661+
662+
const mockStream = {
663+
setEncoding: jest.fn(),
664+
on: function (event: string, cb: (chunk?: unknown) => void) {
665+
if (event === "data") setImmediate(() => cb("root page content"));
666+
if (event === "end") setImmediate(() => cb());
667+
return this;
668+
},
669+
};
670+
mockWikiApi.getPageText.mockResolvedValue(mockStream as unknown);
671+
672+
const result = await handler({ wikiIdentifier: "wiki1", project: "project1" });
673+
674+
expect(mockWikiApi.getPageText).toHaveBeenCalledWith("project1", "wiki1", "/", undefined, undefined, true);
675+
expect(result.content[0].text).toBe('"root page content"');
676+
expect(result.isError).toBeUndefined();
677+
});
678+
679+
it("should handle scenario where resolvedProject/Wiki become null after URL processing", async () => {
680+
configureWikiTools(server, tokenProvider, connectionProvider);
681+
const call = (server.tool as jest.Mock).mock.calls.find(([toolName]) => toolName === "wiki_get_page_content");
682+
if (!call) throw new Error("wiki_get_page_content tool not registered");
683+
const [, , , handler] = call;
684+
685+
(tokenProvider as jest.Mock).mockResolvedValueOnce({ token: "abc", expiresOnTimestamp: Date.now() + 10000 });
686+
687+
const mockFetch = jest.fn();
688+
global.fetch = mockFetch as unknown as typeof fetch;
689+
mockFetch.mockResolvedValueOnce({
690+
ok: false,
691+
status: 404,
692+
});
693+
694+
const url = "https://dev.azure.com//_wiki/wikis//123/Page";
695+
const result = await handler({ url });
696+
697+
expect(result.isError).toBe(true);
698+
expect(result.content[0].text).toContain("URL does not match expected wiki pattern");
699+
});
582700
});
583701

584702
describe("create_or_update_page tool", () => {

0 commit comments

Comments
 (0)