Skip to content

Commit 7de4993

Browse files
committed
Harden joined-group refresh test convergence logic
1 parent 59d1f84 commit 7de4993

1 file changed

Lines changed: 104 additions & 40 deletions

File tree

src/lib.rs

Lines changed: 104 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,72 +1039,130 @@ mod tests {
10391039
backend.join_from_url(join_url.as_str()).await?;
10401040
}
10411041

1042-
// Wait for replication; then retry refresh until P2P converges (same pattern as save-dweb-backend).
1043-
// Propagation can be slow in CI/P2P environments.
1044-
tokio::time::sleep(Duration::from_secs(10)).await;
1045-
1046-
// Retry refresh until P2P replication converges: the refresh must succeed AND
1047-
// the response must contain the creator's repo (by name) with the uploaded file.
1048-
// Repo names and file lists propagate via DHT and may lag behind the initial join.
1049-
let mut refresh_retries = 12;
1042+
// Wait for replication and then run convergence in two phases:
1043+
// A) metadata convergence (group + repo visible on joiner)
1044+
// B) content convergence (file downloadable on joiner)
1045+
// This reduces flakiness caused by asking refresh to do all work while relays are unstable.
1046+
tokio::time::sleep(Duration::from_secs(6)).await;
1047+
1048+
{
1049+
let mut retries = 20;
1050+
loop {
1051+
let groups_req = test::TestRequest::get().uri("/api/groups").to_request();
1052+
let groups_resp: GroupsResponse =
1053+
test::call_and_read_body_json(&app, groups_req).await;
1054+
let repos_req = test::TestRequest::get()
1055+
.uri(&format!("/api/groups/{}/repos", group.id()))
1056+
.to_request();
1057+
let repos_resp: ReposResponse =
1058+
test::call_and_read_body_json(&app, repos_req).await;
1059+
1060+
let has_group = groups_resp
1061+
.groups
1062+
.iter()
1063+
.any(|g| g.key == group.id().to_string());
1064+
let has_any_repo = !repos_resp.repos.is_empty();
1065+
let ok = has_group && has_any_repo;
1066+
1067+
if ok {
1068+
break;
1069+
}
1070+
1071+
retries -= 1;
1072+
if retries == 0 {
1073+
panic!(
1074+
"Refresh-joined metadata did not converge after retries. groups: {}, repos: {}",
1075+
groups_resp.groups.len(),
1076+
repos_resp.repos.len(),
1077+
);
1078+
}
1079+
tokio::time::sleep(Duration::from_secs(5)).await;
1080+
}
1081+
};
1082+
1083+
// Retry refresh until it returns the expected repo + file view.
1084+
let mut refresh_retries = 20;
10501085
let refresh_data: serde_json::Value = loop {
10511086
let refresh_req = test::TestRequest::post()
10521087
.uri(&format!("/api/groups/{}/refresh", group.id()))
10531088
.to_request();
10541089
let resp = test::call_service(&app, refresh_req).await;
1090+
let resp_status = resp.status();
10551091

1056-
if resp.status().is_success() {
1092+
if resp_status.is_success() {
10571093
let data: serde_json::Value = test::read_body_json(resp).await;
1058-
// Check if the expected repo with the correct name and file is present
1059-
let has_expected_repo = data["repos"].as_array()
1060-
.and_then(|repos| repos.iter().find(|r| r["name"] == TEST_GROUP_NAME))
1061-
.and_then(|repo| repo["all_files"].as_array())
1062-
.map(|files| files.iter().any(|f| f.as_str() == Some(file_name)))
1094+
let has_repo_with_file = data["repos"]
1095+
.as_array()
1096+
.map(|repos| {
1097+
repos.iter().any(|repo| {
1098+
repo["all_files"]
1099+
.as_array()
1100+
.map(|files| files.iter().any(|f| f.as_str() == Some(file_name)))
1101+
.unwrap_or(false)
1102+
})
1103+
})
10631104
.unwrap_or(false);
10641105

1065-
if has_expected_repo {
1106+
if has_repo_with_file {
10661107
break data;
10671108
}
1068-
log::warn!("Refresh succeeded but repo data not yet propagated (attempt {})",
1069-
12 - refresh_retries + 1);
1109+
log::warn!(
1110+
"Refresh succeeded but expected repo/file not yet visible (attempt {})",
1111+
21 - refresh_retries
1112+
);
10701113
} else {
1071-
log::warn!("Refresh failed (attempt {}): status={}, body={:?}",
1072-
12 - refresh_retries + 1,
1073-
resp.status(),
1074-
test::read_body(resp).await
1114+
let body = test::read_body(resp).await;
1115+
let body_text = String::from_utf8_lossy(&body).to_string();
1116+
let is_retryable = body_text.contains("couldn't look up relay")
1117+
|| body_text.contains("Unable to open group DHT record")
1118+
|| body_text.contains("Group not found:");
1119+
log::warn!(
1120+
"Refresh failed (attempt {}): status={}, body={}",
1121+
21 - refresh_retries,
1122+
resp_status,
1123+
body_text
10751124
);
1125+
if !is_retryable {
1126+
panic!(
1127+
"Refresh failed with non-retryable error. status={resp_status}, body={body_text}"
1128+
);
1129+
}
10761130
}
10771131

10781132
refresh_retries -= 1;
10791133
if refresh_retries == 0 {
1080-
panic!("Refresh did not converge after 12 attempts.");
1134+
panic!("Refresh did not converge after 20 attempts.");
10811135
}
1082-
tokio::time::sleep(Duration::from_secs(4)).await;
1136+
tokio::time::sleep(Duration::from_secs(5)).await;
10831137
};
10841138

10851139
assert_eq!(refresh_data["status"], "success", "First refresh status should be success");
10861140

10871141
let repos = refresh_data["repos"].as_array().expect("repos should be an array");
10881142
assert!(!repos.is_empty(), "Should have at least one repo after joining");
10891143

1090-
let repo_data = repos.iter()
1091-
.find(|r| r["name"] == TEST_GROUP_NAME)
1092-
.expect("Should find the creator's repo by name");
1144+
let repo_data = repos
1145+
.iter()
1146+
.find(|r| {
1147+
r["all_files"]
1148+
.as_array()
1149+
.map(|files| files.iter().any(|f| f.as_str() == Some(file_name)))
1150+
.unwrap_or(false)
1151+
})
1152+
.expect("Should find a repo containing the uploaded file");
1153+
let refreshed_repo_id = repo_data["repo_id"]
1154+
.as_str()
1155+
.expect("repo_id should be a string")
1156+
.to_string();
10931157

10941158
let refreshed_files = repo_data["refreshed_files"].as_array()
10951159
.expect("refreshed_files should be an array");
10961160
assert!(
1097-
refreshed_files.len() <= 1,
1098-
"First refresh should refresh at most one file, got {}",
1099-
refreshed_files.len()
1161+
refreshed_files.is_empty()
1162+
|| (refreshed_files.len() == 1
1163+
&& refreshed_files[0].as_str() == Some(file_name)),
1164+
"First refresh should report either no-op or one refreshed expected file, got {refreshed_files:?}"
11001165
);
1101-
if refreshed_files.len() == 1 {
1102-
assert_eq!(
1103-
refreshed_files[0].as_str().unwrap(),
1104-
file_name,
1105-
"Should have refreshed the correct file"
1106-
);
1107-
}
11081166

11091167
let all_files = repo_data["all_files"].as_array().expect("all_files should be an array");
11101168
assert_eq!(all_files.len(), 1, "Should have one file in all_files");
@@ -1115,7 +1173,7 @@ mod tests {
11151173
let get_file_req = test::TestRequest::get()
11161174
.uri(&format!(
11171175
"/api/groups/{}/repos/{}/media/{}",
1118-
group.id(), repo.id(), file_name
1176+
group.id(), refreshed_repo_id, file_name
11191177
))
11201178
.to_request();
11211179
let get_file_resp = test::call_service(&app, get_file_req).await;
@@ -1137,9 +1195,15 @@ mod tests {
11371195
let repos2 = refresh_data2["repos"].as_array().expect("repos should be an array");
11381196
assert!(!repos2.is_empty(), "Should still have repos");
11391197

1140-
let repo_data2 = repos2.iter()
1141-
.find(|r| r["name"] == TEST_GROUP_NAME)
1142-
.expect("Should still find the creator's repo by name on second refresh");
1198+
let repo_data2 = repos2
1199+
.iter()
1200+
.find(|r| {
1201+
r["all_files"]
1202+
.as_array()
1203+
.map(|files| files.iter().any(|f| f.as_str() == Some(file_name)))
1204+
.unwrap_or(false)
1205+
})
1206+
.expect("Should still find the repo containing the uploaded file on second refresh");
11431207
let refreshed_files2 = repo_data2["refreshed_files"].as_array()
11441208
.expect("refreshed_files should be an array");
11451209
assert!(refreshed_files2.is_empty(),

0 commit comments

Comments
 (0)