@@ -508,6 +508,97 @@ async fn mock_zap_server_for_default_ajax_spider_timeout() -> MockServer {
508508 server
509509}
510510
511+ async fn mock_zap_server_for_spider_timeout_grace_stop_request ( ) -> MockServer {
512+ let server = MockServer :: start ( ) . await ;
513+
514+ Mock :: given ( method ( "POST" ) )
515+ . and ( path ( "/JSON/context/action/newContext" ) )
516+ . respond_with (
517+ ResponseTemplate :: new ( 200 ) . set_body_raw ( r#"{"contextId":"ctx-1"}"# , "application/json" ) ,
518+ )
519+ . mount ( & server)
520+ . await ;
521+
522+ Mock :: given ( method ( "POST" ) )
523+ . and ( path ( "/JSON/context/action/includeInContext" ) )
524+ . respond_with (
525+ ResponseTemplate :: new ( 200 ) . set_body_raw ( r#"{"Result":"OK"}"# , "application/json" ) ,
526+ )
527+ . mount ( & server)
528+ . await ;
529+
530+ Mock :: given ( method ( "POST" ) )
531+ . and ( path ( "/JSON/ajaxSpider/action/setOptionMaxDuration" ) )
532+ . and ( body_string_contains ( "Integer=1" ) )
533+ . respond_with (
534+ ResponseTemplate :: new ( 200 ) . set_body_raw ( r#"{"Result":"OK"}"# , "application/json" ) ,
535+ )
536+ . expect ( 1 )
537+ . mount ( & server)
538+ . await ;
539+
540+ Mock :: given ( method ( "POST" ) )
541+ . and ( path ( "/JSON/ajaxSpider/action/scan" ) )
542+ . respond_with (
543+ ResponseTemplate :: new ( 200 ) . set_body_raw ( r#"{"Result":"OK"}"# , "application/json" ) ,
544+ )
545+ . expect ( 1 )
546+ . mount ( & server)
547+ . await ;
548+
549+ Mock :: given ( method ( "POST" ) )
550+ . and ( path ( "/JSON/ajaxSpider/view/status" ) )
551+ . respond_with (
552+ ResponseTemplate :: new ( 200 ) . set_body_raw ( r#"{"status":"running"}"# , "application/json" ) ,
553+ )
554+ . mount ( & server)
555+ . await ;
556+
557+ Mock :: given ( method ( "POST" ) )
558+ . and ( path ( "/JSON/ajaxSpider/action/stop" ) )
559+ . respond_with (
560+ ResponseTemplate :: new ( 200 ) . set_body_raw ( r#"{"Result":"OK"}"# , "application/json" ) ,
561+ )
562+ . mount ( & server)
563+ . await ;
564+
565+ Mock :: given ( method ( "POST" ) )
566+ . and ( path ( "/JSON/ascan/action/scan" ) )
567+ . respond_with (
568+ ResponseTemplate :: new ( 200 ) . set_body_raw ( r#"{"scan":"active-1"}"# , "application/json" ) ,
569+ )
570+ . expect ( 0 )
571+ . mount ( & server)
572+ . await ;
573+
574+ Mock :: given ( method ( "POST" ) )
575+ . and ( path ( "/JSON/ascan/view/status" ) )
576+ . respond_with (
577+ ResponseTemplate :: new ( 200 ) . set_body_raw ( r#"{"status":"100"}"# , "application/json" ) ,
578+ )
579+ . expect ( 0 )
580+ . mount ( & server)
581+ . await ;
582+
583+ Mock :: given ( method ( "POST" ) )
584+ . and ( path ( "/JSON/alert/view/alerts" ) )
585+ . respond_with (
586+ ResponseTemplate :: new ( 200 ) . set_body_raw ( r#"{"alerts":[]}"# , "application/json" ) ,
587+ )
588+ . mount ( & server)
589+ . await ;
590+
591+ Mock :: given ( method ( "POST" ) )
592+ . and ( path ( "/JSON/context/action/removeContext" ) )
593+ . respond_with (
594+ ResponseTemplate :: new ( 200 ) . set_body_raw ( r#"{"Result":"OK"}"# , "application/json" ) ,
595+ )
596+ . mount ( & server)
597+ . await ;
598+
599+ server
600+ }
601+
511602async fn mock_zap_server_with_active_status_error ( ) -> MockServer {
512603 let server = MockServer :: start ( ) . await ;
513604
@@ -1208,6 +1299,46 @@ async fn runtime_applies_default_ajax_spider_timeout_when_preference_is_omitted(
12081299 assert_eq ! ( scan. status, ScanStatus :: Succeeded ) ;
12091300}
12101301
1302+ #[ tokio:: test]
1303+ async fn runtime_stops_ajax_spider_when_timeout_plus_grace_period_is_exceeded ( ) {
1304+ let ( storage, _temp_dir) = temporary_sqlite_storage ( ) . await . unwrap ( ) ;
1305+ let server = mock_zap_server_for_spider_timeout_grace_stop_request ( ) . await ;
1306+ let zap_client = ZapClient :: new ( server. uri ( ) , "test-api-key" . to_string ( ) ) . unwrap ( ) ;
1307+ let runtime = start_scan_runtime (
1308+ storage. clone ( ) ,
1309+ zap_client,
1310+ ScanRuntimeConfig {
1311+ worker_count : 1 ,
1312+ alert_poll_interval : Duration :: from_millis ( 1 ) ,
1313+ scan_poll_interval : Duration :: from_millis ( 20 ) ,
1314+ alert_page_size : 100 ,
1315+ passive_scan_placeholder_duration : Duration :: from_millis ( 1 ) ,
1316+ ajax_spider_timeout_grace_period : Duration :: from_millis ( 50 ) ,
1317+ stop_grace_period : Duration :: from_secs ( 5 ) ,
1318+ ..ScanRuntimeConfig :: default ( )
1319+ } ,
1320+ ) ;
1321+ let service = DefaultScanService :: new ( storage. clone ( ) , runtime) ;
1322+
1323+ let scan_id = service
1324+ . create_scan ( make_safe_mode_request_with_ajax_timeout (
1325+ "https://example.test" ,
1326+ 1 ,
1327+ ) )
1328+ . await
1329+ . unwrap ( ) ;
1330+
1331+ service. start_scan ( & scan_id) . await . unwrap ( ) ;
1332+ wait_for_running ( storage. as_ref ( ) , & scan_id) . await ;
1333+ wait_for_request_path ( & server, "/JSON/ajaxSpider/action/stop" ) . await ;
1334+
1335+ service. stop_scan ( & scan_id) . await . unwrap ( ) ;
1336+ wait_for_status ( storage. as_ref ( ) , & scan_id, ScanStatus :: Stopped ) . await ;
1337+
1338+ let scan = storage. get_scan ( & scan_id) . await . unwrap ( ) ;
1339+ assert_eq ! ( scan. status, ScanStatus :: Stopped ) ;
1340+ }
1341+
12111342#[ tokio:: test]
12121343async fn runtime_transitions_running_scan_to_failed_on_worker_error ( ) {
12131344 let ( storage, _temp_dir) = temporary_sqlite_storage ( ) . await . unwrap ( ) ;
0 commit comments