Skip to content

Commit 1d5c150

Browse files
refactor: migrate all tests from mockFetch to MSW (#384)
Co-authored-by: Claude <[email protected]>
1 parent 0cefa48 commit 1d5c150

23 files changed

+1293
-1601
lines changed

jest.config.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,37 @@
1+
const transformIgnorePackages = ['msw', 'until-async', '@mswjs']
2+
13
export default {
24
preset: 'ts-jest/presets/default-esm',
35
extensionsToTreatAsEsm: ['.ts'],
46
transform: {
5-
'^.+\\.ts$': ['ts-jest', { useESM: true }],
7+
'^.+\\.ts$': [
8+
'ts-jest',
9+
{
10+
useESM: true,
11+
tsconfig: {
12+
module: 'ES2022',
13+
moduleResolution: 'bundler',
14+
},
15+
},
16+
],
17+
'^.+\\.js$': [
18+
'ts-jest',
19+
{
20+
useESM: true,
21+
tsconfig: {
22+
allowJs: true,
23+
module: 'ES2022',
24+
moduleResolution: 'bundler',
25+
},
26+
},
27+
],
628
},
729
testMatch: ['**/*.test.ts'],
830
clearMocks: true,
931
testEnvironment: 'node',
10-
transformIgnorePatterns: [
11-
'node_modules/(?!(msw|@mswjs|@bundled-es-modules|until-async|chalk|@open-draft|@inquirer|strict-event-emitter)/)',
12-
],
32+
setupFilesAfterEnv: ['<rootDir>/src/test-utils/msw-setup.ts'],
33+
transformIgnorePatterns: [`/node_modules/(?!(${transformIgnorePackages.join('|')})/)`],
34+
moduleNameMapper: {
35+
'^(\\.{1,2}/.*)\\.js$': '$1',
36+
},
1337
}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/authentication.test.ts

Lines changed: 49 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
revokeToken,
66
Permission,
77
} from './authentication'
8-
import { setupRestClientMock } from './test-utils/mocks'
8+
import { server, http, HttpResponse } from './test-utils/msw-setup'
99
import { assertInstance } from './test-utils/asserts'
1010
import { TodoistRequestError } from './types'
1111
import { getSyncBaseUri } from './consts/endpoints'
@@ -77,23 +77,12 @@ describe('authentication', () => {
7777
tokenType: 'Bearer',
7878
}
7979

80-
test('calls request with expected values', async () => {
81-
const requestMock = setupRestClientMock(successfulTokenResponse)
82-
83-
await getAuthToken(defaultAuthRequest)
84-
85-
expect(requestMock).toHaveBeenCalledTimes(1)
86-
expect(requestMock).toHaveBeenCalledWith({
87-
httpMethod: 'POST',
88-
baseUri: 'https://todoist.com/oauth/',
89-
relativePath: 'access_token',
90-
apiToken: undefined,
91-
payload: defaultAuthRequest,
92-
})
93-
})
94-
9580
test('returns values from successful request', async () => {
96-
setupRestClientMock(successfulTokenResponse)
81+
server.use(
82+
http.post('https://todoist.com/oauth/access_token', () => {
83+
return HttpResponse.json(successfulTokenResponse, { status: 200 })
84+
}),
85+
)
9786

9887
const tokenResponse = await getAuthToken(defaultAuthRequest)
9988

@@ -102,7 +91,11 @@ describe('authentication', () => {
10291

10392
test('throws error if non 200 response', async () => {
10493
const failureStatus = 400
105-
setupRestClientMock(undefined, failureStatus)
94+
server.use(
95+
http.post('https://todoist.com/oauth/access_token', () => {
96+
return HttpResponse.json(undefined, { status: failureStatus })
97+
}),
98+
)
10699

107100
expect.assertions(3)
108101

@@ -122,7 +115,11 @@ describe('authentication', () => {
122115
tokenType: undefined,
123116
}
124117

125-
setupRestClientMock(missingTokenResponse)
118+
server.use(
119+
http.post('https://todoist.com/oauth/access_token', () => {
120+
return HttpResponse.json(missingTokenResponse, { status: 200 })
121+
}),
122+
)
126123

127124
expect.assertions(2)
128125

@@ -143,19 +140,15 @@ describe('authentication', () => {
143140
accessToken: 'AToken',
144141
}
145142

146-
test('calls request with expected values', async () => {
147-
const requestMock = setupRestClientMock(undefined, 200)
143+
test('returns true when revocation succeeds', async () => {
144+
server.use(
145+
http.post(`${getSyncBaseUri()}access_tokens/revoke`, () => {
146+
return HttpResponse.json(undefined, { status: 200 })
147+
}),
148+
)
148149

149150
const isSuccess = await revokeAuthToken(revokeTokenRequest)
150151

151-
expect(requestMock).toHaveBeenCalledTimes(1)
152-
expect(requestMock).toHaveBeenCalledWith({
153-
httpMethod: 'POST',
154-
baseUri: getSyncBaseUri(),
155-
relativePath: 'access_tokens/revoke',
156-
apiToken: undefined,
157-
payload: revokeTokenRequest,
158-
})
159152
expect(isSuccess).toEqual(true)
160153
})
161154
})
@@ -168,52 +161,50 @@ describe('authentication', () => {
168161
}
169162

170163
test('calls request with RFC 7009 compliant parameters', async () => {
171-
const requestMock = setupRestClientMock(undefined, 200)
164+
let capturedHeaders: Record<string, string> = {}
165+
let capturedBody: unknown = null
172166

173-
const isSuccess = await revokeToken(revokeTokenRequest)
167+
server.use(
168+
http.post(`${getSyncBaseUri()}revoke`, async ({ request }) => {
169+
// Capture headers
170+
const headers: Record<string, string> = {}
171+
request.headers.forEach((value, key) => {
172+
headers[key] = value
173+
})
174+
capturedHeaders = headers
175+
176+
// Capture body
177+
capturedBody = await request.json()
174178

175-
expect(requestMock).toHaveBeenCalledTimes(1)
176-
177-
// Verify the correct endpoint is called
178-
expect(requestMock).toHaveBeenCalledTimes(1)
179-
const mockCall = requestMock.mock.calls[0] as [
180-
{
181-
httpMethod: string
182-
baseUri: string
183-
relativePath: string
184-
apiToken?: string
185-
payload: Record<string, unknown>
186-
customHeaders?: Record<string, string>
187-
},
188-
]
189-
const callArgs = mockCall[0]
190-
expect(callArgs.httpMethod).toEqual('POST')
191-
expect(callArgs.baseUri).toEqual(getSyncBaseUri())
192-
expect(callArgs.relativePath).toEqual('revoke')
193-
194-
// Verify no API token is passed (should be undefined)
195-
expect(callArgs.apiToken).toBeUndefined()
179+
return HttpResponse.json(undefined, { status: 200 })
180+
}),
181+
)
182+
183+
const isSuccess = await revokeToken(revokeTokenRequest)
196184

197185
// Verify request body contains only token and token_type_hint
198-
expect(callArgs.payload).toEqual({
186+
expect(capturedBody).toEqual({
199187
token: 'AToken',
200188
token_type_hint: 'access_token',
201189
})
202190

203191
// Verify Basic Auth header is present
204-
const customHeaders = callArgs.customHeaders
205-
expect(customHeaders).toBeDefined()
206-
expect(customHeaders?.Authorization).toMatch(/^Basic /)
192+
expect(capturedHeaders).toBeDefined()
193+
expect(capturedHeaders.authorization).toMatch(/^Basic /)
207194

208195
// Verify Basic Auth is correctly encoded (base64 of "SomeId:ASecret")
209196
const expectedAuth = Buffer.from('SomeId:ASecret').toString('base64')
210-
expect(customHeaders?.Authorization).toEqual(`Basic ${expectedAuth}`)
197+
expect(capturedHeaders.authorization).toEqual(`Basic ${expectedAuth}`)
211198

212199
expect(isSuccess).toEqual(true)
213200
})
214201

215202
test('returns true when revocation succeeds', async () => {
216-
setupRestClientMock(undefined, 200)
203+
server.use(
204+
http.post(`${getSyncBaseUri()}revoke`, () => {
205+
return HttpResponse.json(undefined, { status: 200 })
206+
}),
207+
)
217208

218209
const result = await revokeToken(revokeTokenRequest)
219210

src/authentication.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -144,23 +144,33 @@ export async function getAuthToken(
144144
args: AuthTokenRequestArgs,
145145
baseUrl?: string,
146146
): Promise<AuthTokenResponse> {
147-
const response = await request<AuthTokenResponse>({
148-
httpMethod: 'POST',
149-
baseUri: getAuthBaseUri(baseUrl),
150-
relativePath: ENDPOINT_GET_TOKEN,
151-
apiToken: undefined,
152-
payload: args,
153-
})
147+
try {
148+
const response = await request<AuthTokenResponse>({
149+
httpMethod: 'POST',
150+
baseUri: getAuthBaseUri(baseUrl),
151+
relativePath: ENDPOINT_GET_TOKEN,
152+
apiToken: undefined,
153+
payload: args,
154+
})
155+
156+
if (response.status !== 200 || !response.data?.accessToken) {
157+
throw new TodoistRequestError(
158+
'Authentication token exchange failed.',
159+
response.status,
160+
response.data,
161+
)
162+
}
154163

155-
if (response.status !== 200 || !response.data?.accessToken) {
164+
return response.data
165+
} catch (error) {
166+
// Re-throw with custom message for authentication failures
167+
const err = error as TodoistRequestError
156168
throw new TodoistRequestError(
157169
'Authentication token exchange failed.',
158-
response.status,
159-
response.data,
170+
err.httpStatusCode,
171+
err.responseData,
160172
)
161173
}
162-
163-
return response.data
164174
}
165175

166176
/**

0 commit comments

Comments
 (0)