fix(sync): 恢复过期会话并持久化上传尝试结果

This commit is contained in:
2026-09-08 20:10:26 +08:00
parent 4ff8fdbd70
commit 5a6fa86d10
10 changed files with 325 additions and 41 deletions
+85
View File
@@ -618,6 +618,14 @@ async fn actual_service_accepts_ordered_push_and_repeat_commit_without_duplicate
}
std::fs::remove_file(root.path().join("interrupt-upload")).unwrap();
let large_workspace = Arc::new(Mutex::new(Workspace::open(large_root.path()).unwrap()));
let interrupted = large_workspace
.lock()
.unwrap()
.sync_attempts(&large_binding.id)
.unwrap();
assert_eq!(interrupted.len(), 1);
assert_eq!(interrupted[0]["attempts"], 10);
assert_eq!(interrupted[0]["outcome"], "interrupted");
assert!(client
.push_one(&large_workspace, &large_binding)
.await
@@ -707,6 +715,59 @@ async fn actual_service_accepts_ordered_push_and_repeat_commit_without_duplicate
.unwrap()
.unlock(Zeroizing::new(b"fixture-stronghold-password".to_vec()))
.unwrap();
// Expire the access token early in this isolated fixture; the refresh token stays valid.
let database = rusqlite::Connection::open(root.path().join("sync.sqlite3")).unwrap();
database
.execute("UPDATE sessions SET expires=0", [])
.unwrap();
let attempts = std::sync::atomic::AtomicUsize::new(0);
let recovered = sync_auth::authenticated(&credentials, &canonical, "rust-fixture", |client| {
attempts.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
async move {
client
.json(reqwest::Method::GET, "sync/v1/vaults", None)
.await
}
})
.await
.unwrap();
assert_eq!(attempts.load(std::sync::atomic::Ordering::SeqCst), 2);
assert!(recovered["items"]
.as_array()
.unwrap()
.iter()
.any(|v| v["id"] == remote));
// Even a repeated 401 must stop after one rotation, rather than refresh indefinitely.
attempts.store(0, std::sync::atomic::Ordering::SeqCst);
let denied = sync_auth::authenticated(&credentials, &canonical, "rust-fixture", |_client| {
attempts.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
async {
Err::<(), _>(notesagent_host::sync_client::SyncError {
code: "UNAUTHORIZED".into(),
status: 401,
retry_after: None,
})
}
})
.await
.unwrap_err();
assert_eq!(denied.status, 401);
assert_eq!(attempts.load(std::sync::atomic::Ordering::SeqCst), 2);
attempts.store(0, std::sync::atomic::Ordering::SeqCst);
let denied = sync_auth::authenticated(&credentials, &canonical, "rust-fixture", |_client| {
attempts.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
async {
Err::<(), _>(notesagent_host::sync_client::SyncError {
code: "FORBIDDEN".into(),
status: 403,
retry_after: None,
})
}
})
.await
.unwrap_err();
assert_eq!(denied.status, 403);
assert_eq!(attempts.load(std::sync::atomic::Ordering::SeqCst), 1);
let restored = sync_auth::client(&credentials, &canonical, "rust-fixture", false)
.await
.unwrap();
@@ -723,6 +784,30 @@ async fn actual_service_accepts_ordered_push_and_repeat_commit_without_duplicate
.status,
401
);
sync_auth::login(
&credentials,
&endpoint,
"rust-fixture",
Zeroizing::new("controlled-fixture-password".into()),
"Revoked Host",
true,
)
.await
.unwrap();
database.execute("DELETE FROM sessions", []).unwrap();
attempts.store(0, std::sync::atomic::Ordering::SeqCst);
let revoked = sync_auth::authenticated(&credentials, &canonical, "rust-fixture", |client| {
attempts.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
async move {
client
.json(reqwest::Method::GET, "sync/v1/vaults", None)
.await
}
})
.await
.unwrap_err();
assert_eq!(revoked.status, 401);
assert_eq!(attempts.load(std::sync::atomic::Ordering::SeqCst), 1);
}
#[tokio::test]