@smartbear/mcp 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ import { RemoteOpenAPIDocumentSchema, } from "./ai.js";
2
+ import yaml from "js-yaml";
3
+ // @ts-expect-error missing type declarations
4
+ import Swagger from "swagger-client";
5
+ /**
6
+ * Resolve the OpenAPI specification from the provided input.
7
+ *
8
+ * @param remoteOpenAPIDocument The remote OpenAPI document to resolve.
9
+ * @returns The resolved OpenAPI document.
10
+ * @throws Error if the resolution fails.
11
+ */
12
+ export async function resolveOpenAPISpec(remoteOpenAPIDocument) {
13
+ const openAPISchema = RemoteOpenAPIDocumentSchema.safeParse(remoteOpenAPIDocument);
14
+ if (openAPISchema.error || !remoteOpenAPIDocument) {
15
+ throw new Error(`Invalid RemoteOpenAPIDocument: ${JSON.stringify(openAPISchema.error?.issues)}`);
16
+ }
17
+ const unresolvedSpec = await getRemoteSpecContents(openAPISchema.data);
18
+ const resolvedSpec = await Swagger.resolve({ spec: unresolvedSpec });
19
+ if (resolvedSpec.errors?.length) {
20
+ throw new Error(`Failed to resolve OpenAPI document: ${resolvedSpec.errors?.join(", ")}`);
21
+ }
22
+ return resolvedSpec.spec;
23
+ }
24
+ /**
25
+ * Fetch the contents of a remote OpenAPI document.
26
+ *
27
+ * @param openAPISchema The schema for the remote OpenAPI document.
28
+ * @returns A promise that resolves to a map of the OpenAPI document contents.
29
+ * @throws Error if the URL is not provided or the fetch fails.
30
+ */
31
+ export async function getRemoteSpecContents(openAPISchema) {
32
+ if (!openAPISchema.url) {
33
+ throw new Error("'url' must be provided.");
34
+ }
35
+ let headers = {};
36
+ if (openAPISchema.authToken) {
37
+ headers = {
38
+ Authorization: `${openAPISchema.authScheme ?? "Bearer"} ${openAPISchema.authToken}`,
39
+ };
40
+ }
41
+ const remoteSpec = await fetch(openAPISchema.url, {
42
+ headers,
43
+ method: "GET",
44
+ });
45
+ const specRawBody = await remoteSpec.text();
46
+ try {
47
+ return JSON.parse(specRawBody);
48
+ }
49
+ catch (jsonError) {
50
+ try {
51
+ return yaml.load(specRawBody);
52
+ }
53
+ catch (yamlError) {
54
+ throw new Error(`Unsupported Content-Type: ${remoteSpec.headers.get("Content-Type")} for remote OpenAPI document. Found following parse errors:-\nJSON parse error: ${jsonError}\nYAML parse error: ${yamlError}`);
55
+ }
56
+ }
57
+ }
58
+ /**
59
+ * Adds the OpenAPI specification to the input schema if a remote document is provided.
60
+ *
61
+ * @param inputSchema The input schema to modify.
62
+ * @returns The modified input schema with the OpenAPI specification added.
63
+ */
64
+ export async function addOpenAPISpecToSchema(inputSchema) {
65
+ if (inputSchema.remoteDocument) {
66
+ const resolvedSpec = await resolveOpenAPISpec(inputSchema.remoteDocument);
67
+ inputSchema.document = resolvedSpec;
68
+ }
69
+ return inputSchema;
70
+ }
@@ -1,5 +1,5 @@
1
- import { TOOLS } from "./client/tools.js";
2
1
  import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
2
+ import { TOOLS } from "./client/tools.js";
3
3
  // Tool definitions for PactFlow AI API client
4
4
  export class PactflowClient {
5
5
  name = "Contract Testing";
@@ -30,28 +30,42 @@ export class PactflowClient {
30
30
  this.clientType = clientType;
31
31
  }
32
32
  // PactFlow AI client methods
33
- async generate(body) {
33
+ /**
34
+ * Generate new Pact tests based on the provided input.
35
+ *
36
+ * @param toolInput The input data for the generation process.
37
+ * @returns The result of the generation process.
38
+ * @throws Error if the HTTP request fails or the operation times out.
39
+ */
40
+ async generate(toolInput) {
34
41
  // Submit the generation request
35
42
  const response = await fetch(`${this.aiBaseUrl}/generate`, {
36
43
  method: "POST",
37
44
  headers: this.headers,
38
- body: JSON.stringify(body),
45
+ body: JSON.stringify(toolInput),
39
46
  });
40
47
  if (!response.ok) {
41
- throw new Error(`HTTP error! status: ${response.status}`);
48
+ throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
42
49
  }
43
50
  const status_response = await response.json();
44
51
  return await this.pollForCompletion(status_response, "Generation");
45
52
  }
46
- async review(body) {
47
- // submit review request
53
+ /**
54
+ * Review the provided Pact tests and suggest improvements.
55
+ *
56
+ * @param toolInput The input data for the review process.
57
+ * @returns The result of the review process.
58
+ * @throws Error if the HTTP request fails or the operation times out.
59
+ */
60
+ async review(toolInput) {
61
+ // Submit review request
48
62
  const response = await fetch(`${this.aiBaseUrl}/review`, {
49
63
  method: "POST",
50
64
  headers: this.headers,
51
- body: JSON.stringify(body),
65
+ body: JSON.stringify(toolInput),
52
66
  });
53
67
  if (!response.ok) {
54
- throw new Error(`HTTP error! status: ${response.status}`);
68
+ throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
55
69
  }
56
70
  const status_response = await response.json();
57
71
  return await this.pollForCompletion(status_response, "Review Pacts");
@@ -108,10 +122,44 @@ export class PactflowClient {
108
122
  }
109
123
  return response.json();
110
124
  }
125
+ /**
126
+ * Checks if a given pacticipant version is safe to deploy
127
+ * to a specified environment.
128
+ *
129
+ * @param body - Input containing:
130
+ * - `pacticipant`: The name of the service (pacticipant).
131
+ * - `version`: The version of the pacticipant being evaluated for deployment.
132
+ * - `environment`: The target environment (e.g., staging, production).
133
+ * @returns CanIDeployResponse containing deployment decision and verification results.
134
+ * @throws Error if the request fails or returns a non-OK response.
135
+ */
136
+ async canIDeploy(body) {
137
+ const { pacticipant, version, environment } = body;
138
+ const queryParams = new URLSearchParams({
139
+ pacticipant,
140
+ version,
141
+ environment,
142
+ });
143
+ const url = `${this.baseUrl}/can-i-deploy?${queryParams.toString()}`;
144
+ try {
145
+ const response = await fetch(url, {
146
+ method: "GET",
147
+ headers: this.headers,
148
+ });
149
+ if (!response.ok) {
150
+ const errorText = await response.text().catch(() => "");
151
+ throw new Error(`Can-I-Deploy Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
152
+ }
153
+ return (await response.json());
154
+ }
155
+ catch (error) {
156
+ console.error("[CanIDeploy] Unexpected error:", error);
157
+ throw error;
158
+ }
159
+ }
111
160
  registerTools(register, _getInput) {
112
161
  for (const tool of TOOLS.filter(t => t.clients.includes(this.clientType))) {
113
- const { handler, clients, formatResponse, ...toolparams } = tool;
114
- console.log(clients);
162
+ const { handler, clients: _, formatResponse, ...toolparams } = tool; // eslint-disable-line @typescript-eslint/no-unused-vars
115
163
  register(toolparams, async (args, _extra) => {
116
164
  const handler_fn = this[handler];
117
165
  if (typeof handler_fn !== "function") {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@smartbear/mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for interacting SmartBear Products",
5
5
  "keywords": [
6
6
  "smartbear",
7
7
  "mcp",
8
- "insight-hub",
8
+ "bugsnag",
9
9
  "reflect",
10
10
  "api-hub",
11
11
  "pactflow"
@@ -44,10 +44,12 @@
44
44
  "@bugsnag/js": "^8.2.0",
45
45
  "@modelcontextprotocol/sdk": "^1.15.0",
46
46
  "node-cache": "^5.1.2",
47
+ "swagger-client": "^3.35.6",
47
48
  "zod": "^3"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@eslint/js": "^9.29.0",
52
+ "@types/js-yaml": "^4.0.9",
51
53
  "@types/node": "^22",
52
54
  "@vitest/coverage-v8": "^3.2.4",
53
55
  "eslint": "^9.29.0",
@@ -55,6 +57,7 @@
55
57
  "shx": "^0.3.4",
56
58
  "typescript": "^5.6.2",
57
59
  "typescript-eslint": "^8.34.1",
58
- "vitest": "^3.2.4"
60
+ "vitest": "^3.2.4",
61
+ "vitest-fetch-mock": "^0.4.5"
59
62
  }
60
63
  }
package/dist/package.json DELETED
@@ -1,60 +0,0 @@
1
- {
2
- "name": "@smartbear/mcp",
3
- "version": "0.4.0",
4
- "description": "MCP server for interacting SmartBear Products",
5
- "keywords": [
6
- "smartbear",
7
- "mcp",
8
- "insight-hub",
9
- "reflect",
10
- "api-hub",
11
- "pactflow"
12
- ],
13
- "homepage": "https://developer.smartbear.com/smartbear-mcp",
14
- "repository": {
15
- "type": "git",
16
- "url": "git@github.com:SmartBear/smartbear-mcp.git"
17
- },
18
- "license": "MIT",
19
- "type": "module",
20
- "bin": {
21
- "mcp": "dist/index.js"
22
- },
23
- "files": [
24
- "dist",
25
- "assets",
26
- "**/README.md"
27
- ],
28
- "config": {
29
- "mcpServerName": "SmartBear MCP Server"
30
- },
31
- "scripts": {
32
- "build": "tsc && shx chmod +x dist/*.js",
33
- "lint": "eslint . --ext .ts",
34
- "prepare": "npm run build",
35
- "watch": "tsc --watch",
36
- "test": "vitest",
37
- "test:watch": "vitest --watch",
38
- "test:coverage": "vitest --coverage",
39
- "test:coverage:ci": "vitest --coverage --reporter=verbose",
40
- "test:run": "vitest run",
41
- "coverage:check": "vitest --coverage --reporter=verbose --config vitest.config.coverage.ts"
42
- },
43
- "dependencies": {
44
- "@bugsnag/js": "^8.2.0",
45
- "@modelcontextprotocol/sdk": "^1.15.0",
46
- "node-cache": "^5.1.2",
47
- "zod": "^3"
48
- },
49
- "devDependencies": {
50
- "@eslint/js": "^9.29.0",
51
- "@types/node": "^22",
52
- "@vitest/coverage-v8": "^3.2.4",
53
- "eslint": "^9.29.0",
54
- "globals": "^16.2.0",
55
- "shx": "^0.3.4",
56
- "typescript": "^5.6.2",
57
- "typescript-eslint": "^8.34.1",
58
- "vitest": "^3.2.4"
59
- }
60
- }
@@ -1,319 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { SmartBearMcpServer } from "../../../common/server.js";
3
- import z from "zod";
4
- import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5
- import Bugsnag from "../../../common/bugsnag.js";
6
- // Mock Bugsnag
7
- vi.mock("../../../common/bugsnag.js", () => ({
8
- default: {
9
- notify: vi.fn(),
10
- },
11
- }));
12
- describe("SmartBearMcpServer", () => {
13
- let server;
14
- let superRegisterToolMock;
15
- let superRegisterResourceMock;
16
- beforeEach(() => {
17
- server = new SmartBearMcpServer();
18
- // This approach is required to mock the super call - other techniques result in mocking the actual server
19
- superRegisterToolMock = vi
20
- .spyOn(Object.getPrototypeOf(Object.getPrototypeOf(server)), "registerTool")
21
- .mockImplementation(vi.fn());
22
- superRegisterResourceMock = vi
23
- .spyOn(Object.getPrototypeOf(Object.getPrototypeOf(server)), "registerResource")
24
- .mockImplementation(vi.fn());
25
- server.server.elicitInput = vi.fn().mockResolvedValue("mocked input");
26
- // Reset the Bugsnag mock
27
- vi.mocked(Bugsnag.notify).mockClear();
28
- });
29
- describe("getDescription tests", () => {
30
- it("should return the correct description", () => {
31
- const zSchema = z.object({
32
- examples: z.enum(["test_1", "test_2"]),
33
- constraints: z.enum(["test_1", "test_2"]),
34
- });
35
- const toolparams = {
36
- title: "Test Tool",
37
- summary: "A test tool",
38
- zodSchema: zSchema,
39
- };
40
- const description = server["getDescription"](toolparams);
41
- expect(description).toBe(`A test tool
42
-
43
- **Parameters:**
44
- - examples (enum) *required* (e.g. test_1, test_2)
45
- - constraints (enum) *required*
46
- - test_1
47
- - test_2`);
48
- });
49
- });
50
- describe("addClient", () => {
51
- let mockClient;
52
- beforeEach(() => {
53
- mockClient = {
54
- name: "Test Product",
55
- prefix: "test_product",
56
- registerTools: vi.fn(),
57
- registerResources: vi.fn(),
58
- };
59
- });
60
- it("should register tools when client provides them", async () => {
61
- server.addClient(mockClient);
62
- // The server should call the client's registerTools function
63
- expect(mockClient.registerTools).toHaveBeenCalledWith(expect.any(Function), expect.any(Function));
64
- // Get the register function passed from the server and execute it with test tool details
65
- const registerFn = mockClient.registerTools.mock.calls[0][0];
66
- const registerCbMock = vi.fn();
67
- registerFn({
68
- title: "Test Tool",
69
- summary: "A test tool",
70
- parameters: [
71
- {
72
- name: "p1",
73
- type: z.string(),
74
- required: true,
75
- description: "The input for the tool",
76
- },
77
- ],
78
- }, registerCbMock);
79
- expect(superRegisterToolMock).toHaveBeenCalledOnce();
80
- // Assert some of the details
81
- const registerToolParams = superRegisterToolMock.mock.calls[0];
82
- expect(registerToolParams[0]).toBe("test_product_test_tool");
83
- expect(registerToolParams[1]["title"]).toBe("Test Product: Test Tool");
84
- expect(registerToolParams[1]["description"]).toBe("A test tool\n\n" +
85
- "**Parameters:**\n" +
86
- "- p1 (string) *required*: The input for the tool");
87
- expect(registerToolParams[1]["inputSchema"]["p1"].toString()).toBe(z.string().describe("The input for the tool").toString());
88
- expect(registerToolParams[1]["annotations"]).toEqual({
89
- title: "Test Product: Test Tool",
90
- readOnlyHint: true,
91
- destructiveHint: false,
92
- idempotentHint: true,
93
- openWorldHint: false,
94
- });
95
- // Get the wrapper function that will execute the tool and call it
96
- registerToolParams[2]();
97
- expect(registerCbMock).toHaveBeenCalledOnce();
98
- expect(vi.mocked(Bugsnag.notify)).not.toHaveBeenCalled();
99
- });
100
- it("should register tools with complex parameters", async () => {
101
- server.addClient(mockClient);
102
- // Get the register function passed from the server and execute it with test tool details
103
- const registerFn = mockClient.registerTools.mock.calls[0][0];
104
- registerFn({
105
- title: "Test Tool",
106
- summary: "A test tool",
107
- parameters: [
108
- {
109
- name: "p1",
110
- type: z.string(),
111
- required: true,
112
- description: "The input for the tool",
113
- examples: ["example1", "example2"],
114
- constraints: ["constraint1", "constraint2"],
115
- },
116
- {
117
- name: "p2",
118
- type: z.number(),
119
- required: false,
120
- description: "The optional numeric input for the tool",
121
- },
122
- {
123
- name: "p3",
124
- type: z.boolean(),
125
- required: true,
126
- },
127
- {
128
- name: "p4",
129
- type: z.array(z.string()),
130
- required: true,
131
- },
132
- {
133
- name: "p5",
134
- type: z.object({
135
- key1: z.string(),
136
- key2: z.number(),
137
- }),
138
- required: true,
139
- },
140
- {
141
- name: "p6",
142
- type: z.enum(["value1", "value2", "value3"]),
143
- required: true,
144
- },
145
- {
146
- name: "p7",
147
- type: z.literal("value"),
148
- required: true,
149
- },
150
- {
151
- name: "p8",
152
- type: z.union([z.literal("value1"), z.literal("value2")]),
153
- required: true,
154
- },
155
- {
156
- name: "p9",
157
- type: z.any(),
158
- required: true,
159
- },
160
- ],
161
- purpose: "To test the tool registration process",
162
- useCases: ["Testing", "Development"],
163
- examples: [
164
- {
165
- description: "Example 1",
166
- parameters: {
167
- p1: "example1",
168
- p2: 42,
169
- },
170
- expectedOutput: "Expected output for example 1",
171
- },
172
- {
173
- description: "Example 2",
174
- parameters: {
175
- p1: "example2",
176
- p2: 24,
177
- },
178
- },
179
- ],
180
- hints: ["First hint", "Second hint"],
181
- outputFormat: "The output format",
182
- readOnly: true,
183
- destructive: true,
184
- idempotent: true,
185
- openWorld: true,
186
- }, vi.fn());
187
- // Assert some of the details
188
- const registerToolParams = superRegisterToolMock.mock.calls[0];
189
- expect(registerToolParams[0]).toBe("test_product_test_tool");
190
- expect(registerToolParams[1]["title"]).toBe("Test Product: Test Tool");
191
- expect(registerToolParams[1]["description"]).toBe("A test tool\n\n" +
192
- "**Parameters:**\n" +
193
- "- p1 (string) *required*: The input for the tool (e.g. example1, example2)\n" +
194
- " - constraint1\n" +
195
- " - constraint2\n" +
196
- "- p2 (number): The optional numeric input for the tool\n" +
197
- "- p3 (boolean) *required*\n" +
198
- "- p4 (array) *required*\n" +
199
- "- p5 (object) *required*\n" +
200
- "- p6 (enum) *required*\n" +
201
- "- p7 (literal) *required*\n" +
202
- "- p8 (union) *required*\n" +
203
- "- p9 (any) *required*\n\n" +
204
- "**Output Format:** The output format\n\n" +
205
- "**Use Cases:** 1. Testing 2. Development\n\n" +
206
- "**Examples:**\n" +
207
- "1. Example 1\n" +
208
- "```json\n" +
209
- "{\n" +
210
- ' "p1": "example1",\n' +
211
- ' "p2": 42\n' +
212
- "}\n" +
213
- "```\n" +
214
- "Expected Output: Expected output for example 1\n\n" +
215
- "2. Example 2\n" +
216
- "```json\n" +
217
- "{\n" +
218
- ' "p1": "example2",\n' +
219
- ' "p2": 24\n' +
220
- "}\n" +
221
- "```\n\n" +
222
- "**Hints:** 1. First hint 2. Second hint");
223
- expect(registerToolParams[1]["inputSchema"]["p1"].toString()).toBe(z.string().describe("The input for the tool").toString());
224
- expect(registerToolParams[1]["annotations"]).toEqual({
225
- title: "Test Product: Test Tool",
226
- readOnlyHint: true,
227
- destructiveHint: true,
228
- idempotentHint: true,
229
- openWorldHint: true,
230
- });
231
- });
232
- it("should handle errors when registering tools", async () => {
233
- server.addClient(mockClient);
234
- // Get the register function passed from the server and execute it with test tool details
235
- const registerFn = mockClient.registerTools.mock.calls[0][0];
236
- const registerCbMock = vi.fn();
237
- registerFn({
238
- title: "Test Tool",
239
- summary: "A test tool",
240
- parameters: [],
241
- }, registerCbMock);
242
- // Make the callback throw an error to test error handling
243
- registerCbMock.mockImplementation(() => {
244
- throw new Error("Test error from registerCbMock");
245
- });
246
- // Get the wrapper function that will execute the tool and call it
247
- const registerToolParams = superRegisterToolMock.mock.calls[0];
248
- await expect(registerToolParams[2]()).rejects.toThrow("Test error from registerCbMock");
249
- expect(registerCbMock).toHaveBeenCalledOnce();
250
- expect(vi.mocked(Bugsnag.notify)).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
251
- message: "Test error from registerCbMock",
252
- }));
253
- });
254
- it("should elicit input when client requires it", async () => {
255
- server.addClient(mockClient);
256
- // The server should call the client's registerTools function
257
- expect(mockClient.registerTools).toHaveBeenCalledWith(expect.any(Function), expect.any(Function));
258
- // Get the register function passed from the server and execute it with test tool details
259
- const getInputFn = mockClient.registerTools.mock.calls[0][1];
260
- const params = vi.mockObject({});
261
- const options = vi.mockObject({});
262
- getInputFn(params, options);
263
- expect(server.server.elicitInput).toHaveBeenCalledExactlyOnceWith(params, options);
264
- });
265
- it("should register resources when client provides them", async () => {
266
- const mockClient = {
267
- name: "Test Product",
268
- prefix: "test_product",
269
- registerTools: vi.fn(),
270
- registerResources: vi.fn(),
271
- };
272
- server.addClient(mockClient);
273
- // The server should call the client's registerResources function
274
- expect(mockClient.registerResources).toHaveBeenCalledWith(expect.any(Function));
275
- // Get the register function passed from the server and execute it with test resource details
276
- const registerFn = mockClient.registerResources.mock.calls[0][0];
277
- const registerCbMock = vi.fn();
278
- registerFn("test_resource", "{identifier}", registerCbMock);
279
- expect(superRegisterResourceMock).toHaveBeenCalledExactlyOnceWith(expect.any(String), expect.any(ResourceTemplate), expect.any(Object), expect.any(Function));
280
- // Assert some of the details
281
- const registerResourceParams = superRegisterResourceMock.mock.calls[0];
282
- expect(registerResourceParams[0]).toBe("test_resource");
283
- expect(registerResourceParams[1].uriTemplate.template).toBe("test_product://test_resource/{identifier}");
284
- // Get the wrapper function that will execute the tool and call it
285
- registerResourceParams[3]();
286
- expect(registerCbMock).toHaveBeenCalledOnce();
287
- expect(vi.mocked(Bugsnag.notify)).not.toHaveBeenCalled();
288
- });
289
- it("should not register resources when client does not provide them", async () => {
290
- const mockClient = {
291
- name: "Test Product",
292
- prefix: "test_product",
293
- registerTools: vi.fn(),
294
- registerResources: undefined,
295
- };
296
- server.addClient(mockClient);
297
- // It should not crash with undefined registerResources function
298
- expect(vi.mocked(Bugsnag.notify)).not.toHaveBeenCalled();
299
- });
300
- it("should handle errors when registering resources", async () => {
301
- server.addClient(mockClient);
302
- // Get the register function passed from the server and execute it with test resource details
303
- const registerFn = mockClient.registerResources.mock.calls[0][0];
304
- const registerCbMock = vi.fn();
305
- registerFn("test_resource", "{identifier}", registerCbMock);
306
- // Make the callback throw an error to test error handling
307
- registerCbMock.mockImplementation(() => {
308
- throw new Error("Test error from registerCbMock");
309
- });
310
- // Get the wrapper function that will execute the resource and call it
311
- const registerResourceParams = superRegisterResourceMock.mock.calls[0];
312
- await expect(registerResourceParams[3]()).rejects.toThrow("Test error from registerCbMock");
313
- expect(registerCbMock).toHaveBeenCalledOnce();
314
- expect(vi.mocked(Bugsnag.notify)).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
315
- message: "Test error from registerCbMock",
316
- }));
317
- });
318
- });
319
- });
@@ -1,31 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { pickFields, pickFieldsFromArray } from '../../../insight-hub/client/api/base.js';
3
- describe('API Utilities', () => {
4
- describe('pickFields', () => {
5
- it('should pick only specified fields from object', () => {
6
- const input = { id: '1', name: 'Test', secret: 'hidden', extra: 'data' };
7
- const result = pickFields(input, ['id', 'name']);
8
- expect(result).toEqual({ id: '1', name: 'Test' });
9
- expect(result).not.toHaveProperty('secret');
10
- expect(result).not.toHaveProperty('extra');
11
- });
12
- it('should handle missing fields gracefully', () => {
13
- const input = { id: '1' };
14
- const result = pickFields(input, ['id', 'name', 'missing']);
15
- expect(result).toEqual({ id: '1' });
16
- });
17
- });
18
- describe('pickFieldsFromArray', () => {
19
- it('should pick fields from array of objects', () => {
20
- const input = [
21
- { id: '1', name: 'Test1', secret: 'hidden1' },
22
- { id: '2', name: 'Test2', secret: 'hidden2' }
23
- ];
24
- const result = pickFieldsFromArray(input, ['id', 'name']);
25
- expect(result).toEqual([
26
- { id: '1', name: 'Test1' },
27
- { id: '2', name: 'Test2' }
28
- ]);
29
- });
30
- });
31
- });