@notionhq/notion-mcp-server 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/LICENSE +7 -0
- package/README.md +58 -0
- package/bin/cli.mjs +71 -0
- package/package.json +57 -0
- package/scripts/build-cli.js +30 -0
- package/scripts/notion-openapi.json +1032 -0
- package/scripts/start-server.ts +70 -0
- package/src/openapi-mcp-server/README.md +1 -0
- package/src/openapi-mcp-server/auth/index.ts +2 -0
- package/src/openapi-mcp-server/auth/template.ts +24 -0
- package/src/openapi-mcp-server/auth/types.ts +26 -0
- package/src/openapi-mcp-server/client/__tests__/http-client-upload.test.ts +205 -0
- package/src/openapi-mcp-server/client/__tests__/http-client.integration.test.ts +173 -0
- package/src/openapi-mcp-server/client/__tests__/http-client.test.ts +537 -0
- package/src/openapi-mcp-server/client/http-client.ts +192 -0
- package/src/openapi-mcp-server/index.ts +3 -0
- package/src/openapi-mcp-server/mcp/__tests__/proxy.test.ts +270 -0
- package/src/openapi-mcp-server/mcp/proxy.ts +170 -0
- package/src/openapi-mcp-server/openapi/__tests__/file-upload.test.ts +100 -0
- package/src/openapi-mcp-server/openapi/__tests__/parser-multipart.test.ts +602 -0
- package/src/openapi-mcp-server/openapi/__tests__/parser.test.ts +1448 -0
- package/src/openapi-mcp-server/openapi/file-upload.ts +40 -0
- package/src/openapi-mcp-server/openapi/parser.ts +519 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
import { OpenAPIV3 } from 'openapi-types'
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
7
|
+
import OpenAPISchemaValidator from 'openapi-schema-validator'
|
|
8
|
+
|
|
9
|
+
import { MCPProxy } from '../src/openapi-mcp-server/mcp/proxy'
|
|
10
|
+
|
|
11
|
+
export class ValidationError extends Error {
|
|
12
|
+
constructor(public errors: any[]) {
|
|
13
|
+
super('OpenAPI validation failed')
|
|
14
|
+
this.name = 'ValidationError'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function loadOpenApiSpec(specPath: string): Promise<OpenAPIV3.Document> {
|
|
19
|
+
let rawSpec: string
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
rawSpec = fs.readFileSync(path.resolve(process.cwd(), specPath), 'utf-8')
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Failed to read OpenAPI specification file:', (error as Error).message)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Parse and validate the spec
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(rawSpec)
|
|
31
|
+
const baseUrl = process.env.BASE_URL
|
|
32
|
+
|
|
33
|
+
if (baseUrl) {
|
|
34
|
+
parsed.servers[0].url = baseUrl
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return parsed as OpenAPIV3.Document
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof ValidationError) {
|
|
40
|
+
throw error
|
|
41
|
+
}
|
|
42
|
+
console.error('Failed to parse OpenAPI specification:', (error as Error).message)
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Main execution
|
|
48
|
+
export async function main(args: string[] = process.argv.slice(2)) {
|
|
49
|
+
const filename = fileURLToPath(import.meta.url)
|
|
50
|
+
const directory = path.dirname(filename)
|
|
51
|
+
const specPath = path.resolve(directory, '../scripts/notion-openapi.json')
|
|
52
|
+
const openApiSpec = await loadOpenApiSpec(specPath)
|
|
53
|
+
const proxy = new MCPProxy('OpenAPI Tools', openApiSpec)
|
|
54
|
+
|
|
55
|
+
return proxy.connect(new StdioServerTransport())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const shouldStart = process.argv[1].endsWith('notion-mcp-server')
|
|
59
|
+
// Only run main if this is the entry point
|
|
60
|
+
if (shouldStart) {
|
|
61
|
+
main().catch(error => {
|
|
62
|
+
if (error instanceof ValidationError) {
|
|
63
|
+
console.error('Invalid OpenAPI 3.1 specification:')
|
|
64
|
+
error.errors.forEach(err => console.error(err))
|
|
65
|
+
} else {
|
|
66
|
+
console.error('Error:', error.message)
|
|
67
|
+
}
|
|
68
|
+
process.exit(1)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Note: This is a fork from v1 of https://github.com/snaggle-ai/openapi-mcp-server. The library took a different direction with v2 which is not compatible with our development approach.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Mustache from 'mustache'
|
|
2
|
+
import { AuthTemplate, TemplateContext } from './types'
|
|
3
|
+
|
|
4
|
+
export function renderAuthTemplate(template: AuthTemplate, context: TemplateContext): AuthTemplate {
|
|
5
|
+
// Disable HTML escaping for URLs
|
|
6
|
+
Mustache.escape = (text) => text
|
|
7
|
+
|
|
8
|
+
// Render URL with template variables
|
|
9
|
+
const renderedUrl = Mustache.render(template.url, context)
|
|
10
|
+
|
|
11
|
+
// Create a new template object with rendered values
|
|
12
|
+
const renderedTemplate: AuthTemplate = {
|
|
13
|
+
...template,
|
|
14
|
+
url: renderedUrl,
|
|
15
|
+
headers: { ...template.headers }, // Create a new headers object to avoid modifying the original
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Render body if it exists
|
|
19
|
+
if (template.body) {
|
|
20
|
+
renderedTemplate.body = Mustache.render(template.body, context)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return renderedTemplate
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
2
|
+
|
|
3
|
+
export interface AuthTemplate {
|
|
4
|
+
url: string
|
|
5
|
+
method: HttpMethod
|
|
6
|
+
headers: Record<string, string>
|
|
7
|
+
body?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SecurityScheme {
|
|
11
|
+
[key: string]: {
|
|
12
|
+
tokenUrl?: string
|
|
13
|
+
[key: string]: any
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Server {
|
|
18
|
+
url: string
|
|
19
|
+
description?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TemplateContext {
|
|
23
|
+
securityScheme?: SecurityScheme
|
|
24
|
+
servers?: Server[]
|
|
25
|
+
args: Record<string, string>
|
|
26
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { HttpClient } from '../http-client'
|
|
3
|
+
import { OpenAPIV3 } from 'openapi-types'
|
|
4
|
+
import fs from 'fs'
|
|
5
|
+
import FormData from 'form-data'
|
|
6
|
+
|
|
7
|
+
vi.mock('fs')
|
|
8
|
+
vi.mock('form-data')
|
|
9
|
+
|
|
10
|
+
describe('HttpClient File Upload', () => {
|
|
11
|
+
let client: HttpClient
|
|
12
|
+
const mockApiInstance = {
|
|
13
|
+
uploadFile: vi.fn(),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const baseConfig = {
|
|
17
|
+
baseUrl: 'http://test.com',
|
|
18
|
+
headers: {},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const mockOpenApiSpec: OpenAPIV3.Document = {
|
|
22
|
+
openapi: '3.0.0',
|
|
23
|
+
info: {
|
|
24
|
+
title: 'Test API',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
},
|
|
27
|
+
paths: {
|
|
28
|
+
'/upload': {
|
|
29
|
+
post: {
|
|
30
|
+
operationId: 'uploadFile',
|
|
31
|
+
responses: {
|
|
32
|
+
'200': {
|
|
33
|
+
description: 'File uploaded successfully',
|
|
34
|
+
content: {
|
|
35
|
+
'application/json': {
|
|
36
|
+
schema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
success: {
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
requestBody: {
|
|
49
|
+
content: {
|
|
50
|
+
'multipart/form-data': {
|
|
51
|
+
schema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
file: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
format: 'binary',
|
|
57
|
+
},
|
|
58
|
+
description: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
vi.clearAllMocks()
|
|
73
|
+
client = new HttpClient(baseConfig, mockOpenApiSpec)
|
|
74
|
+
// @ts-expect-error - Mock the private api property
|
|
75
|
+
client['api'] = Promise.resolve(mockApiInstance)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should handle file uploads with FormData', async () => {
|
|
79
|
+
const mockFormData = new FormData()
|
|
80
|
+
const mockFileStream = { pipe: vi.fn() }
|
|
81
|
+
const mockFormDataHeaders = { 'content-type': 'multipart/form-data; boundary=---123' }
|
|
82
|
+
|
|
83
|
+
vi.mocked(fs.createReadStream).mockReturnValue(mockFileStream as any)
|
|
84
|
+
vi.mocked(FormData.prototype.append).mockImplementation(() => {})
|
|
85
|
+
vi.mocked(FormData.prototype.getHeaders).mockReturnValue(mockFormDataHeaders)
|
|
86
|
+
|
|
87
|
+
const uploadPath = mockOpenApiSpec.paths['/upload']
|
|
88
|
+
if (!uploadPath?.post) {
|
|
89
|
+
throw new Error('Upload path not found in spec')
|
|
90
|
+
}
|
|
91
|
+
const operation = uploadPath.post as OpenAPIV3.OperationObject & { method: string; path: string }
|
|
92
|
+
const params = {
|
|
93
|
+
file: '/path/to/test.txt',
|
|
94
|
+
description: 'Test file',
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
mockApiInstance.uploadFile.mockResolvedValue({
|
|
98
|
+
data: { success: true },
|
|
99
|
+
status: 200,
|
|
100
|
+
headers: {},
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
await client.executeOperation(operation, params)
|
|
104
|
+
|
|
105
|
+
expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/test.txt')
|
|
106
|
+
expect(FormData.prototype.append).toHaveBeenCalledWith('file', mockFileStream)
|
|
107
|
+
expect(FormData.prototype.append).toHaveBeenCalledWith('description', 'Test file')
|
|
108
|
+
expect(mockApiInstance.uploadFile).toHaveBeenCalledWith({}, expect.any(FormData), { headers: mockFormDataHeaders })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should throw error for invalid file path', async () => {
|
|
112
|
+
vi.mocked(fs.createReadStream).mockImplementation(() => {
|
|
113
|
+
throw new Error('File not found')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const uploadPath = mockOpenApiSpec.paths['/upload']
|
|
117
|
+
if (!uploadPath?.post) {
|
|
118
|
+
throw new Error('Upload path not found in spec')
|
|
119
|
+
}
|
|
120
|
+
const operation = uploadPath.post as OpenAPIV3.OperationObject & { method: string; path: string }
|
|
121
|
+
const params = {
|
|
122
|
+
file: '/nonexistent/file.txt',
|
|
123
|
+
description: 'Test file',
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await expect(client.executeOperation(operation, params)).rejects.toThrow('Failed to read file at /nonexistent/file.txt')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should handle multiple file uploads', async () => {
|
|
130
|
+
const mockFormData = new FormData()
|
|
131
|
+
const mockFileStream1 = { pipe: vi.fn() }
|
|
132
|
+
const mockFileStream2 = { pipe: vi.fn() }
|
|
133
|
+
const mockFormDataHeaders = { 'content-type': 'multipart/form-data; boundary=---123' }
|
|
134
|
+
|
|
135
|
+
vi.mocked(fs.createReadStream)
|
|
136
|
+
.mockReturnValueOnce(mockFileStream1 as any)
|
|
137
|
+
.mockReturnValueOnce(mockFileStream2 as any)
|
|
138
|
+
vi.mocked(FormData.prototype.append).mockImplementation(() => {})
|
|
139
|
+
vi.mocked(FormData.prototype.getHeaders).mockReturnValue(mockFormDataHeaders)
|
|
140
|
+
|
|
141
|
+
const operation: OpenAPIV3.OperationObject = {
|
|
142
|
+
operationId: 'uploadFile',
|
|
143
|
+
responses: {
|
|
144
|
+
'200': {
|
|
145
|
+
description: 'Files uploaded successfully',
|
|
146
|
+
content: {
|
|
147
|
+
'application/json': {
|
|
148
|
+
schema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
success: {
|
|
152
|
+
type: 'boolean',
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
requestBody: {
|
|
161
|
+
content: {
|
|
162
|
+
'multipart/form-data': {
|
|
163
|
+
schema: {
|
|
164
|
+
type: 'object',
|
|
165
|
+
properties: {
|
|
166
|
+
file1: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
format: 'binary',
|
|
169
|
+
},
|
|
170
|
+
file2: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
format: 'binary',
|
|
173
|
+
},
|
|
174
|
+
description: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const params = {
|
|
185
|
+
file1: '/path/to/test1.txt',
|
|
186
|
+
file2: '/path/to/test2.txt',
|
|
187
|
+
description: 'Test files',
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
mockApiInstance.uploadFile.mockResolvedValue({
|
|
191
|
+
data: { success: true },
|
|
192
|
+
status: 200,
|
|
193
|
+
headers: {},
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
await client.executeOperation(operation as OpenAPIV3.OperationObject & { method: string; path: string }, params)
|
|
197
|
+
|
|
198
|
+
expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/test1.txt')
|
|
199
|
+
expect(fs.createReadStream).toHaveBeenCalledWith('/path/to/test2.txt')
|
|
200
|
+
expect(FormData.prototype.append).toHaveBeenCalledWith('file1', mockFileStream1)
|
|
201
|
+
expect(FormData.prototype.append).toHaveBeenCalledWith('file2', mockFileStream2)
|
|
202
|
+
expect(FormData.prototype.append).toHaveBeenCalledWith('description', 'Test files')
|
|
203
|
+
expect(mockApiInstance.uploadFile).toHaveBeenCalledWith({}, expect.any(FormData), { headers: mockFormDataHeaders })
|
|
204
|
+
})
|
|
205
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
2
|
+
import { HttpClient } from '../http-client'
|
|
3
|
+
import type express from 'express'
|
|
4
|
+
//@ts-ignore
|
|
5
|
+
import { createPetstoreServer } from '../../../examples/petstore-server.cjs'
|
|
6
|
+
import type { OpenAPIV3 } from 'openapi-types'
|
|
7
|
+
import axios from 'axios'
|
|
8
|
+
|
|
9
|
+
interface Pet {
|
|
10
|
+
id: number
|
|
11
|
+
name: string
|
|
12
|
+
species: string
|
|
13
|
+
age: number
|
|
14
|
+
status: 'available' | 'pending' | 'sold'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('HttpClient Integration Tests', () => {
|
|
18
|
+
const PORT = 3456
|
|
19
|
+
const BASE_URL = `http://localhost:${PORT}`
|
|
20
|
+
let server: ReturnType<typeof express>
|
|
21
|
+
let openApiSpec: OpenAPIV3.Document
|
|
22
|
+
let client: HttpClient
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
// Start the petstore server
|
|
26
|
+
server = createPetstoreServer(PORT) as unknown as express.Express
|
|
27
|
+
|
|
28
|
+
// Fetch the OpenAPI spec from the server
|
|
29
|
+
const response = await axios.get(`${BASE_URL}/openapi.json`)
|
|
30
|
+
openApiSpec = response.data
|
|
31
|
+
|
|
32
|
+
// Create HTTP client
|
|
33
|
+
client = new HttpClient(
|
|
34
|
+
{
|
|
35
|
+
baseUrl: BASE_URL,
|
|
36
|
+
headers: {
|
|
37
|
+
Accept: 'application/json',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
openApiSpec,
|
|
41
|
+
)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
afterAll(() => {
|
|
45
|
+
//@ts-expect-error
|
|
46
|
+
server.close()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should list all pets', async () => {
|
|
50
|
+
const operation = openApiSpec.paths['/pets']?.get
|
|
51
|
+
if (!operation) throw new Error('Operation not found')
|
|
52
|
+
|
|
53
|
+
const response = await client.executeOperation<Pet[]>(operation as OpenAPIV3.OperationObject & { method: string; path: string })
|
|
54
|
+
|
|
55
|
+
expect(response.status).toBe(200)
|
|
56
|
+
expect(Array.isArray(response.data)).toBe(true)
|
|
57
|
+
expect(response.data.length).toBeGreaterThan(0)
|
|
58
|
+
expect(response.data[0]).toHaveProperty('name')
|
|
59
|
+
expect(response.data[0]).toHaveProperty('species')
|
|
60
|
+
expect(response.data[0]).toHaveProperty('status')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should filter pets by status', async () => {
|
|
64
|
+
const operation = openApiSpec.paths['/pets']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
|
|
65
|
+
if (!operation) throw new Error('Operation not found')
|
|
66
|
+
|
|
67
|
+
const response = await client.executeOperation<Pet[]>(operation, { status: 'available' })
|
|
68
|
+
|
|
69
|
+
expect(response.status).toBe(200)
|
|
70
|
+
expect(Array.isArray(response.data)).toBe(true)
|
|
71
|
+
response.data.forEach((pet: Pet) => {
|
|
72
|
+
expect(pet.status).toBe('available')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should get a specific pet by ID', async () => {
|
|
77
|
+
const operation = openApiSpec.paths['/pets/{id}']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
|
|
78
|
+
if (!operation) throw new Error('Operation not found')
|
|
79
|
+
|
|
80
|
+
const response = await client.executeOperation<Pet>(operation, { id: 1 })
|
|
81
|
+
|
|
82
|
+
expect(response.status).toBe(200)
|
|
83
|
+
expect(response.data).toHaveProperty('id', 1)
|
|
84
|
+
expect(response.data).toHaveProperty('name')
|
|
85
|
+
expect(response.data).toHaveProperty('species')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should create a new pet', async () => {
|
|
89
|
+
const operation = openApiSpec.paths['/pets']?.post as OpenAPIV3.OperationObject & { method: string; path: string }
|
|
90
|
+
if (!operation) throw new Error('Operation not found')
|
|
91
|
+
|
|
92
|
+
const newPet = {
|
|
93
|
+
name: 'TestPet',
|
|
94
|
+
species: 'Dog',
|
|
95
|
+
age: 2,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const response = await client.executeOperation<Pet>(operation as OpenAPIV3.OperationObject & { method: string; path: string }, newPet)
|
|
99
|
+
|
|
100
|
+
expect(response.status).toBe(201)
|
|
101
|
+
expect(response.data).toMatchObject({
|
|
102
|
+
...newPet,
|
|
103
|
+
status: 'available',
|
|
104
|
+
})
|
|
105
|
+
expect(response.data.id).toBeDefined()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("should update a pet's status", async () => {
|
|
109
|
+
const operation = openApiSpec.paths['/pets/{id}']?.put
|
|
110
|
+
if (!operation) throw new Error('Operation not found')
|
|
111
|
+
|
|
112
|
+
const response = await client.executeOperation<Pet>(operation as OpenAPIV3.OperationObject & { method: string; path: string }, {
|
|
113
|
+
id: 1,
|
|
114
|
+
status: 'sold',
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
expect(response.status).toBe(200)
|
|
118
|
+
expect(response.data).toHaveProperty('id', 1)
|
|
119
|
+
expect(response.data).toHaveProperty('status', 'sold')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should delete a pet', async () => {
|
|
123
|
+
// First create a pet to delete
|
|
124
|
+
const createOperation = openApiSpec.paths['/pets']?.post
|
|
125
|
+
if (!createOperation) throw new Error('Operation not found')
|
|
126
|
+
|
|
127
|
+
const createResponse = await client.executeOperation<Pet>(
|
|
128
|
+
createOperation as OpenAPIV3.OperationObject & { method: string; path: string },
|
|
129
|
+
{
|
|
130
|
+
name: 'ToDelete',
|
|
131
|
+
species: 'Cat',
|
|
132
|
+
age: 3,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
const petId = createResponse.data.id
|
|
136
|
+
|
|
137
|
+
// Then delete it
|
|
138
|
+
const deleteOperation = openApiSpec.paths['/pets/{id}']?.delete
|
|
139
|
+
if (!deleteOperation) throw new Error('Operation not found')
|
|
140
|
+
|
|
141
|
+
const deleteResponse = await client.executeOperation(deleteOperation as OpenAPIV3.OperationObject & { method: string; path: string }, {
|
|
142
|
+
id: petId,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(deleteResponse.status).toBe(204)
|
|
146
|
+
|
|
147
|
+
// Verify the pet is deleted
|
|
148
|
+
const getOperation = openApiSpec.paths['/pets/{id}']?.get
|
|
149
|
+
if (!getOperation) throw new Error('Operation not found')
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await client.executeOperation(getOperation as OpenAPIV3.OperationObject & { method: string; path: string }, { id: petId })
|
|
153
|
+
throw new Error('Should not reach here')
|
|
154
|
+
} catch (error: any) {
|
|
155
|
+
expect(error.message).toContain('404')
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should handle errors appropriately', async () => {
|
|
160
|
+
const operation = openApiSpec.paths['/pets/{id}']?.get as OpenAPIV3.OperationObject & { method: string; path: string }
|
|
161
|
+
if (!operation) throw new Error('Operation not found')
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await client.executeOperation(
|
|
165
|
+
operation as OpenAPIV3.OperationObject & { method: string; path: string },
|
|
166
|
+
{ id: 99999 }, // Non-existent ID
|
|
167
|
+
)
|
|
168
|
+
throw new Error('Should not reach here')
|
|
169
|
+
} catch (error: any) {
|
|
170
|
+
expect(error.message).toContain('404')
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
})
|