Skip to content

Commit 2f80738

Browse files
[Zephyr] TM4J-14632 -Create Test Cycle Web link tool (#354)
* TM4J-14632 -Implements Create test cycle web link Tool * fixes * Adds Changelog changes + client.ts * Addressing Comments + Added 201 Response. * removing 201 response
1 parent a9a5015 commit 2f80738

5 files changed

Lines changed: 242 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- [Zephyr] Added a tool `update-test-execution` for updating a test execution [#345](https://github.com/SmartBear/smartbear-mcp/pull/345)
1717
- [Zephyr] Added a tool `get-test-steps` for getting a list of test steps for test case [#355](https://github.com/SmartBear/smartbear-mcp/pull/355)
1818
- [Zephyr] Added a tool `get-test-cases` for fetching test cases linked to a Jira issue [#358](https://github.com/SmartBear/smartbear-mcp/pull/358)
19+
- [Zephyr] Added a tool `create-web-link` for creating a Web link for a Test Cycle [#354](https://github.com/SmartBear/smartbear-mcp/pull/354)
1920

2021
### Changed
2122

docs/products/SmartBear MCP Server/zephyr-integration.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ The following environment variables configure the Zephyr integration:
7575
- **Returns**: The created Test Cycle ID, with the API URL to access it and the Test Cycle key.
7676
- **Use case**: Creating a Test Cycle with its properties.
7777

78+
### Create Test Cycle Web Link
79+
80+
- **Purpose**: Creates a web link that associates a test cycle with a specified generic URL.
81+
**Parameters:**
82+
- Test Cycle key or id (`testCycleIdOrKey`)
83+
- optional description (`description`)
84+
- url (`url`)
85+
- **Returns**: The created Test Cycle Web Link ID and the API self URL that can be used to delete the link.
86+
- **Use case**: Creates a link between a test cycle and a generic URL.
87+
7888
### Update Test Cycle
7989

8090
- **Purpose**: Update an existing Test Cycle within the Zephyr project specified by ID or KEY
@@ -175,7 +185,7 @@ The following environment variables configure the Zephyr integration:
175185
- Test Case key (`testCaseKey`)
176186
- optional description (`description`)
177187
- url (`url`)
178-
- **Returns**: The created Test Case Web Link ID and the API self URL to access it.
188+
- **Returns**: The created Test Case Web Link ID and the API self URL that can be used to delete the link.
179189
- **Use case**: Creates a link between a test case and a generic URL.
180190

181191
### Create Test Case Issue Link
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
2+
import type {
3+
ServerNotification,
4+
ServerRequest,
5+
} from "@modelcontextprotocol/sdk/types.js";
6+
import { beforeEach, describe, expect, it, vi } from "vitest";
7+
import { CreateTestCycleWebLink } from "../../../../../zephyr/tool/test-cycle/create-web-link";
8+
9+
describe("CreateTestCycleWebLink", () => {
10+
let mockClient: any;
11+
let instance: CreateTestCycleWebLink;
12+
13+
const EXTRA_REQUEST_HANDLER: RequestHandlerExtra<
14+
ServerRequest,
15+
ServerNotification
16+
> = {
17+
signal: AbortSignal.timeout(5000),
18+
requestId: "",
19+
sendNotification: (_notification) => {
20+
throw new Error("Function not implemented.");
21+
},
22+
sendRequest: (_request, _resultSchema, _options?) => {
23+
throw new Error("Function not implemented.");
24+
},
25+
};
26+
27+
beforeEach(() => {
28+
mockClient = {
29+
getApiClient: vi.fn().mockReturnValue({
30+
post: vi.fn(),
31+
}),
32+
};
33+
instance = new CreateTestCycleWebLink(mockClient as any);
34+
});
35+
36+
it("should set specification correctly", () => {
37+
expect(instance.specification.title).toBe("Create Test Cycle Web Link");
38+
expect(instance.specification.summary).toBe(
39+
"Create a new Web Link for a Test Cycle in Zephyr",
40+
);
41+
expect(instance.specification.readOnly).toBe(false);
42+
expect(instance.specification.idempotent).toBe(false);
43+
expect(instance.specification.inputSchema).toBeDefined();
44+
});
45+
46+
it("should call apiClient.post with correct params (Test Cycle Key) and return created web link information", async () => {
47+
const responseMock = {
48+
id: 53,
49+
self: "https://<api-base-url>/weblinks/53",
50+
};
51+
52+
mockClient.getApiClient().post.mockResolvedValueOnce(responseMock);
53+
54+
const args = {
55+
testCycleIdOrKey: "SA-R1",
56+
url: "https://example.com",
57+
description: "Link to documentation",
58+
};
59+
60+
const result = await instance.handle(args, EXTRA_REQUEST_HANDLER);
61+
62+
expect(mockClient.getApiClient().post).toHaveBeenCalledWith(
63+
"/testcycles/SA-R1/links/weblinks",
64+
{
65+
url: args.url,
66+
description: args.description,
67+
},
68+
);
69+
70+
expect(result.structuredContent).toBe(responseMock);
71+
});
72+
73+
it("should call apiClient.post with correct params (Test Cycle Id) and return created web link information", async () => {
74+
const responseMock = {
75+
id: 53,
76+
self: "https://<api-base-url>/weblinks/53",
77+
};
78+
79+
mockClient.getApiClient().post.mockResolvedValueOnce(responseMock);
80+
81+
const args = {
82+
testCycleIdOrKey: "10001",
83+
url: "https://example.com",
84+
description: "Link to documentation",
85+
};
86+
87+
const result = await instance.handle(args, EXTRA_REQUEST_HANDLER);
88+
89+
expect(mockClient.getApiClient().post).toHaveBeenCalledWith(
90+
"/testcycles/10001/links/weblinks",
91+
{
92+
url: args.url,
93+
description: args.description,
94+
},
95+
);
96+
97+
expect(result.structuredContent).toBe(responseMock);
98+
});
99+
100+
it("should ignore extra parameters not in the schema", async () => {
101+
const responseMock = {
102+
id: 54,
103+
self: "https://<api-base-url>/weblinks/54",
104+
};
105+
106+
mockClient.getApiClient().post.mockResolvedValueOnce(responseMock);
107+
108+
const args = {
109+
testCycleIdOrKey: "SA-R1",
110+
url: "https://example.com",
111+
description: "Link to documentation",
112+
extraField: "should be ignored",
113+
};
114+
115+
const result = await instance.handle(args, EXTRA_REQUEST_HANDLER);
116+
117+
expect(mockClient.getApiClient().post).toHaveBeenCalledWith(
118+
"/testcycles/SA-R1/links/weblinks",
119+
{
120+
url: args.url,
121+
description: args.description,
122+
},
123+
);
124+
125+
expect(result.structuredContent).toBe(responseMock);
126+
});
127+
128+
it("should handle apiClient.post throwing error", async () => {
129+
mockClient
130+
.getApiClient()
131+
.post.mockRejectedValueOnce(new Error("API error"));
132+
133+
const args = {
134+
testCycleIdOrKey: "SA-R1",
135+
url: "https://example.com",
136+
description: "Link to documentation",
137+
};
138+
139+
await expect(instance.handle(args, EXTRA_REQUEST_HANDLER)).rejects.toThrow(
140+
"API error",
141+
);
142+
});
143+
144+
it("should throw validation error if url is missing", async () => {
145+
const args = {
146+
testCycleIdOrKey: "SA-R1",
147+
description: "Link to documentation",
148+
};
149+
150+
await expect(
151+
instance.handle(args, EXTRA_REQUEST_HANDLER),
152+
).rejects.toThrow();
153+
});
154+
155+
it("should throw validation error if testCycleIdOrKey is missing", async () => {
156+
const args = {
157+
url: "https://example.com",
158+
description: "Link to documentation",
159+
};
160+
161+
await expect(
162+
instance.handle(args, EXTRA_REQUEST_HANDLER),
163+
).rejects.toThrow();
164+
});
165+
});

src/zephyr/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { GetTestCases } from "./tool/test-case/get-test-cases";
2323
import { GetTestCaseSteps } from "./tool/test-case/get-test-steps.ts";
2424
import { UpdateTestCase } from "./tool/test-case/update-test-case.ts";
2525
import { CreateTestCycle } from "./tool/test-cycle/create-test-cycle";
26+
import { CreateTestCycleWebLink } from "./tool/test-cycle/create-web-link.ts";
2627
import { GetTestCycle } from "./tool/test-cycle/get-test-cycle";
2728
import { GetTestCycles } from "./tool/test-cycle/get-test-cycles";
2829
import { UpdateTestCycle } from "./tool/test-cycle/update-test-cycle.ts";
@@ -100,6 +101,7 @@ export class ZephyrClient implements Client {
100101
new UpdateTestExecution(this),
101102
new GetTestCaseSteps(this),
102103
new GetIssueLinkTestCases(this),
104+
new CreateTestCycleWebLink(this),
103105
];
104106

105107
for (const tool of tools) {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import type { ZodRawShape } from "zod";
3+
import { Tool } from "../../../common/tools";
4+
import type { ToolParams } from "../../../common/types";
5+
import type { ZephyrClient } from "../../client";
6+
import {
7+
CreateTestCycleWebLinkBody,
8+
CreateTestCycleWebLinkParams,
9+
} from "../../common/rest-api-schemas";
10+
export class CreateTestCycleWebLink extends Tool<ZephyrClient> {
11+
specification: ToolParams = {
12+
title: "Create Test Cycle Web Link",
13+
summary: "Create a new Web Link for a Test Cycle in Zephyr",
14+
readOnly: false,
15+
idempotent: false,
16+
inputSchema: CreateTestCycleWebLinkParams.and(
17+
CreateTestCycleWebLinkBody.partial(),
18+
),
19+
examples: [
20+
{
21+
description:
22+
"Create a link between the specified test cycle by Id '100001' and generic URL 'https://www.atlassian.com' with description 'Atlassian homepage'",
23+
parameters: {
24+
testCycleIdOrKey: "100001",
25+
url: "https://www.atlassian.com",
26+
description: "Atlassian homepage",
27+
},
28+
expectedOutput: "The newly created Web Link with its ID and self link",
29+
},
30+
{
31+
description:
32+
"Create a web link for test cycle 'SA-R15' pointing to url: 'https://atlassian.com' with description 'Atlassian homepage'",
33+
parameters: {
34+
testCycleIdOrKey: "SA-R15",
35+
url: "https://atlassian.com",
36+
description: "Documentation for pump specifications",
37+
},
38+
expectedOutput: "The newly created Web Link with its ID and self link",
39+
},
40+
{
41+
description:
42+
"Attach a documentation link 'https://docs.atlassian.com' to test cycle MM2-R15 for pump specifications",
43+
parameters: {
44+
testCycleIdOrKey: "10001",
45+
url: "https://docs.atlassian.com",
46+
description: "Documentation for pump specifications",
47+
},
48+
expectedOutput: "The newly created Web Link with its ID and self link",
49+
},
50+
],
51+
};
52+
handle: ToolCallback<ZodRawShape> = async (args) => {
53+
const { testCycleIdOrKey } = CreateTestCycleWebLinkParams.parse(args);
54+
const body = CreateTestCycleWebLinkBody.parse(args);
55+
const response = await this.client
56+
.getApiClient()
57+
.post(`/testcycles/${testCycleIdOrKey}/links/weblinks`, body);
58+
return {
59+
structuredContent: response,
60+
content: [],
61+
};
62+
};
63+
}

0 commit comments

Comments
 (0)