@@ -394,6 +394,222 @@ mod tests {
394394 file_content,
395395 "Downloaded back file content"
396396 ) ;
397+ #[ actix_web:: test]
398+ async fn test_refresh_endpoint ( ) -> Result < ( ) > {
399+ // Initialize the app
400+ let path = TmpDir :: new ( "test_api_repo_file_operations" ) . await ?;
401+
402+ // Initialize backend2 first
403+ let store2 = iroh_blobs:: store:: fs:: Store :: load ( path. to_path_buf ( ) . join ( "iroh2" ) ) . await ?;
404+ let ( veilid_api2, update_rx2) = save_dweb_backend:: common:: init_veilid (
405+ path. to_path_buf ( ) . join ( "test2" ) . as_path ( ) ,
406+ "test2" . to_string ( ) ,
407+ )
408+ . await ?;
409+ let backend2 = save_dweb_backend:: backend:: Backend :: from_dependencies (
410+ & path. to_path_buf ( ) ,
411+ veilid_api2. clone ( ) ,
412+ update_rx2,
413+ store2,
414+ )
415+ . await
416+ . unwrap ( ) ;
417+
418+ // Initialize the main backend after backend2
419+ BACKEND . get_or_init ( || init_backend ( path. to_path_buf ( ) . as_path ( ) ) ) ;
420+ {
421+ let backend = get_backend ( ) . await ?;
422+ backend. start ( ) . await . expect ( "Backend failed to start" ) ;
423+ }
424+
425+ // Create group and repo in backend2
426+ let mut group = backend2. create_group ( ) . await ?;
427+ let join_url = group. get_url ( ) ;
428+ group. set_name ( TEST_GROUP_NAME ) . await ?;
429+
430+ let repo = group. create_repo ( ) . await ?;
431+ repo. set_name ( TEST_GROUP_NAME ) . await ?;
432+
433+ // Upload a file
434+ let file_name = "example.txt" ;
435+ let file_content = b"Test content for file upload" ;
436+ repo. upload ( & file_name, file_content. to_vec ( ) ) . await ?;
437+
438+ // Give the upload some time to propagate through the network
439+ // Simple polling function with backoff
440+ async fn poll_until_success (
441+ attempts : u32 ,
442+ base_delay_ms : u64 ,
443+ max_delay_ms : u64 ,
444+ check_condition : impl Fn ( ) -> Result < bool >
445+ ) -> Result < ( ) > {
446+ let mut delay_ms = base_delay_ms;
447+ for attempt in 0 ..attempts {
448+ if check_condition ( ) ? {
449+ return Ok ( ( ) ) ;
450+ }
451+
452+ if attempt < attempts - 1 {
453+ let jitter = ( attempt as u64 * 37 ) % ( delay_ms / 4 ) ;
454+ let sleep_duration = std:: cmp:: min (
455+ delay_ms + jitter,
456+ max_delay_ms
457+ ) ;
458+
459+ tokio:: time:: sleep ( Duration :: from_millis ( sleep_duration) ) . await ;
460+ delay_ms = std:: cmp:: min ( delay_ms * 2 , max_delay_ms) ;
461+ }
462+ }
463+
464+ anyhow:: bail!( "Operation timed out after {} attempts" , attempts)
465+ }
466+
467+ // Initialize the app
468+ let app = test:: init_service (
469+ App :: new ( )
470+ . service ( status)
471+ . service ( web:: scope ( "/api" ) . service ( groups:: scope ( ) ) ) ,
472+ )
473+ . await ;
474+
475+ // Join the group from the first backend and wait for it to be ready
476+ {
477+ let backend = get_backend ( ) . await ?;
478+ backend. join_from_url ( join_url. as_str ( ) ) . await ?;
479+
480+ // Using a simpler approach with manual retries
481+ let group_id = group. id ( ) . clone ( ) ;
482+ let mut found = false ;
483+
484+ for attempt in 0 ..10 {
485+ match backend. list_groups ( ) . await {
486+ Ok ( groups) => {
487+ if groups. iter ( ) . any ( |g| g. id ( ) == group_id) {
488+ found = true ;
489+ break ;
490+ }
491+ eprintln ! ( "Group not found yet (attempt {}), waiting..." , attempt + 1 ) ;
492+ }
493+ Err ( e) => {
494+ eprintln ! ( "Error checking groups (attempt {}): {}" , attempt + 1 , e) ;
495+ }
496+ }
497+
498+ let delay = 500 * ( 1 << attempt. min ( 5 ) ) ; // Exponential backoff, max 16s
499+ tokio:: time:: sleep ( Duration :: from_millis ( delay) ) . await ;
500+ }
501+
502+ if !found {
503+ anyhow:: bail!( "Failed to verify group join completion after multiple attempts" ) ;
504+ }
505+ }
506+
507+ // Call refresh endpoint and handle response
508+ let group_id_str = group. id ( ) . to_string ( ) ;
509+ let mut refresh_data = serde_json:: Value :: Null ;
510+
511+ // Make multiple attempts for the refresh operation
512+ for attempt in 0 ..5 {
513+ // Create a fresh request each time since they can't be reused
514+ let refresh_req = test:: TestRequest :: post ( )
515+ . uri ( & format ! ( "/api/groups/{}/refresh" , group_id_str) )
516+ . to_request ( ) ;
517+
518+ let refresh_resp = test:: call_service ( & app, refresh_req) . await ;
519+ if !refresh_resp. status ( ) . is_success ( ) {
520+ eprintln ! ( "Refresh failed with status: {} (attempt {})" , refresh_resp. status( ) , attempt + 1 ) ;
521+ tokio:: time:: sleep ( Duration :: from_millis ( 1000 * ( attempt + 1 ) ) ) . await ;
522+ continue ;
523+ }
524+
525+ // Try to parse the response body
526+ let body = test:: read_body ( refresh_resp) . await ;
527+ match serde_json:: from_slice :: < serde_json:: Value > ( & body) {
528+ Ok ( data) => {
529+ refresh_data = data;
530+ if refresh_data[ "status" ] == "success" {
531+ if let Some ( refreshed_files) = refresh_data[ "refreshed_files" ] . as_array ( ) {
532+ if refreshed_files. iter ( ) . any ( |f| f. as_str ( ) == Some ( file_name) ) {
533+ break ; // Success case
534+ }
535+ }
536+ }
537+ eprintln ! ( "Refresh response not as expected (attempt {}): {:?}" , attempt + 1 , refresh_data) ;
538+ }
539+ Err ( e) => {
540+ eprintln ! ( "Error parsing refresh response (attempt {}): {}" , attempt + 1 , e) ;
541+ }
542+ }
543+
544+ // Wait before retrying
545+ tokio:: time:: sleep ( Duration :: from_millis ( 1000 * ( attempt + 1 ) ) ) . await ;
546+ }
547+
548+ // Check if we got a successful response from any attempt
549+ assert_eq ! ( refresh_data[ "status" ] , "success" , "Refresh should return success after multiple attempts" ) ;
550+
551+ // Verify file is accessible with retry logic
552+ let repo_id_str = repo. id ( ) . to_string ( ) ;
553+ let mut got_file_data = Vec :: new ( ) ;
554+ let file_name_clone = file_name. to_string ( ) ;
555+
556+ // Manual retry approach without capturing app
557+ for attempt in 0 ..10 {
558+ // Create a fresh request for each attempt
559+ let get_file_req = test:: TestRequest :: get ( )
560+ . uri ( & format ! (
561+ "/api/groups/{}/repos/{}/media/{}" ,
562+ group_id_str, repo_id_str, file_name_clone
563+ ) )
564+ . to_request ( ) ;
565+
566+ let get_file_resp = test:: call_service ( & app, get_file_req) . await ;
567+ if !get_file_resp. status ( ) . is_success ( ) {
568+ eprintln ! ( "File not yet available, status: {} (attempt {})" ,
569+ get_file_resp. status( ) , attempt + 1 ) ;
570+ tokio:: time:: sleep ( Duration :: from_millis ( 1000 * ( attempt + 1 ) ) ) . await ;
571+ continue ;
572+ }
573+
574+ got_file_data = test:: read_body ( get_file_resp) . await . to_vec ( ) ;
575+ if got_file_data. as_slice ( ) == file_content {
576+ break ; // Success - exit the retry loop
577+ } else {
578+ eprintln ! ( "File content mismatch (attempt {}), retrying..." , attempt + 1 ) ;
579+ tokio:: time:: sleep ( Duration :: from_millis ( 1000 * ( attempt + 1 ) ) ) . await ;
580+ }
581+ }
582+
583+ // Final verification
584+ assert_eq ! (
585+ got_file_data. as_slice( ) ,
586+ file_content,
587+ "Downloaded file content should match"
588+ ) ;
589+
590+ // Verify the refresh response had the expected format
591+ let refreshed_files = refresh_data[ "refreshed_files" ]
592+ . as_array ( )
593+ . expect ( "refreshed_files should be an array" ) ;
594+
595+ assert ! (
596+ refreshed_files. iter( ) . any( |f| f. as_str( ) == Some ( file_name) ) ,
597+ "File should be in refreshed_files list"
598+ ) ;
599+
600+ // Clean up in reverse order of initialization - with grace periods
601+ backend2. stop ( ) . await ?;
602+ {
603+ let backend = get_backend ( ) . await ?;
604+ backend. stop ( ) . await . expect ( "Backend failed to stop" ) ;
605+ }
606+
607+ // Allow time for clean shutdown
608+ tokio:: time:: sleep ( Duration :: from_secs ( 1 ) ) . await ;
609+ veilid_api2. shutdown ( ) . await ;
610+
611+ Ok ( ( ) )
612+ }
397613
398614 Ok ( ( ) )
399615 }
0 commit comments