@@ -167,3 +167,178 @@ def test_scopes(
167
167
)
168
168
expected_status_code = 200 if expected_permitted else 401
169
169
assert response .status_code == expected_status_code
170
+
171
+
172
+ @pytest .mark .parametrize (
173
+ "path,default_public,private_endpoints" ,
174
+ [
175
+ ("/" , False , {}),
176
+ ("/collections" , False , {}),
177
+ ("/search" , False , {}),
178
+ ("/collections" , True , {r"^/collections$" : [("POST" , "collection:create" )]}),
179
+ ("/search" , True , {r"^/search$" : [("POST" , "search:write" )]}),
180
+ (
181
+ "/collections/example-collection/items" ,
182
+ True ,
183
+ {r"^/collections/.*/items$" : [("POST" , "item:create" )]},
184
+ ),
185
+ ],
186
+ )
187
+ def test_options_bypass_auth (
188
+ path , default_public , private_endpoints , source_api_server
189
+ ):
190
+ """OPTIONS requests should bypass authentication regardless of endpoint configuration."""
191
+ test_app = app_factory (
192
+ upstream_url = source_api_server ,
193
+ default_public = default_public ,
194
+ private_endpoints = private_endpoints ,
195
+ )
196
+ client = TestClient (test_app )
197
+ response = client .options (path )
198
+ assert response .status_code == 200 , "OPTIONS request should bypass authentication"
199
+
200
+
201
+ @pytest .mark .parametrize (
202
+ "path,method,default_public,private_endpoints,expected_status" ,
203
+ [
204
+ # Test that non-OPTIONS requests still require auth when endpoints are private
205
+ ("/collections" , "GET" , False , {}, 403 ),
206
+ ("/collections" , "POST" , False , {}, 403 ),
207
+ ("/search" , "GET" , False , {}, 403 ),
208
+ # Test that OPTIONS requests bypass auth even when endpoints are private
209
+ ("/collections" , "OPTIONS" , False , {}, 200 ),
210
+ ("/search" , "OPTIONS" , False , {}, 200 ),
211
+ # Test with specific private endpoint configurations
212
+ (
213
+ "/collections" ,
214
+ "POST" ,
215
+ True ,
216
+ {r"^/collections$" : [("POST" , "collection:create" )]},
217
+ 403 ,
218
+ ),
219
+ (
220
+ "/collections" ,
221
+ "OPTIONS" ,
222
+ True ,
223
+ {r"^/collections$" : [("POST" , "collection:create" )]},
224
+ 200 ,
225
+ ),
226
+ ],
227
+ )
228
+ def test_options_vs_other_methods_auth_behavior (
229
+ path , method , default_public , private_endpoints , expected_status , source_api_server
230
+ ):
231
+ """Compare authentication behavior between OPTIONS and other HTTP methods."""
232
+ test_app = app_factory (
233
+ upstream_url = source_api_server ,
234
+ default_public = default_public ,
235
+ private_endpoints = private_endpoints ,
236
+ )
237
+ client = TestClient (test_app )
238
+ response = client .request (method = method , url = path , headers = {})
239
+ assert response .status_code == expected_status
240
+
241
+
242
+ @pytest .mark .parametrize (
243
+ "path,method,default_public,private_endpoints,expected_status" ,
244
+ [
245
+ # Test that requests with valid auth succeed
246
+ ("/collections" , "GET" , False , {}, 200 ),
247
+ ("/collections" , "POST" , False , {}, 200 ),
248
+ ("/search" , "GET" , False , {}, 200 ),
249
+ ("/collections" , "OPTIONS" , False , {}, 200 ),
250
+ ("/search" , "OPTIONS" , False , {}, 200 ),
251
+ # Test with specific private endpoint configurations
252
+ (
253
+ "/collections" ,
254
+ "POST" ,
255
+ True ,
256
+ {r"^/collections$" : [("POST" , "collection:create" )]},
257
+ 200 ,
258
+ ),
259
+ (
260
+ "/collections" ,
261
+ "OPTIONS" ,
262
+ True ,
263
+ {r"^/collections$" : [("POST" , "collection:create" )]},
264
+ 200 ,
265
+ ),
266
+ ],
267
+ )
268
+ def test_options_vs_other_methods_with_valid_auth (
269
+ path ,
270
+ method ,
271
+ default_public ,
272
+ private_endpoints ,
273
+ expected_status ,
274
+ source_api_server ,
275
+ token_builder ,
276
+ ):
277
+ """Compare authentication behavior between OPTIONS and other HTTP methods with valid auth."""
278
+ test_app = app_factory (
279
+ upstream_url = source_api_server ,
280
+ default_public = default_public ,
281
+ private_endpoints = private_endpoints ,
282
+ )
283
+ valid_auth_token = token_builder ({"scope" : "collection:create" })
284
+ client = TestClient (test_app )
285
+ response = client .request (
286
+ method = method ,
287
+ url = path ,
288
+ headers = {"Authorization" : f"Bearer { valid_auth_token } " },
289
+ )
290
+ assert response .status_code == expected_status
291
+
292
+
293
+ @pytest .mark .parametrize (
294
+ "invalid_token,expected_status" ,
295
+ [
296
+ ("Bearer invalid-token" , 401 ),
297
+ (
298
+ "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" ,
299
+ 401 ,
300
+ ),
301
+ ("InvalidFormat" , 401 ),
302
+ ("Bearer" , 401 ),
303
+ ("" , 403 ), # No auth header returns 403, not 401
304
+ ],
305
+ )
306
+ def test_with_invalid_tokens_fails (invalid_token , expected_status , source_api_server ):
307
+ """GET requests should fail with invalid or malformed tokens."""
308
+ test_app = app_factory (
309
+ upstream_url = source_api_server ,
310
+ default_public = False , # All endpoints private
311
+ private_endpoints = {},
312
+ )
313
+ client = TestClient (test_app )
314
+ response = client .get ("/collections" , headers = {"Authorization" : invalid_token })
315
+ assert (
316
+ response .status_code == expected_status
317
+ ), f"GET request should fail with token: { invalid_token } "
318
+
319
+ response = client .options ("/collections" , headers = {"Authorization" : invalid_token })
320
+ assert (
321
+ response .status_code == 200
322
+ ), f"OPTIONS request should succeed with token: { invalid_token } "
323
+
324
+
325
+ def test_options_requests_with_cors_headers (source_api_server ):
326
+ """OPTIONS requests should work properly with CORS headers."""
327
+ test_app = app_factory (
328
+ upstream_url = source_api_server ,
329
+ default_public = False , # All endpoints private
330
+ private_endpoints = {},
331
+ )
332
+ client = TestClient (test_app )
333
+
334
+ # Test OPTIONS request with CORS headers
335
+ cors_headers = {
336
+ "Origin" : "https://example.com" ,
337
+ "Access-Control-Request-Method" : "POST" ,
338
+ "Access-Control-Request-Headers" : "Content-Type,Authorization" ,
339
+ }
340
+
341
+ response = client .options ("/collections" , headers = cors_headers )
342
+ assert (
343
+ response .status_code == 200
344
+ ), "OPTIONS request with CORS headers should succeed"
0 commit comments