1
+ import { Response } from 'express' ;
2
+ import { DemoInMemoryAuthProvider , DemoInMemoryClientsStore } from './demoInMemoryOAuthProvider.js' ;
3
+ import { AuthorizationParams } from '../../server/auth/provider.js' ;
4
+ import { OAuthClientInformationFull } from '../../shared/auth.js' ;
5
+ import { InvalidRequestError } from '../../server/auth/errors.js' ;
6
+
7
+ describe ( 'DemoInMemoryAuthProvider' , ( ) => {
8
+ let provider : DemoInMemoryAuthProvider ;
9
+ let mockResponse : Response & { getRedirectUrl : ( ) => string } ;
10
+
11
+ const createMockResponse = ( ) : Response & { getRedirectUrl : ( ) => string } => {
12
+ let capturedRedirectUrl : string | undefined ;
13
+
14
+ const mockRedirect = jest . fn ( ) . mockImplementation ( ( url : string | number , status ?: number ) => {
15
+ if ( typeof url === 'string' ) {
16
+ capturedRedirectUrl = url ;
17
+ } else if ( typeof status === 'string' ) {
18
+ capturedRedirectUrl = status ;
19
+ }
20
+ return mockResponse ;
21
+ } ) ;
22
+
23
+ const mockResponse = {
24
+ redirect : mockRedirect ,
25
+ status : jest . fn ( ) . mockReturnThis ( ) ,
26
+ json : jest . fn ( ) . mockReturnThis ( ) ,
27
+ send : jest . fn ( ) . mockReturnThis ( ) ,
28
+ getRedirectUrl : ( ) => {
29
+ if ( capturedRedirectUrl === undefined ) {
30
+ throw new Error ( 'No redirect URL was captured. Ensure redirect() was called first.' ) ;
31
+ }
32
+ return capturedRedirectUrl ;
33
+ } ,
34
+ } as unknown as Response & { getRedirectUrl : ( ) => string } ;
35
+
36
+ return mockResponse ;
37
+ } ;
38
+
39
+ beforeEach ( ( ) => {
40
+ provider = new DemoInMemoryAuthProvider ( ) ;
41
+ mockResponse = createMockResponse ( ) ;
42
+ } ) ;
43
+
44
+ describe ( 'authorize' , ( ) => {
45
+ const validClient : OAuthClientInformationFull = {
46
+ client_id : 'test-client' ,
47
+ client_secret : 'test-secret' ,
48
+ redirect_uris : [
49
+ 'https://example.com/callback' ,
50
+ 'https://example.com/callback2'
51
+ ] ,
52
+ scope : 'test-scope'
53
+ } ;
54
+
55
+ it ( 'should redirect to the requested redirect_uri when valid' , async ( ) => {
56
+ const params : AuthorizationParams = {
57
+ redirectUri : 'https://example.com/callback' ,
58
+ state : 'test-state' ,
59
+ codeChallenge : 'test-challenge' ,
60
+ scopes : [ 'test-scope' ]
61
+ } ;
62
+
63
+ await provider . authorize ( validClient , params , mockResponse ) ;
64
+
65
+ expect ( mockResponse . redirect ) . toHaveBeenCalled ( ) ;
66
+ expect ( mockResponse . getRedirectUrl ( ) ) . toBeDefined ( ) ;
67
+
68
+ const url = new URL ( mockResponse . getRedirectUrl ( ) ) ;
69
+ expect ( url . origin + url . pathname ) . toBe ( 'https://example.com/callback' ) ;
70
+ expect ( url . searchParams . get ( 'state' ) ) . toBe ( 'test-state' ) ;
71
+ expect ( url . searchParams . has ( 'code' ) ) . toBe ( true ) ;
72
+ } ) ;
73
+
74
+ it ( 'should throw InvalidRequestError for unregistered redirect_uri' , async ( ) => {
75
+ const params : AuthorizationParams = {
76
+ redirectUri : 'https://evil.com/callback' ,
77
+ state : 'test-state' ,
78
+ codeChallenge : 'test-challenge' ,
79
+ scopes : [ 'test-scope' ]
80
+ } ;
81
+
82
+ await expect (
83
+ provider . authorize ( validClient , params , mockResponse )
84
+ ) . rejects . toThrow ( InvalidRequestError ) ;
85
+
86
+ await expect (
87
+ provider . authorize ( validClient , params , mockResponse )
88
+ ) . rejects . toThrow ( 'Unregistered redirect_uri' ) ;
89
+
90
+ expect ( mockResponse . redirect ) . not . toHaveBeenCalled ( ) ;
91
+ } ) ;
92
+
93
+ it ( 'should generate unique authorization codes for multiple requests' , async ( ) => {
94
+ const params1 : AuthorizationParams = {
95
+ redirectUri : 'https://example.com/callback' ,
96
+ state : 'state-1' ,
97
+ codeChallenge : 'challenge-1' ,
98
+ scopes : [ 'test-scope' ]
99
+ } ;
100
+
101
+ const params2 : AuthorizationParams = {
102
+ redirectUri : 'https://example.com/callback' ,
103
+ state : 'state-2' ,
104
+ codeChallenge : 'challenge-2' ,
105
+ scopes : [ 'test-scope' ]
106
+ } ;
107
+
108
+ await provider . authorize ( validClient , params1 , mockResponse ) ;
109
+ const firstRedirectUrl = mockResponse . getRedirectUrl ( ) ;
110
+ const firstCode = new URL ( firstRedirectUrl ) . searchParams . get ( 'code' ) ;
111
+
112
+ // Reset the mock for the second call
113
+ mockResponse = createMockResponse ( ) ;
114
+ await provider . authorize ( validClient , params2 , mockResponse ) ;
115
+ const secondRedirectUrl = mockResponse . getRedirectUrl ( ) ;
116
+ const secondCode = new URL ( secondRedirectUrl ) . searchParams . get ( 'code' ) ;
117
+
118
+ expect ( firstCode ) . toBeDefined ( ) ;
119
+ expect ( secondCode ) . toBeDefined ( ) ;
120
+ expect ( firstCode ) . not . toBe ( secondCode ) ;
121
+ } ) ;
122
+
123
+ it ( 'should handle params without state' , async ( ) => {
124
+ const params : AuthorizationParams = {
125
+ redirectUri : 'https://example.com/callback' ,
126
+ codeChallenge : 'test-challenge' ,
127
+ scopes : [ 'test-scope' ]
128
+ } ;
129
+
130
+ await provider . authorize ( validClient , params , mockResponse ) ;
131
+
132
+ expect ( mockResponse . redirect ) . toHaveBeenCalled ( ) ;
133
+ expect ( mockResponse . getRedirectUrl ( ) ) . toBeDefined ( ) ;
134
+
135
+ const url = new URL ( mockResponse . getRedirectUrl ( ) ) ;
136
+ expect ( url . searchParams . has ( 'state' ) ) . toBe ( false ) ;
137
+ expect ( url . searchParams . has ( 'code' ) ) . toBe ( true ) ;
138
+ } ) ;
139
+ } ) ;
140
+
141
+ describe ( 'challengeForAuthorizationCode' , ( ) => {
142
+ const validClient : OAuthClientInformationFull = {
143
+ client_id : 'test-client' ,
144
+ client_secret : 'test-secret' ,
145
+ redirect_uris : [ 'https://example.com/callback' ] ,
146
+ scope : 'test-scope'
147
+ } ;
148
+
149
+ it ( 'should return the code challenge for a valid authorization code' , async ( ) => {
150
+ const params : AuthorizationParams = {
151
+ redirectUri : 'https://example.com/callback' ,
152
+ state : 'test-state' ,
153
+ codeChallenge : 'test-challenge-value' ,
154
+ scopes : [ 'test-scope' ]
155
+ } ;
156
+
157
+ await provider . authorize ( validClient , params , mockResponse ) ;
158
+ const code = new URL ( mockResponse . getRedirectUrl ( ) ) . searchParams . get ( 'code' ) ! ;
159
+
160
+ const challenge = await provider . challengeForAuthorizationCode ( validClient , code ) ;
161
+ expect ( challenge ) . toBe ( 'test-challenge-value' ) ;
162
+ } ) ;
163
+
164
+ it ( 'should throw error for invalid authorization code' , async ( ) => {
165
+ await expect (
166
+ provider . challengeForAuthorizationCode ( validClient , 'invalid-code' )
167
+ ) . rejects . toThrow ( 'Invalid authorization code' ) ;
168
+ } ) ;
169
+ } ) ;
170
+
171
+ describe ( 'exchangeAuthorizationCode' , ( ) => {
172
+ const validClient : OAuthClientInformationFull = {
173
+ client_id : 'test-client' ,
174
+ client_secret : 'test-secret' ,
175
+ redirect_uris : [ 'https://example.com/callback' ] ,
176
+ scope : 'test-scope'
177
+ } ;
178
+
179
+ it ( 'should exchange valid authorization code for tokens' , async ( ) => {
180
+ const params : AuthorizationParams = {
181
+ redirectUri : 'https://example.com/callback' ,
182
+ state : 'test-state' ,
183
+ codeChallenge : 'test-challenge' ,
184
+ scopes : [ 'test-scope' , 'other-scope' ]
185
+ } ;
186
+
187
+ await provider . authorize ( validClient , params , mockResponse ) ;
188
+ const code = new URL ( mockResponse . getRedirectUrl ( ) ) . searchParams . get ( 'code' ) ! ;
189
+
190
+ const tokens = await provider . exchangeAuthorizationCode ( validClient , code ) ;
191
+
192
+ expect ( tokens ) . toEqual ( {
193
+ access_token : expect . any ( String ) ,
194
+ token_type : 'bearer' ,
195
+ expires_in : 3600 ,
196
+ scope : 'test-scope other-scope'
197
+ } ) ;
198
+ } ) ;
199
+
200
+ it ( 'should throw error for invalid authorization code' , async ( ) => {
201
+ await expect (
202
+ provider . exchangeAuthorizationCode ( validClient , 'invalid-code' )
203
+ ) . rejects . toThrow ( 'Invalid authorization code' ) ;
204
+ } ) ;
205
+
206
+ it ( 'should throw error when client_id does not match' , async ( ) => {
207
+ const params : AuthorizationParams = {
208
+ redirectUri : 'https://example.com/callback' ,
209
+ state : 'test-state' ,
210
+ codeChallenge : 'test-challenge' ,
211
+ scopes : [ 'test-scope' ]
212
+ } ;
213
+
214
+ await provider . authorize ( validClient , params , mockResponse ) ;
215
+ const code = new URL ( mockResponse . getRedirectUrl ( ) ) . searchParams . get ( 'code' ) ! ;
216
+
217
+ const differentClient : OAuthClientInformationFull = {
218
+ client_id : 'different-client' ,
219
+ client_secret : 'different-secret' ,
220
+ redirect_uris : [ 'https://example.com/callback' ] ,
221
+ scope : 'test-scope'
222
+ } ;
223
+
224
+ await expect (
225
+ provider . exchangeAuthorizationCode ( differentClient , code )
226
+ ) . rejects . toThrow ( 'Authorization code was not issued to this client' ) ;
227
+ } ) ;
228
+
229
+ it ( 'should delete authorization code after successful exchange' , async ( ) => {
230
+ const params : AuthorizationParams = {
231
+ redirectUri : 'https://example.com/callback' ,
232
+ state : 'test-state' ,
233
+ codeChallenge : 'test-challenge' ,
234
+ scopes : [ 'test-scope' ]
235
+ } ;
236
+
237
+ await provider . authorize ( validClient , params , mockResponse ) ;
238
+ const code = new URL ( mockResponse . getRedirectUrl ( ) ) . searchParams . get ( 'code' ) ! ;
239
+
240
+ // First exchange should succeed
241
+ await provider . exchangeAuthorizationCode ( validClient , code ) ;
242
+
243
+ // Second exchange should fail
244
+ await expect (
245
+ provider . exchangeAuthorizationCode ( validClient , code )
246
+ ) . rejects . toThrow ( 'Invalid authorization code' ) ;
247
+ } ) ;
248
+
249
+ it ( 'should validate resource when validateResource is provided' , async ( ) => {
250
+ const validateResource = jest . fn ( ) . mockReturnValue ( false ) ;
251
+ const strictProvider = new DemoInMemoryAuthProvider ( validateResource ) ;
252
+
253
+ const params : AuthorizationParams = {
254
+ redirectUri : 'https://example.com/callback' ,
255
+ state : 'test-state' ,
256
+ codeChallenge : 'test-challenge' ,
257
+ scopes : [ 'test-scope' ] ,
258
+ resource : new URL ( 'https://invalid-resource.com' )
259
+ } ;
260
+
261
+ await strictProvider . authorize ( validClient , params , mockResponse ) ;
262
+ const code = new URL ( mockResponse . getRedirectUrl ( ) ) . searchParams . get ( 'code' ) ! ;
263
+
264
+ await expect (
265
+ strictProvider . exchangeAuthorizationCode ( validClient , code )
266
+ ) . rejects . toThrow ( 'Invalid resource: https://invalid-resource.com/' ) ;
267
+
268
+ expect ( validateResource ) . toHaveBeenCalledWith ( params . resource ) ;
269
+ } ) ;
270
+ } ) ;
271
+
272
+ describe ( 'DemoInMemoryClientsStore' , ( ) => {
273
+ let store : DemoInMemoryClientsStore ;
274
+
275
+ beforeEach ( ( ) => {
276
+ store = new DemoInMemoryClientsStore ( ) ;
277
+ } ) ;
278
+
279
+ it ( 'should register and retrieve client' , async ( ) => {
280
+ const client : OAuthClientInformationFull = {
281
+ client_id : 'test-client' ,
282
+ client_secret : 'test-secret' ,
283
+ redirect_uris : [ 'https://example.com/callback' ] ,
284
+ scope : 'test-scope'
285
+ } ;
286
+
287
+ await store . registerClient ( client ) ;
288
+ const retrieved = await store . getClient ( 'test-client' ) ;
289
+
290
+ expect ( retrieved ) . toEqual ( client ) ;
291
+ } ) ;
292
+
293
+ it ( 'should return undefined for non-existent client' , async ( ) => {
294
+ const retrieved = await store . getClient ( 'non-existent' ) ;
295
+ expect ( retrieved ) . toBeUndefined ( ) ;
296
+ } ) ;
297
+ } ) ;
298
+ } ) ;
0 commit comments