Skip to content

Commit 2fdb508

Browse files
committed
Add refresh endpoint test to verify group data updates
1 parent 3a18a82 commit 2fdb508

1 file changed

Lines changed: 216 additions & 0 deletions

File tree

src/lib.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)