Skip to content

Commit 87c0ce6

Browse files
test: Add Minimal MCP API Client Tests (#474)
* test: add minimal mcp api client tests * fix: address pr feedback for type safety improvements * test: improve test assertion formatting for readability
1 parent 3d21c1a commit 87c0ce6

1 file changed

Lines changed: 185 additions & 0 deletions

File tree

src/mcp/utils/api.test.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { expect } from '@oclif/test'
2+
import sinon from 'sinon'
3+
import * as assert from 'assert'
4+
import { DevCycleApiClient, handleZodiosValidationErrors } from './api'
5+
import { DevCycleAuth } from './auth'
6+
import { setMCPToolCommand } from './headers'
7+
import * as apiClientModule from '../../api/apiClient'
8+
9+
describe('DevCycleApiClient', () => {
10+
let apiClient: DevCycleApiClient
11+
let authStub: sinon.SinonStubbedInstance<DevCycleAuth>
12+
let setDVCReferrerStub: sinon.SinonStub
13+
14+
beforeEach(() => {
15+
// Create stubbed auth instance
16+
authStub = sinon.createStubInstance(DevCycleAuth)
17+
authStub.getAuthToken.returns('mock-auth-token')
18+
authStub.getProjectKey.returns('test-project')
19+
authStub.getOrgId.returns('test-org-id')
20+
21+
apiClient = new DevCycleApiClient(authStub)
22+
23+
// Stub the setDVCReferrer function from apiClient module
24+
setDVCReferrerStub = sinon.stub(apiClientModule, 'setDVCReferrer')
25+
})
26+
27+
afterEach(() => {
28+
sinon.restore()
29+
})
30+
31+
describe('Zodios Error Handling', () => {
32+
it('should extract data from Zodios validation errors with 200 OK response', async () => {
33+
const mockResponseData = { id: '123', name: 'Test Feature' }
34+
35+
// Create a proper mock Zodios error with data property
36+
class ZodiosValidationError extends Error {
37+
constructor(
38+
message: string,
39+
public data: any,
40+
) {
41+
super(message)
42+
this.name = 'ZodiosValidationError'
43+
}
44+
}
45+
46+
const zodiosError = new ZodiosValidationError(
47+
'Zodios: Invalid response - status: 200 OK',
48+
mockResponseData,
49+
)
50+
51+
const apiCall = sinon.stub().rejects(zodiosError)
52+
53+
const result = await handleZodiosValidationErrors(
54+
apiCall,
55+
'testOperation',
56+
)
57+
58+
expect(result).to.deep.equal(mockResponseData)
59+
})
60+
61+
it('should re-throw non-Zodios errors unchanged', async () => {
62+
const networkError = new Error('Network timeout')
63+
const apiCall = sinon.stub().rejects(networkError)
64+
65+
try {
66+
await handleZodiosValidationErrors(apiCall, 'testOperation')
67+
assert.fail('Expected function to throw')
68+
} catch (error) {
69+
expect(error).to.equal(networkError)
70+
}
71+
})
72+
})
73+
74+
describe('executeWithLogging', () => {
75+
it('should execute operation successfully with valid auth and project', async () => {
76+
const mockResult = { id: '123', name: 'Test Feature' }
77+
const mockOperation = sinon.stub().resolves(mockResult)
78+
79+
authStub.requireAuth.returns()
80+
authStub.requireProject.returns()
81+
82+
const result = await apiClient.executeWithLogging(
83+
'testOperation',
84+
{ key: 'test-key' },
85+
mockOperation,
86+
)
87+
88+
expect(result).to.deep.equal(mockResult)
89+
sinon.assert.calledOnce(authStub.requireAuth)
90+
sinon.assert.calledOnce(authStub.requireProject)
91+
sinon.assert.calledWith(
92+
mockOperation,
93+
'mock-auth-token',
94+
'test-project',
95+
)
96+
sinon.assert.calledWith(
97+
setDVCReferrerStub,
98+
'testOperation',
99+
sinon.match.string,
100+
'mcp',
101+
)
102+
})
103+
104+
it('should handle authentication errors gracefully', async () => {
105+
const authError = new Error('Authentication failed')
106+
authStub.requireAuth.throws(authError)
107+
108+
const mockOperation = sinon.stub().resolves({})
109+
110+
try {
111+
await apiClient.executeWithLogging(
112+
'testOperation',
113+
null,
114+
mockOperation,
115+
)
116+
assert.fail('Expected function to throw')
117+
} catch (error) {
118+
expect((error as Error).message).to.equal(
119+
'Authentication failed',
120+
)
121+
sinon.assert.notCalled(mockOperation)
122+
}
123+
})
124+
})
125+
126+
describe('executeWithDashboardLink', () => {
127+
it('should generate dashboard links correctly', async () => {
128+
const mockResult = { key: 'test-feature', name: 'Test Feature' }
129+
const mockOperation = sinon.stub().resolves(mockResult)
130+
const dashboardLinkGenerator = sinon
131+
.stub()
132+
.returns(
133+
'https://app.devcycle.com/o/test-org-id/p/test-project/features',
134+
)
135+
136+
authStub.requireAuth.returns()
137+
authStub.requireProject.returns()
138+
139+
const result = await apiClient.executeWithDashboardLink(
140+
'createFeature',
141+
{ key: 'test-feature' },
142+
mockOperation,
143+
dashboardLinkGenerator,
144+
)
145+
146+
expect(result).to.deep.equal({
147+
result: mockResult,
148+
dashboardLink:
149+
'https://app.devcycle.com/o/test-org-id/p/test-project/features',
150+
})
151+
152+
sinon.assert.calledWith(
153+
dashboardLinkGenerator,
154+
'test-org-id',
155+
'test-project',
156+
mockResult,
157+
)
158+
})
159+
})
160+
})
161+
162+
describe('Header Management', () => {
163+
let setDVCReferrerStub: sinon.SinonStub
164+
165+
beforeEach(() => {
166+
setDVCReferrerStub = sinon.stub(apiClientModule, 'setDVCReferrer')
167+
})
168+
169+
afterEach(() => {
170+
sinon.restore()
171+
})
172+
173+
describe('setMCPToolCommand', () => {
174+
it('should set MCP headers correctly for tool commands', () => {
175+
setMCPToolCommand('list_features')
176+
177+
sinon.assert.calledWith(
178+
setDVCReferrerStub,
179+
'list_features',
180+
sinon.match.string, // version
181+
'mcp',
182+
)
183+
})
184+
})
185+
})

0 commit comments

Comments
 (0)