@@ -213,6 +213,112 @@ void apiKeyMissingReturnsApiKeyChallengeHeader() throws Exception {
213213 .isEqualTo ("ApiKey location=header, name=\" X-API-Key\" " );
214214 }
215215
216+ @ Test
217+ void andGroupRequiresAllSchemesToSucceed () throws Exception {
218+ // Group requires both apiKeyAuth AND bearerAuth.
219+ Operation op =
220+ new Operation (
221+ "getX" ,
222+ HttpMethod .GET ,
223+ null ,
224+ Optional .empty (),
225+ List .of (),
226+ Map .of (),
227+ Map .of (),
228+ Optional .of (
229+ List .of (
230+ new SecurityRequirement (
231+ Map .of ("apiKeyAuth" , List .of (), "bearerAuth" , List .of ())))));
232+
233+ Map <String , SecurityScheme > schemes =
234+ Map .of (
235+ "apiKeyAuth" , new ApiKey ("X-API-Key" , Location .HEADER ),
236+ "bearerAuth" , new HttpBearer (Optional .empty ()));
237+
238+ Map <String , SchemeValidator > validators =
239+ Map .of (
240+ "apiKeyAuth" , (req , cred ) -> Optional .of ("api-principal" ),
241+ "bearerAuth" , (req , cred ) -> Optional .of ("bearer-principal" ));
242+
243+ SecurityFilter filter =
244+ new SecurityFilter (Map .of ("getX" , op ), schemes , List .of (), validators , false );
245+
246+ HttpExchange ex = mock (HttpExchange .class );
247+ Headers headers = new Headers ();
248+ headers .add ("X-API-Key" , "abc" );
249+ headers .add ("Authorization" , "Bearer token" );
250+ when (ex .getRequestHeaders ()).thenReturn (headers );
251+ when (ex .getRequestURI ()).thenReturn (URI .create ("http://h/getX" ));
252+
253+ Map <String , Object > captured = new java .util .HashMap <>();
254+ Chain chain = mock (Chain .class );
255+ doAnswer (
256+ inv -> {
257+ captured .putAll (DispatchHandler .CURRENT .get ().principals ());
258+ return null ;
259+ })
260+ .when (chain )
261+ .doFilter (ex );
262+
263+ ScopedValueHarness .runWith (newMinimalRequest ("getX" ), () -> filter .doFilter (ex , chain ));
264+
265+ assertThat (captured )
266+ .containsEntry ("apiKeyAuth" , "api-principal" )
267+ .containsEntry ("bearerAuth" , "bearer-principal" );
268+ }
269+
270+ @ Test
271+ void orFallsBackToSecondGroupWhenFirstDenied () throws Exception {
272+ // [{apiKeyAuth}, {bearerAuth}] — first group denied, second allowed.
273+ Operation op =
274+ new Operation (
275+ "getX" ,
276+ HttpMethod .GET ,
277+ null ,
278+ Optional .empty (),
279+ List .of (),
280+ Map .of (),
281+ Map .of (),
282+ Optional .of (
283+ List .of (
284+ new SecurityRequirement (Map .of ("apiKeyAuth" , List .of ())),
285+ new SecurityRequirement (Map .of ("bearerAuth" , List .of ())))));
286+
287+ Map <String , SecurityScheme > schemes =
288+ Map .of (
289+ "apiKeyAuth" , new ApiKey ("X-API-Key" , Location .HEADER ),
290+ "bearerAuth" , new HttpBearer (Optional .empty ()));
291+
292+ Map <String , SchemeValidator > validators =
293+ Map .of (
294+ "apiKeyAuth" , (req , cred ) -> Optional .empty (),
295+ "bearerAuth" , (req , cred ) -> Optional .of ("bearer-ok" ));
296+
297+ SecurityFilter filter =
298+ new SecurityFilter (Map .of ("getX" , op ), schemes , List .of (), validators , false );
299+
300+ HttpExchange ex = mock (HttpExchange .class );
301+ Headers headers = new Headers ();
302+ headers .add ("X-API-Key" , "bad" );
303+ headers .add ("Authorization" , "Bearer token" );
304+ when (ex .getRequestHeaders ()).thenReturn (headers );
305+ when (ex .getRequestURI ()).thenReturn (URI .create ("http://h/getX" ));
306+
307+ Map <String , Object > captured = new java .util .HashMap <>();
308+ Chain chain = mock (Chain .class );
309+ doAnswer (
310+ inv -> {
311+ captured .putAll (DispatchHandler .CURRENT .get ().principals ());
312+ return null ;
313+ })
314+ .when (chain )
315+ .doFilter (ex );
316+
317+ ScopedValueHarness .runWith (newMinimalRequest ("getX" ), () -> filter .doFilter (ex , chain ));
318+
319+ assertThat (captured ).containsEntry ("bearerAuth" , "bearer-ok" ).doesNotContainKey ("apiKeyAuth" );
320+ }
321+
216322 private static Request newMinimalRequest (String operationId ) {
217323 return new Request (new byte [0 ], null , null , operationId , Map .of (), null , h -> null );
218324 }
0 commit comments