@@ -167,3 +167,178 @@ def test_scopes(
167167 )
168168 expected_status_code = 200 if expected_permitted else 401
169169 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