mcp-openapi-schema-explorer 1.0.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.
- package/.devcontainer/devcontainer.json +24 -0
- package/.github/dependabot.yml +13 -0
- package/.github/workflows/ci.yml +111 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +12 -0
- package/.releaserc.json +23 -0
- package/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +67 -0
- package/Dockerfile +3 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +19 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/handlers/component-detail-handler.d.ts +14 -0
- package/dist/src/handlers/component-detail-handler.js +87 -0
- package/dist/src/handlers/component-detail-handler.js.map +1 -0
- package/dist/src/handlers/component-map-handler.d.ts +14 -0
- package/dist/src/handlers/component-map-handler.js +63 -0
- package/dist/src/handlers/component-map-handler.js.map +1 -0
- package/dist/src/handlers/handler-utils.d.ts +69 -0
- package/dist/src/handlers/handler-utils.js +180 -0
- package/dist/src/handlers/handler-utils.js.map +1 -0
- package/dist/src/handlers/operation-handler.d.ts +14 -0
- package/dist/src/handlers/operation-handler.js +86 -0
- package/dist/src/handlers/operation-handler.js.map +1 -0
- package/dist/src/handlers/path-item-handler.d.ts +14 -0
- package/dist/src/handlers/path-item-handler.js +66 -0
- package/dist/src/handlers/path-item-handler.js.map +1 -0
- package/dist/src/handlers/top-level-field-handler.d.ts +14 -0
- package/dist/src/handlers/top-level-field-handler.js +72 -0
- package/dist/src/handlers/top-level-field-handler.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +177 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/rendering/components.d.ts +67 -0
- package/dist/src/rendering/components.js +177 -0
- package/dist/src/rendering/components.js.map +1 -0
- package/dist/src/rendering/document.d.ts +36 -0
- package/dist/src/rendering/document.js +147 -0
- package/dist/src/rendering/document.js.map +1 -0
- package/dist/src/rendering/path-item.d.ts +45 -0
- package/dist/src/rendering/path-item.js +141 -0
- package/dist/src/rendering/path-item.js.map +1 -0
- package/dist/src/rendering/paths.d.ts +26 -0
- package/dist/src/rendering/paths.js +78 -0
- package/dist/src/rendering/paths.js.map +1 -0
- package/dist/src/rendering/types.d.ts +50 -0
- package/dist/src/rendering/types.js +12 -0
- package/dist/src/rendering/types.js.map +1 -0
- package/dist/src/rendering/utils.d.ts +31 -0
- package/dist/src/rendering/utils.js +79 -0
- package/dist/src/rendering/utils.js.map +1 -0
- package/dist/src/services/formatters.d.ts +36 -0
- package/dist/src/services/formatters.js +52 -0
- package/dist/src/services/formatters.js.map +1 -0
- package/dist/src/services/reference-transform.d.ts +27 -0
- package/dist/src/services/reference-transform.js +75 -0
- package/dist/src/services/reference-transform.js.map +1 -0
- package/dist/src/services/spec-loader.d.ts +27 -0
- package/dist/src/services/spec-loader.js +77 -0
- package/dist/src/services/spec-loader.js.map +1 -0
- package/dist/src/types.d.ts +11 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/uri-builder.d.ts +81 -0
- package/dist/src/utils/uri-builder.js +121 -0
- package/dist/src/utils/uri-builder.js.map +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +4 -0
- package/dist/src/version.js.map +1 -0
- package/eslint.config.js +88 -0
- package/jest.config.js +32 -0
- package/justfile +66 -0
- package/memory-bank/activeContext.md +139 -0
- package/memory-bank/productContext.md +39 -0
- package/memory-bank/progress.md +141 -0
- package/memory-bank/projectbrief.md +50 -0
- package/memory-bank/systemPatterns.md +224 -0
- package/memory-bank/techContext.md +131 -0
- package/package.json +76 -0
- package/scripts/generate-version.js +49 -0
- package/src/config.ts +33 -0
- package/src/handlers/component-detail-handler.ts +121 -0
- package/src/handlers/component-map-handler.ts +92 -0
- package/src/handlers/handler-utils.ts +230 -0
- package/src/handlers/operation-handler.ts +114 -0
- package/src/handlers/path-item-handler.ts +88 -0
- package/src/handlers/top-level-field-handler.ts +92 -0
- package/src/index.ts +222 -0
- package/src/rendering/components.ts +228 -0
- package/src/rendering/document.ts +167 -0
- package/src/rendering/path-item.ts +157 -0
- package/src/rendering/paths.ts +87 -0
- package/src/rendering/types.ts +63 -0
- package/src/rendering/utils.ts +107 -0
- package/src/services/formatters.ts +71 -0
- package/src/services/reference-transform.ts +105 -0
- package/src/services/spec-loader.ts +88 -0
- package/src/types.ts +17 -0
- package/src/utils/uri-builder.ts +134 -0
- package/src/version.ts +4 -0
- package/test/__tests__/e2e/format.test.ts +224 -0
- package/test/__tests__/e2e/resources.test.ts +369 -0
- package/test/__tests__/e2e/spec-loading.test.ts +172 -0
- package/test/__tests__/unit/config.test.ts +39 -0
- package/test/__tests__/unit/handlers/component-detail-handler.test.ts +241 -0
- package/test/__tests__/unit/handlers/component-map-handler.test.ts +187 -0
- package/test/__tests__/unit/handlers/handler-utils.test.ts +255 -0
- package/test/__tests__/unit/handlers/operation-handler.test.ts +202 -0
- package/test/__tests__/unit/handlers/path-item-handler.test.ts +153 -0
- package/test/__tests__/unit/handlers/top-level-field-handler.test.ts +182 -0
- package/test/__tests__/unit/rendering/components.test.ts +269 -0
- package/test/__tests__/unit/rendering/document.test.ts +172 -0
- package/test/__tests__/unit/rendering/path-item.test.ts +197 -0
- package/test/__tests__/unit/rendering/paths.test.ts +115 -0
- package/test/__tests__/unit/services/formatters.test.ts +109 -0
- package/test/__tests__/unit/services/reference-transform.test.ts +320 -0
- package/test/__tests__/unit/services/spec-loader.test.ts +214 -0
- package/test/__tests__/unit/utils/uri-builder.test.ts +103 -0
- package/test/fixtures/complex-endpoint.json +146 -0
- package/test/fixtures/empty-api.json +8 -0
- package/test/fixtures/multi-component-types.json +55 -0
- package/test/fixtures/paths-test.json +61 -0
- package/test/fixtures/sample-api.json +68 -0
- package/test/fixtures/sample-v2-api.json +39 -0
- package/test/setup.ts +32 -0
- package/test/utils/console-helpers.ts +48 -0
- package/test/utils/mcp-test-helpers.ts +66 -0
- package/test/utils/test-types.ts +54 -0
- package/tsconfig.json +25 -0
- package/tsconfig.test.json +5 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import { ComponentDetailHandler } from '../../../../src/handlers/component-detail-handler';
|
|
3
|
+
import { SpecLoaderService } from '../../../../src/types';
|
|
4
|
+
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
|
|
5
|
+
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
|
|
7
|
+
import { suppressExpectedConsoleError } from '../../../utils/console-helpers';
|
|
8
|
+
|
|
9
|
+
// Mocks
|
|
10
|
+
const mockGetTransformedSpec = jest.fn();
|
|
11
|
+
const mockSpecLoader: SpecLoaderService = {
|
|
12
|
+
getSpec: jest.fn(),
|
|
13
|
+
getTransformedSpec: mockGetTransformedSpec,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const mockFormatter: IFormatter = new JsonFormatter();
|
|
17
|
+
|
|
18
|
+
// Sample Data
|
|
19
|
+
const userSchema: OpenAPIV3.SchemaObject = {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: { name: { type: 'string' } },
|
|
22
|
+
};
|
|
23
|
+
const errorSchema: OpenAPIV3.SchemaObject = {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: { message: { type: 'string' } },
|
|
26
|
+
};
|
|
27
|
+
const limitParam: OpenAPIV3.ParameterObject = {
|
|
28
|
+
name: 'limit',
|
|
29
|
+
in: 'query',
|
|
30
|
+
schema: { type: 'integer' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const sampleSpec: OpenAPIV3.Document = {
|
|
34
|
+
openapi: '3.0.3',
|
|
35
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
36
|
+
paths: {},
|
|
37
|
+
components: {
|
|
38
|
+
schemas: {
|
|
39
|
+
User: userSchema,
|
|
40
|
+
Error: errorSchema,
|
|
41
|
+
},
|
|
42
|
+
parameters: {
|
|
43
|
+
limitParam: limitParam,
|
|
44
|
+
},
|
|
45
|
+
// No securitySchemes defined
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
describe('ComponentDetailHandler', () => {
|
|
50
|
+
let handler: ComponentDetailHandler;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
handler = new ComponentDetailHandler(mockSpecLoader, mockFormatter);
|
|
54
|
+
mockGetTransformedSpec.mockReset();
|
|
55
|
+
mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return the correct template', () => {
|
|
59
|
+
const template = handler.getTemplate();
|
|
60
|
+
expect(template).toBeInstanceOf(ResourceTemplate);
|
|
61
|
+
expect(template.uriTemplate.toString()).toBe('openapi://components/{type}/{name*}');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('handleRequest', () => {
|
|
65
|
+
const mockExtra = { signal: new AbortController().signal };
|
|
66
|
+
|
|
67
|
+
it('should return detail for a single valid component (schema)', async () => {
|
|
68
|
+
const variables: Variables = { type: 'schemas', name: 'User' }; // Use 'name' key
|
|
69
|
+
const uri = new URL('openapi://components/schemas/User');
|
|
70
|
+
|
|
71
|
+
const result = await handler.handleRequest(uri, variables, mockExtra);
|
|
72
|
+
|
|
73
|
+
expect(mockGetTransformedSpec).toHaveBeenCalledWith({
|
|
74
|
+
resourceType: 'schema',
|
|
75
|
+
format: 'openapi',
|
|
76
|
+
});
|
|
77
|
+
expect(result.contents).toHaveLength(1);
|
|
78
|
+
expect(result.contents[0]).toEqual({
|
|
79
|
+
uri: 'openapi://components/schemas/User',
|
|
80
|
+
mimeType: 'application/json',
|
|
81
|
+
text: JSON.stringify(userSchema, null, 2),
|
|
82
|
+
isError: false,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should return detail for a single valid component (parameter)', async () => {
|
|
87
|
+
const variables: Variables = { type: 'parameters', name: 'limitParam' };
|
|
88
|
+
const uri = new URL('openapi://components/parameters/limitParam');
|
|
89
|
+
|
|
90
|
+
const result = await handler.handleRequest(uri, variables, mockExtra);
|
|
91
|
+
|
|
92
|
+
expect(result.contents).toHaveLength(1);
|
|
93
|
+
expect(result.contents[0]).toEqual({
|
|
94
|
+
uri: 'openapi://components/parameters/limitParam',
|
|
95
|
+
mimeType: 'application/json',
|
|
96
|
+
text: JSON.stringify(limitParam, null, 2),
|
|
97
|
+
isError: false,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return details for multiple valid components (array input)', async () => {
|
|
102
|
+
const variables: Variables = { type: 'schemas', name: ['User', 'Error'] }; // Use 'name' key with array
|
|
103
|
+
const uri = new URL('openapi://components/schemas/User,Error'); // URI might not reflect array input
|
|
104
|
+
|
|
105
|
+
const result = await handler.handleRequest(uri, variables, mockExtra);
|
|
106
|
+
|
|
107
|
+
expect(result.contents).toHaveLength(2);
|
|
108
|
+
expect(result.contents).toContainEqual({
|
|
109
|
+
uri: 'openapi://components/schemas/User',
|
|
110
|
+
mimeType: 'application/json',
|
|
111
|
+
text: JSON.stringify(userSchema, null, 2),
|
|
112
|
+
isError: false,
|
|
113
|
+
});
|
|
114
|
+
expect(result.contents).toContainEqual({
|
|
115
|
+
uri: 'openapi://components/schemas/Error',
|
|
116
|
+
mimeType: 'application/json',
|
|
117
|
+
text: JSON.stringify(errorSchema, null, 2),
|
|
118
|
+
isError: false,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should return error for invalid component type', async () => {
|
|
123
|
+
const variables: Variables = { type: 'invalidType', name: 'SomeName' };
|
|
124
|
+
const uri = new URL('openapi://components/invalidType/SomeName');
|
|
125
|
+
const expectedLogMessage = /Invalid component type: invalidType/;
|
|
126
|
+
|
|
127
|
+
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
|
|
128
|
+
handler.handleRequest(uri, variables, mockExtra)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(result.contents).toHaveLength(1);
|
|
132
|
+
expect(result.contents[0]).toEqual({
|
|
133
|
+
uri: 'openapi://components/invalidType/SomeName',
|
|
134
|
+
mimeType: 'text/plain',
|
|
135
|
+
text: 'Invalid component type: invalidType',
|
|
136
|
+
isError: true,
|
|
137
|
+
});
|
|
138
|
+
expect(mockGetTransformedSpec).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return error for non-existent component type in spec', async () => {
|
|
142
|
+
const variables: Variables = { type: 'securitySchemes', name: 'apiKey' };
|
|
143
|
+
const uri = new URL('openapi://components/securitySchemes/apiKey');
|
|
144
|
+
const expectedLogMessage = /Component type "securitySchemes" not found/;
|
|
145
|
+
|
|
146
|
+
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
|
|
147
|
+
handler.handleRequest(uri, variables, mockExtra)
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(result.contents).toHaveLength(1);
|
|
151
|
+
// Expect the specific error message from getValidatedComponentMap
|
|
152
|
+
expect(result.contents[0]).toEqual({
|
|
153
|
+
uri: 'openapi://components/securitySchemes/apiKey',
|
|
154
|
+
mimeType: 'text/plain',
|
|
155
|
+
text: 'Component type "securitySchemes" not found in the specification. Available types: schemas, parameters',
|
|
156
|
+
isError: true,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should return error for non-existent component name', async () => {
|
|
161
|
+
const variables: Variables = { type: 'schemas', name: 'NonExistent' };
|
|
162
|
+
const uri = new URL('openapi://components/schemas/NonExistent');
|
|
163
|
+
const expectedLogMessage = /None of the requested names \(NonExistent\) are valid/;
|
|
164
|
+
|
|
165
|
+
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
|
|
166
|
+
handler.handleRequest(uri, variables, mockExtra)
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(result.contents).toHaveLength(1);
|
|
170
|
+
// Expect the specific error message from getValidatedComponentDetails
|
|
171
|
+
expect(result.contents[0]).toEqual({
|
|
172
|
+
uri: 'openapi://components/schemas/NonExistent',
|
|
173
|
+
mimeType: 'text/plain',
|
|
174
|
+
// Expect sorted names: Error, User
|
|
175
|
+
text: 'None of the requested names (NonExistent) are valid for component type "schemas". Available names: Error, User',
|
|
176
|
+
isError: true,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Remove test for mix of valid/invalid names, as getValidatedComponentDetails throws now
|
|
181
|
+
// it('should handle mix of valid and invalid component names', async () => { ... });
|
|
182
|
+
|
|
183
|
+
it('should handle empty name array', async () => {
|
|
184
|
+
const variables: Variables = { type: 'schemas', name: [] };
|
|
185
|
+
const uri = new URL('openapi://components/schemas/');
|
|
186
|
+
const expectedLogMessage = /No valid component name specified/;
|
|
187
|
+
|
|
188
|
+
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
|
|
189
|
+
handler.handleRequest(uri, variables, mockExtra)
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(result.contents).toHaveLength(1);
|
|
193
|
+
expect(result.contents[0]).toEqual({
|
|
194
|
+
uri: 'openapi://components/schemas/',
|
|
195
|
+
mimeType: 'text/plain',
|
|
196
|
+
text: 'No valid component name specified.',
|
|
197
|
+
isError: true,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should handle spec loading errors', async () => {
|
|
202
|
+
const error = new Error('Spec load failed');
|
|
203
|
+
mockGetTransformedSpec.mockRejectedValue(error);
|
|
204
|
+
const variables: Variables = { type: 'schemas', name: 'User' };
|
|
205
|
+
const uri = new URL('openapi://components/schemas/User');
|
|
206
|
+
const expectedLogMessage = /Spec load failed/;
|
|
207
|
+
|
|
208
|
+
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
|
|
209
|
+
handler.handleRequest(uri, variables, mockExtra)
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(result.contents).toHaveLength(1);
|
|
213
|
+
expect(result.contents[0]).toEqual({
|
|
214
|
+
uri: 'openapi://components/schemas/User',
|
|
215
|
+
mimeType: 'text/plain',
|
|
216
|
+
text: 'Spec load failed',
|
|
217
|
+
isError: true,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle non-OpenAPI v3 spec', async () => {
|
|
222
|
+
const invalidSpec = { swagger: '2.0', info: {} };
|
|
223
|
+
mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
|
|
224
|
+
const variables: Variables = { type: 'schemas', name: 'User' };
|
|
225
|
+
const uri = new URL('openapi://components/schemas/User');
|
|
226
|
+
const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
|
|
227
|
+
|
|
228
|
+
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
|
|
229
|
+
handler.handleRequest(uri, variables, mockExtra)
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(result.contents).toHaveLength(1);
|
|
233
|
+
expect(result.contents[0]).toEqual({
|
|
234
|
+
uri: 'openapi://components/schemas/User',
|
|
235
|
+
mimeType: 'text/plain',
|
|
236
|
+
text: 'Only OpenAPI v3 specifications are supported',
|
|
237
|
+
isError: true,
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import { ComponentMapHandler } from '../../../../src/handlers/component-map-handler';
|
|
3
|
+
import { SpecLoaderService } from '../../../../src/types';
|
|
4
|
+
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
|
|
5
|
+
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js';
|
|
7
|
+
import { suppressExpectedConsoleError } from '../../../utils/console-helpers';
|
|
8
|
+
|
|
9
|
+
// Mocks
|
|
10
|
+
const mockGetTransformedSpec = jest.fn();
|
|
11
|
+
const mockSpecLoader: SpecLoaderService = {
|
|
12
|
+
getSpec: jest.fn(),
|
|
13
|
+
getTransformedSpec: mockGetTransformedSpec,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const mockFormatter: IFormatter = new JsonFormatter(); // Needed for context
|
|
17
|
+
|
|
18
|
+
// Sample Data
|
|
19
|
+
const sampleSpec: OpenAPIV3.Document = {
|
|
20
|
+
openapi: '3.0.3',
|
|
21
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
22
|
+
paths: {},
|
|
23
|
+
components: {
|
|
24
|
+
schemas: {
|
|
25
|
+
User: { type: 'object', properties: { name: { type: 'string' } } },
|
|
26
|
+
Error: { type: 'object', properties: { message: { type: 'string' } } },
|
|
27
|
+
},
|
|
28
|
+
parameters: {
|
|
29
|
+
limitParam: { name: 'limit', in: 'query', schema: { type: 'integer' } },
|
|
30
|
+
},
|
|
31
|
+
examples: {}, // Empty type
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('ComponentMapHandler', () => {
|
|
36
|
+
let handler: ComponentMapHandler;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
handler = new ComponentMapHandler(mockSpecLoader, mockFormatter);
|
|
40
|
+
mockGetTransformedSpec.mockReset();
|
|
41
|
+
mockGetTransformedSpec.mockResolvedValue(sampleSpec); // Default mock
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return the correct template', () => {
|
|
45
|
+
const template = handler.getTemplate();
|
|
46
|
+
expect(template).toBeInstanceOf(ResourceTemplate);
|
|
47
|
+
expect(template.uriTemplate.toString()).toBe('openapi://components/{type}');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('handleRequest (List Component Names)', () => {
|
|
51
|
+
const mockExtra = { signal: new AbortController().signal };
|
|
52
|
+
|
|
53
|
+
it('should list names for a valid component type (schemas)', async () => {
|
|
54
|
+
const variables: Variables = { type: 'schemas' };
|
|
55
|
+
const uri = new URL('openapi://components/schemas');
|
|
56
|
+
|
|
57
|
+
const result = await handler.handleRequest(uri, variables, mockExtra);
|
|
58
|
+
|
|
59
|
+
expect(mockGetTransformedSpec).toHaveBeenCalledWith({
|
|
60
|
+
resourceType: 'schema',
|
|
61
|
+
format: 'openapi',
|
|
62
|
+
});
|
|
63
|
+
expect(result.contents).toHaveLength(1);
|
|
64
|
+
expect(result.contents[0]).toMatchObject({
|
|
65
|
+
uri: 'openapi://components/schemas',
|
|
66
|
+
mimeType: 'text/plain',
|
|
67
|
+
isError: false,
|
|
68
|
+
});
|
|
69
|
+
expect(result.contents[0].text).toContain('Available schemas:');
|
|
70
|
+
expect(result.contents[0].text).toMatch(/-\sError\n/); // Sorted
|
|
71
|
+
expect(result.contents[0].text).toMatch(/-\sUser\n/);
|
|
72
|
+
expect(result.contents[0].text).toContain("Hint: Use 'openapi://components/schemas/{name}'");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should list names for another valid type (parameters)', async () => {
|
|
76
|
+
const variables: Variables = { type: 'parameters' };
|
|
77
|
+
const uri = new URL('openapi://components/parameters');
|
|
78
|
+
|
|
79
|
+
const result = await handler.handleRequest(uri, variables, mockExtra);
|
|
80
|
+
|
|
81
|
+
expect(result.contents).toHaveLength(1);
|
|
82
|
+
expect(result.contents[0]).toMatchObject({
|
|
83
|
+
uri: 'openapi://components/parameters',
|
|
84
|
+
mimeType: 'text/plain',
|
|
85
|
+
isError: false,
|
|
86
|
+
});
|
|
87
|
+
expect(result.contents[0].text).toContain('Available parameters:');
|
|
88
|
+
expect(result.contents[0].text).toMatch(/-\slimitParam\n/);
|
|
89
|
+
expect(result.contents[0].text).toContain(
|
|
90
|
+
"Hint: Use 'openapi://components/parameters/{name}'"
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle component type with no components defined (examples)', async () => {
|
|
95
|
+
const variables: Variables = { type: 'examples' };
|
|
96
|
+
const uri = new URL('openapi://components/examples');
|
|
97
|
+
|
|
98
|
+
const result = await handler.handleRequest(uri, variables, mockExtra);
|
|
99
|
+
|
|
100
|
+
expect(result.contents).toHaveLength(1);
|
|
101
|
+
expect(result.contents[0]).toEqual({
|
|
102
|
+
uri: 'openapi://components/examples',
|
|
103
|
+
mimeType: 'text/plain',
|
|
104
|
+
text: 'No components of type "examples" found.',
|
|
105
|
+
isError: true, // Treat as error because map exists but is empty
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle component type not present in spec (securitySchemes)', async () => {
|
|
110
|
+
const variables: Variables = { type: 'securitySchemes' };
|
|
111
|
+
const uri = new URL('openapi://components/securitySchemes');
|
|
112
|
+
const expectedLogMessage = /Component type "securitySchemes" not found/;
|
|
113
|
+
|
|
114
|
+
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
|
|
115
|
+
handler.handleRequest(uri, variables, mockExtra)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(result.contents).toHaveLength(1);
|
|
119
|
+
// Expect the specific error message from getValidatedComponentMap
|
|
120
|
+
expect(result.contents[0]).toEqual({
|
|
121
|
+
uri: 'openapi://components/securitySchemes',
|
|
122
|
+
mimeType: 'text/plain',
|
|
123
|
+
text: 'Component type "securitySchemes" not found in the specification. Available types: schemas, parameters, examples',
|
|
124
|
+
isError: true,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should return error for invalid component type', async () => {
|
|
129
|
+
const variables: Variables = { type: 'invalidType' };
|
|
130
|
+
const uri = new URL('openapi://components/invalidType');
|
|
131
|
+
const expectedLogMessage = /Invalid component type: invalidType/;
|
|
132
|
+
|
|
133
|
+
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
|
|
134
|
+
handler.handleRequest(uri, variables, mockExtra)
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(result.contents).toHaveLength(1);
|
|
138
|
+
expect(result.contents[0]).toEqual({
|
|
139
|
+
uri: 'openapi://components/invalidType',
|
|
140
|
+
mimeType: 'text/plain',
|
|
141
|
+
text: 'Invalid component type: invalidType',
|
|
142
|
+
isError: true,
|
|
143
|
+
});
|
|
144
|
+
expect(mockGetTransformedSpec).not.toHaveBeenCalled(); // Should fail before loading spec
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle spec loading errors', async () => {
|
|
148
|
+
const error = new Error('Spec load failed');
|
|
149
|
+
mockGetTransformedSpec.mockRejectedValue(error);
|
|
150
|
+
const variables: Variables = { type: 'schemas' };
|
|
151
|
+
const uri = new URL('openapi://components/schemas');
|
|
152
|
+
const expectedLogMessage = /Spec load failed/;
|
|
153
|
+
|
|
154
|
+
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
|
|
155
|
+
handler.handleRequest(uri, variables, mockExtra)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result.contents).toHaveLength(1);
|
|
159
|
+
expect(result.contents[0]).toEqual({
|
|
160
|
+
uri: 'openapi://components/schemas',
|
|
161
|
+
mimeType: 'text/plain',
|
|
162
|
+
text: 'Spec load failed',
|
|
163
|
+
isError: true,
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should handle non-OpenAPI v3 spec', async () => {
|
|
168
|
+
const invalidSpec = { swagger: '2.0', info: {} };
|
|
169
|
+
mockGetTransformedSpec.mockResolvedValue(invalidSpec as unknown as OpenAPIV3.Document);
|
|
170
|
+
const variables: Variables = { type: 'schemas' };
|
|
171
|
+
const uri = new URL('openapi://components/schemas');
|
|
172
|
+
const expectedLogMessage = /Only OpenAPI v3 specifications are supported/;
|
|
173
|
+
|
|
174
|
+
const result = await suppressExpectedConsoleError(expectedLogMessage, () =>
|
|
175
|
+
handler.handleRequest(uri, variables, mockExtra)
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(result.contents).toHaveLength(1);
|
|
179
|
+
expect(result.contents[0]).toEqual({
|
|
180
|
+
uri: 'openapi://components/schemas',
|
|
181
|
+
mimeType: 'text/plain',
|
|
182
|
+
text: 'Only OpenAPI v3 specifications are supported',
|
|
183
|
+
isError: true,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import {
|
|
3
|
+
getValidatedPathItem,
|
|
4
|
+
getValidatedOperations,
|
|
5
|
+
getValidatedComponentMap,
|
|
6
|
+
getValidatedComponentDetails,
|
|
7
|
+
// We might also test formatResults and isOpenAPIV3 if needed, but focus on new helpers first
|
|
8
|
+
} from '../../../../src/handlers/handler-utils.js'; // Adjust path as needed
|
|
9
|
+
|
|
10
|
+
// --- Mocks and Fixtures ---
|
|
11
|
+
|
|
12
|
+
const mockSpec: OpenAPIV3.Document = {
|
|
13
|
+
openapi: '3.0.0',
|
|
14
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
15
|
+
paths: {
|
|
16
|
+
'/users': {
|
|
17
|
+
get: { responses: { '200': { description: 'OK' } } },
|
|
18
|
+
post: { responses: { '201': { description: 'Created' } } },
|
|
19
|
+
},
|
|
20
|
+
'/users/{id}': {
|
|
21
|
+
get: { responses: { '200': { description: 'OK' } } },
|
|
22
|
+
delete: { responses: { '204': { description: 'No Content' } } },
|
|
23
|
+
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
24
|
+
},
|
|
25
|
+
'/items': {
|
|
26
|
+
// Path item with no standard methods
|
|
27
|
+
description: 'Collection of items',
|
|
28
|
+
parameters: [],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
components: {
|
|
32
|
+
schemas: {
|
|
33
|
+
User: { type: 'object', properties: { id: { type: 'string' } } },
|
|
34
|
+
Error: { type: 'object', properties: { message: { type: 'string' } } },
|
|
35
|
+
},
|
|
36
|
+
responses: {
|
|
37
|
+
NotFound: { description: 'Resource not found' },
|
|
38
|
+
},
|
|
39
|
+
// Intentionally missing 'parameters' section for testing
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const mockSpecNoPaths: OpenAPIV3.Document = {
|
|
44
|
+
openapi: '3.0.0',
|
|
45
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
46
|
+
paths: {}, // Empty paths
|
|
47
|
+
components: {},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const mockSpecNoComponents: OpenAPIV3.Document = {
|
|
51
|
+
openapi: '3.0.0',
|
|
52
|
+
info: { title: 'Test API', version: '1.0.0' },
|
|
53
|
+
paths: { '/ping': { get: { responses: { '200': { description: 'OK' } } } } },
|
|
54
|
+
// No components section
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// --- Tests ---
|
|
58
|
+
|
|
59
|
+
describe('Handler Utils', () => {
|
|
60
|
+
// --- getValidatedPathItem ---
|
|
61
|
+
describe('getValidatedPathItem', () => {
|
|
62
|
+
it('should return the path item object for a valid path', () => {
|
|
63
|
+
const pathItem = getValidatedPathItem(mockSpec, '/users');
|
|
64
|
+
expect(pathItem).toBeDefined();
|
|
65
|
+
expect(pathItem).toHaveProperty('get');
|
|
66
|
+
expect(pathItem).toHaveProperty('post');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should return the path item object for a path with parameters', () => {
|
|
70
|
+
const pathItem = getValidatedPathItem(mockSpec, '/users/{id}');
|
|
71
|
+
expect(pathItem).toBeDefined();
|
|
72
|
+
expect(pathItem).toHaveProperty('get');
|
|
73
|
+
expect(pathItem).toHaveProperty('delete');
|
|
74
|
+
expect(pathItem).toHaveProperty('parameters');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should throw Error if path is not found', () => {
|
|
78
|
+
expect(() => getValidatedPathItem(mockSpec, '/nonexistent')).toThrow(
|
|
79
|
+
new Error('Path "/nonexistent" not found in the specification.')
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw Error if spec has no paths object', () => {
|
|
84
|
+
const specWithoutPaths = { ...mockSpec, paths: undefined };
|
|
85
|
+
// @ts-expect-error - Intentionally passing spec with undefined paths to test error handling
|
|
86
|
+
expect(() => getValidatedPathItem(specWithoutPaths, '/users')).toThrow(
|
|
87
|
+
new Error('Specification does not contain any paths.')
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should throw Error if spec has empty paths object', () => {
|
|
92
|
+
expect(() => getValidatedPathItem(mockSpecNoPaths, '/users')).toThrow(
|
|
93
|
+
new Error('Path "/users" not found in the specification.')
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// --- getValidatedOperations ---
|
|
99
|
+
describe('getValidatedOperations', () => {
|
|
100
|
+
const usersPathItem = mockSpec.paths['/users'] as OpenAPIV3.PathItemObject;
|
|
101
|
+
const userIdPathItem = mockSpec.paths['/users/{id}'] as OpenAPIV3.PathItemObject;
|
|
102
|
+
const itemsPathItem = mockSpec.paths['/items'] as OpenAPIV3.PathItemObject;
|
|
103
|
+
|
|
104
|
+
it('should return valid requested methods when all exist', () => {
|
|
105
|
+
const validMethods = getValidatedOperations(usersPathItem, ['get', 'post'], '/users');
|
|
106
|
+
expect(validMethods).toEqual(['get', 'post']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return valid requested methods when some exist', () => {
|
|
110
|
+
const validMethods = getValidatedOperations(usersPathItem, ['get', 'put', 'post'], '/users');
|
|
111
|
+
expect(validMethods).toEqual(['get', 'post']);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should return valid requested methods ignoring case', () => {
|
|
115
|
+
const validMethods = getValidatedOperations(usersPathItem, ['GET', 'POST'], '/users');
|
|
116
|
+
// Note: the helper expects lowercase input, but the internal map uses lowercase keys
|
|
117
|
+
expect(validMethods).toEqual(['GET', 'POST']); // It returns the original case of valid inputs
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return only the valid method when one exists', () => {
|
|
121
|
+
const validMethods = getValidatedOperations(
|
|
122
|
+
userIdPathItem,
|
|
123
|
+
['delete', 'patch'],
|
|
124
|
+
'/users/{id}'
|
|
125
|
+
);
|
|
126
|
+
expect(validMethods).toEqual(['delete']);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should throw Error if no requested methods are valid', () => {
|
|
130
|
+
expect(() => getValidatedOperations(usersPathItem, ['put', 'delete'], '/users')).toThrow(
|
|
131
|
+
new Error(
|
|
132
|
+
'None of the requested methods (put, delete) are valid for path "/users". Available methods: get, post'
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should throw Error if requested methods array is empty', () => {
|
|
138
|
+
// The calling handler should prevent this, but test the helper
|
|
139
|
+
expect(() => getValidatedOperations(usersPathItem, [], '/users')).toThrow(
|
|
140
|
+
new Error(
|
|
141
|
+
'None of the requested methods () are valid for path "/users". Available methods: get, post'
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should throw Error if path item has no valid methods', () => {
|
|
147
|
+
expect(() => getValidatedOperations(itemsPathItem, ['get'], '/items')).toThrow(
|
|
148
|
+
new Error(
|
|
149
|
+
'None of the requested methods (get) are valid for path "/items". Available methods: '
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// --- getValidatedComponentMap ---
|
|
156
|
+
describe('getValidatedComponentMap', () => {
|
|
157
|
+
it('should return the component map for a valid type', () => {
|
|
158
|
+
const schemasMap = getValidatedComponentMap(mockSpec, 'schemas');
|
|
159
|
+
expect(schemasMap).toBeDefined();
|
|
160
|
+
expect(schemasMap).toHaveProperty('User');
|
|
161
|
+
expect(schemasMap).toHaveProperty('Error');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should return the component map for another valid type', () => {
|
|
165
|
+
const responsesMap = getValidatedComponentMap(mockSpec, 'responses');
|
|
166
|
+
expect(responsesMap).toBeDefined();
|
|
167
|
+
expect(responsesMap).toHaveProperty('NotFound');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should throw Error if component type is not found', () => {
|
|
171
|
+
expect(() => getValidatedComponentMap(mockSpec, 'parameters')).toThrow(
|
|
172
|
+
new Error(
|
|
173
|
+
'Component type "parameters" not found in the specification. Available types: schemas, responses'
|
|
174
|
+
)
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should throw Error if spec has no components section', () => {
|
|
179
|
+
expect(() => getValidatedComponentMap(mockSpecNoComponents, 'schemas')).toThrow(
|
|
180
|
+
new Error('Specification does not contain a components section.')
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// --- getValidatedComponentDetails ---
|
|
186
|
+
describe('getValidatedComponentDetails', () => {
|
|
187
|
+
const schemasMap = mockSpec.components?.schemas as Record<string, OpenAPIV3.SchemaObject>;
|
|
188
|
+
const responsesMap = mockSpec.components?.responses as Record<string, OpenAPIV3.ResponseObject>;
|
|
189
|
+
const detailsMapSchemas = new Map(Object.entries(schemasMap));
|
|
190
|
+
const detailsMapResponses = new Map(Object.entries(responsesMap));
|
|
191
|
+
|
|
192
|
+
it('should return details for valid requested names', () => {
|
|
193
|
+
const validDetails = getValidatedComponentDetails(
|
|
194
|
+
detailsMapSchemas,
|
|
195
|
+
['User', 'Error'],
|
|
196
|
+
'schemas'
|
|
197
|
+
);
|
|
198
|
+
expect(validDetails).toHaveLength(2);
|
|
199
|
+
expect(validDetails[0].name).toBe('User');
|
|
200
|
+
expect(validDetails[0].detail).toEqual(schemasMap['User']);
|
|
201
|
+
expect(validDetails[1].name).toBe('Error');
|
|
202
|
+
expect(validDetails[1].detail).toEqual(schemasMap['Error']);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should return details for a single valid requested name', () => {
|
|
206
|
+
const validDetails = getValidatedComponentDetails(detailsMapSchemas, ['User'], 'schemas');
|
|
207
|
+
expect(validDetails).toHaveLength(1);
|
|
208
|
+
expect(validDetails[0].name).toBe('User');
|
|
209
|
+
expect(validDetails[0].detail).toEqual(schemasMap['User']);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should return only details for valid names when some are invalid', () => {
|
|
213
|
+
const validDetails = getValidatedComponentDetails(
|
|
214
|
+
detailsMapSchemas,
|
|
215
|
+
['User', 'NonExistent', 'Error'],
|
|
216
|
+
'schemas'
|
|
217
|
+
);
|
|
218
|
+
expect(validDetails).toHaveLength(2);
|
|
219
|
+
expect(validDetails[0].name).toBe('User');
|
|
220
|
+
expect(validDetails[1].name).toBe('Error');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should throw Error if no requested names are valid', () => {
|
|
224
|
+
expect(() =>
|
|
225
|
+
getValidatedComponentDetails(detailsMapSchemas, ['NonExistent1', 'NonExistent2'], 'schemas')
|
|
226
|
+
).toThrow(
|
|
227
|
+
new Error(
|
|
228
|
+
// Expect sorted names: Error, User
|
|
229
|
+
'None of the requested names (NonExistent1, NonExistent2) are valid for component type "schemas". Available names: Error, User'
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should throw Error if requested names array is empty', () => {
|
|
235
|
+
// The calling handler should prevent this, but test the helper
|
|
236
|
+
expect(() => getValidatedComponentDetails(detailsMapSchemas, [], 'schemas')).toThrow(
|
|
237
|
+
new Error(
|
|
238
|
+
// Expect sorted names: Error, User
|
|
239
|
+
'None of the requested names () are valid for component type "schemas". Available names: Error, User'
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should work for other component types (responses)', () => {
|
|
245
|
+
const validDetails = getValidatedComponentDetails(
|
|
246
|
+
detailsMapResponses,
|
|
247
|
+
['NotFound'],
|
|
248
|
+
'responses'
|
|
249
|
+
);
|
|
250
|
+
expect(validDetails).toHaveLength(1);
|
|
251
|
+
expect(validDetails[0].name).toBe('NotFound');
|
|
252
|
+
expect(validDetails[0].detail).toEqual(responsesMap['NotFound']);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|