@@ -469,6 +469,61 @@ pub async fn pages_get(cfg: &Config, page_id: &str) -> Result<()> {
469469 formatter:: output ( cfg, & resp)
470470}
471471
472+ /// Lists on-call pages, optionally filtered by team handle (server-side) and
473+ /// responder user id (client-side).
474+ ///
475+ /// Uses `raw_client::raw_get` against the unstable endpoint because
476+ /// `datadog-api-client` exposes no list binding, and the stable v2 collection
477+ /// endpoint currently returns an empty body.
478+ pub async fn pages_list (
479+ cfg : & Config ,
480+ team : Option < & str > ,
481+ responder : Option < & str > ,
482+ page_size : u32 ,
483+ ) -> Result < ( ) > {
484+ if !( 1 ..=1000 ) . contains ( & page_size) {
485+ anyhow:: bail!( "invalid page_size: {page_size}. Expected a value from 1 to 1000" ) ;
486+ }
487+
488+ let page_size = page_size. to_string ( ) ;
489+ let team_filter = team. map ( |t| format ! ( "team:{t}" ) ) ;
490+ let mut query = vec ! [ ( "page[size]" , page_size. as_str( ) ) ] ;
491+ if let Some ( filter) = team_filter. as_deref ( ) {
492+ query. push ( ( "filter" , filter) ) ;
493+ }
494+
495+ let mut resp = raw_client:: raw_get ( cfg, "/api/unstable/on-call/pages" , & query)
496+ . await
497+ . map_err ( |e| anyhow:: anyhow!( "failed to list pages: {e:?}" ) ) ?;
498+
499+ if let Some ( responder) = responder {
500+ filter_pages_by_responder ( & mut resp, responder) ;
501+ }
502+
503+ formatter:: output ( cfg, & resp)
504+ }
505+
506+ fn filter_pages_by_responder ( resp : & mut serde_json:: Value , responder : & str ) {
507+ if let Some ( pages) = resp
508+ . get_mut ( "data" )
509+ . and_then ( serde_json:: Value :: as_array_mut)
510+ {
511+ pages. retain ( |page| page_has_responder ( page, responder) ) ;
512+ }
513+ }
514+
515+ fn page_has_responder ( page : & serde_json:: Value , responder : & str ) -> bool {
516+ page. get ( "relationships" )
517+ . and_then ( |relationships| relationships. get ( "responders" ) )
518+ . and_then ( |responders| responders. get ( "data" ) )
519+ . and_then ( serde_json:: Value :: as_array)
520+ . is_some_and ( |responders| {
521+ responders. iter ( ) . any ( |responder_ref| {
522+ responder_ref. get ( "id" ) . and_then ( serde_json:: Value :: as_str) == Some ( responder)
523+ } )
524+ } )
525+ }
526+
472527#[ cfg( test) ]
473528mod tests {
474529 use crate :: test_support:: * ;
@@ -865,6 +920,88 @@ mod tests {
865920 cleanup_env ( ) ;
866921 }
867922
923+ #[ tokio:: test]
924+ async fn test_on_call_pages_list_by_team ( ) {
925+ let _lock = lock_env ( ) . await ;
926+ let mut s = mockito:: Server :: new_async ( ) . await ;
927+ let cfg = test_config ( & s. url ( ) ) ;
928+ let mock = s
929+ . mock ( "GET" , "/api/unstable/on-call/pages" )
930+ . match_query ( mockito:: Matcher :: AllOf ( vec ! [
931+ mockito:: Matcher :: UrlEncoded ( "page[size]" . into( ) , "42" . into( ) ) ,
932+ mockito:: Matcher :: UrlEncoded ( "filter" . into( ) , "team:core-platform" . into( ) ) ,
933+ ] ) )
934+ . with_status ( 200 )
935+ . with_header ( "content-type" , "application/json" )
936+ . with_body ( r#"{"data": []}"# )
937+ . create_async ( )
938+ . await ;
939+ let result = super :: pages_list ( & cfg, Some ( "core-platform" ) , None , 42 ) . await ;
940+ assert ! ( result. is_ok( ) , "pages_list failed: {:?}" , result. err( ) ) ;
941+ mock. assert_async ( ) . await ;
942+ cleanup_env ( ) ;
943+ }
944+
945+ #[ tokio:: test]
946+ async fn test_on_call_pages_list_rejects_invalid_page_size ( ) {
947+ let _lock = lock_env ( ) . await ;
948+ let cfg = test_config ( "http://unused.local" ) ;
949+ let result = super :: pages_list ( & cfg, None , None , 0 ) . await ;
950+ assert ! ( result. is_err( ) ) ;
951+ assert ! ( result
952+ . unwrap_err( )
953+ . to_string( )
954+ . contains( "invalid page_size" ) ) ;
955+ cleanup_env ( ) ;
956+ }
957+
958+ #[ test]
959+ fn test_page_has_responder_matches_and_rejects ( ) {
960+ let page = serde_json:: json!( {
961+ "relationships" : {
962+ "responders" : {
963+ "data" : [
964+ { "id" : "user-1" , "type" : "users" } ,
965+ { "id" : "user-2" , "type" : "users" }
966+ ]
967+ }
968+ }
969+ } ) ;
970+
971+ assert ! ( super :: page_has_responder( & page, "user-2" ) ) ;
972+ assert ! ( !super :: page_has_responder( & page, "user-3" ) ) ;
973+ }
974+
975+ #[ test]
976+ fn test_filter_pages_by_responder ( ) {
977+ let mut resp = serde_json:: json!( {
978+ "data" : [
979+ {
980+ "id" : "page-1" ,
981+ "relationships" : {
982+ "responders" : {
983+ "data" : [ { "id" : "user-1" , "type" : "users" } ]
984+ }
985+ }
986+ } ,
987+ {
988+ "id" : "page-2" ,
989+ "relationships" : {
990+ "responders" : {
991+ "data" : [ { "id" : "user-2" , "type" : "users" } ]
992+ }
993+ }
994+ } ,
995+ { "id" : "page-3" }
996+ ]
997+ } ) ;
998+
999+ super :: filter_pages_by_responder ( & mut resp, "user-2" ) ;
1000+ let pages = resp[ "data" ] . as_array ( ) . unwrap ( ) ;
1001+ assert_eq ! ( pages. len( ) , 1 ) ;
1002+ assert_eq ! ( pages[ 0 ] [ "id" ] , "page-2" ) ;
1003+ }
1004+
8681005 #[ tokio:: test]
8691006 async fn test_memberships_list_invalid_sort ( ) {
8701007 let cfg = test_config ( "http://unused.local" ) ;
0 commit comments