@@ -587,6 +587,115 @@ async fn test_audit_log_coverage(ctx: &ControlPlaneTestContext) {
587587 }
588588 }
589589
590+ // Exercise endpoints not in VERIFY_ENDPOINTS. These require special
591+ // handling (unauthenticated, non-JSON bodies, etc.) that prevents them
592+ // from being included in the generic loop above.
593+ //
594+ // For each endpoint we derive the operation_id from the URL via the
595+ // API spec (api_operations.find) rather than hardcoding it, so that a
596+ // URL change or mix-up is caught automatically.
597+
598+ // Make a request and check whether an audit log entry was produced.
599+ // Looks up the operation_id from the URL via the API spec rather than
600+ // hardcoding it, so a URL change or mix-up is caught automatically.
601+ // The builder is wrapped in NexusRequest with UnprivilegedUser auth,
602+ // which is harmless for unauthenticated endpoints (they ignore it).
603+ // Panics if the URL doesn't match any API operation.
604+ let mut check_manual =
605+ async |method : & str , url : & str , builder : RequestBuilder < ' _ > | {
606+ let before = fetch_log ( client, t_start, None ) . await . items . len ( ) ;
607+ let _ = NexusRequest :: new (
608+ builder. allow_non_dropshot_errors ( ) . expect_status ( None ) ,
609+ )
610+ . authn_as ( AuthnMode :: UnprivilegedUser )
611+ . execute ( )
612+ . await ;
613+ let after = fetch_log ( client, t_start, None ) . await . items . len ( ) ;
614+ let op = api_operations. find ( method, url) . unwrap_or_else ( || {
615+ panic ! ( "{} {} does not match any API operation" , method, url)
616+ } ) ;
617+ if let Some ( info) = untested_mutating. remove ( & op. operation_id ) {
618+ if after <= before {
619+ missing_audit. insert ( op. operation_id . clone ( ) , info) ;
620+ }
621+ }
622+ } ;
623+
624+ // login_local: unauthenticated, JSON body
625+ check_manual (
626+ "POST" ,
627+ "/v1/login/fake-silo/local" ,
628+ RequestBuilder :: new (
629+ client,
630+ Method :: POST ,
631+ "/v1/login/fake-silo/local" ,
632+ )
633+ . body ( Some ( & serde_json:: json!( {
634+ "username" : "nonexistent" ,
635+ "password" : "doesntmatter"
636+ } ) ) ) ,
637+ )
638+ . await ;
639+
640+ // login_saml: unauthenticated, takes UntypedBody (any bytes work)
641+ check_manual (
642+ "POST" ,
643+ "/login/fake-silo/saml/fake-provider" ,
644+ RequestBuilder :: new (
645+ client,
646+ Method :: POST ,
647+ "/login/fake-silo/saml/fake-provider" ,
648+ )
649+ . body ( Some ( & serde_json:: json!( { } ) ) ) ,
650+ )
651+ . await ;
652+
653+ // logout: session cookie-based, no body needed
654+ check_manual (
655+ "POST" ,
656+ "/v1/logout" ,
657+ RequestBuilder :: new ( client, Method :: POST , "/v1/logout" ) ,
658+ )
659+ . await ;
660+
661+ // device_auth_request: unauthenticated, URL-encoded body
662+ check_manual (
663+ "POST" ,
664+ "/device/auth" ,
665+ RequestBuilder :: new ( client, Method :: POST , "/device/auth" )
666+ . body_urlencoded ( Some ( & device:: DeviceAuthRequest {
667+ client_id : uuid:: Uuid :: nil ( ) ,
668+ ttl_seconds : None ,
669+ } ) ) ,
670+ )
671+ . await ;
672+
673+ // device_auth_confirm: authenticated, JSON body
674+ check_manual (
675+ "POST" ,
676+ "/device/confirm" ,
677+ RequestBuilder :: new ( client, Method :: POST , "/device/confirm" )
678+ . body ( Some ( & device:: DeviceAuthVerify {
679+ user_code : "fake-code" . to_string ( ) ,
680+ } ) ) ,
681+ )
682+ . await ;
683+
684+ // device_access_token: unauthenticated, URL-encoded body
685+ check_manual (
686+ "POST" ,
687+ "/device/token" ,
688+ RequestBuilder :: new ( client, Method :: POST , "/device/token" )
689+ . body_urlencoded ( Some ( & device:: DeviceAccessTokenRequest {
690+ grant_type :
691+ "urn:ietf:params:oauth:grant-type:device_code"
692+ . to_string ( ) ,
693+ device_code : "fake-code" . to_string ( ) ,
694+ client_id : uuid:: Uuid :: nil ( ) ,
695+ } ) ) ,
696+ )
697+ . await ;
698+
590699 let mut output =
591700 String :: from ( "Mutating endpoints without audit logging:\n " ) ;
592701 for ( op_id, ( method, path) ) in & missing_audit {
0 commit comments