mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 18:39:55 +02:00
fix(transfer): finalize delivery completion
This commit is contained in:
@@ -20,7 +20,7 @@ use crate::{
|
||||
util::now_ms,
|
||||
};
|
||||
|
||||
const SCHEMA_VERSION: i64 = 5;
|
||||
const SCHEMA_VERSION: i64 = 6;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Repository {
|
||||
@@ -78,6 +78,23 @@ pub(crate) struct ReceiverRequestInsert<'a> {
|
||||
pub(crate) app_version: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct PendingDeliveryReceipt {
|
||||
pub(crate) local_transfer_id: u64,
|
||||
pub(crate) sender_blob_ticket: String,
|
||||
pub(crate) request_id: String,
|
||||
pub(crate) sender_transfer_id: u64,
|
||||
pub(crate) token: String,
|
||||
}
|
||||
|
||||
pub(crate) struct PendingDeliveryReceiptInsert<'a> {
|
||||
pub(crate) local_transfer_id: u64,
|
||||
pub(crate) sender_blob_ticket: &'a str,
|
||||
pub(crate) request_id: &'a str,
|
||||
pub(crate) sender_transfer_id: u64,
|
||||
pub(crate) token: &'a str,
|
||||
}
|
||||
|
||||
impl Repository {
|
||||
pub(crate) async fn open(app_data_dir: &Path) -> Result<Self> {
|
||||
let db_path = app_data_dir.join("vnidrop.sqlite3");
|
||||
@@ -268,6 +285,21 @@ impl Repository {
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS pending_delivery_receipts (
|
||||
request_id TEXT PRIMARY KEY,
|
||||
local_transfer_id INTEGER NOT NULL,
|
||||
sender_blob_ticket TEXT NOT NULL,
|
||||
sender_transfer_id INTEGER NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
"#,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query(&format!("PRAGMA user_version = {SCHEMA_VERSION}"))
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
@@ -506,6 +538,82 @@ impl Repository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn complete_receive_with_pending_receipt(
|
||||
&self,
|
||||
receipt: PendingDeliveryReceiptInsert<'_>,
|
||||
) -> Result<()> {
|
||||
self.maybe_fail_write()?;
|
||||
let mut transaction = self.pool.begin().await?;
|
||||
let updated = sqlx::query(
|
||||
r#"
|
||||
UPDATE transfers
|
||||
SET status = 'done', updated_at = ?1
|
||||
WHERE transfer_id = ?2 AND direction = 'receive' AND status = 'receiving'
|
||||
"#,
|
||||
)
|
||||
.bind(now_ms())
|
||||
.bind(to_db_id(receipt.local_transfer_id)?)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
require_one_changed(updated.rows_affected(), "complete receive")?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO pending_delivery_receipts (
|
||||
request_id, local_transfer_id, sender_blob_ticket,
|
||||
sender_transfer_id, token, created_at
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(request_id) DO UPDATE SET
|
||||
local_transfer_id = excluded.local_transfer_id,
|
||||
sender_blob_ticket = excluded.sender_blob_ticket,
|
||||
sender_transfer_id = excluded.sender_transfer_id,
|
||||
token = excluded.token
|
||||
"#,
|
||||
)
|
||||
.bind(receipt.request_id)
|
||||
.bind(to_db_id(receipt.local_transfer_id)?)
|
||||
.bind(receipt.sender_blob_ticket)
|
||||
.bind(to_db_id(receipt.sender_transfer_id)?)
|
||||
.bind(receipt.token)
|
||||
.bind(now_ms())
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn list_pending_delivery_receipts(
|
||||
&self,
|
||||
) -> Result<Vec<PendingDeliveryReceipt>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT local_transfer_id, sender_blob_ticket, request_id,
|
||||
sender_transfer_id, token
|
||||
FROM pending_delivery_receipts
|
||||
ORDER BY created_at ASC
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|row| PendingDeliveryReceipt {
|
||||
local_transfer_id: row.get::<i64, _>("local_transfer_id") as u64,
|
||||
sender_blob_ticket: row.get("sender_blob_ticket"),
|
||||
request_id: row.get("request_id"),
|
||||
sender_transfer_id: row.get::<i64, _>("sender_transfer_id") as u64,
|
||||
token: row.get("token"),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_pending_delivery_receipt(&self, request_id: &str) -> Result<()> {
|
||||
sqlx::query("DELETE FROM pending_delivery_receipts WHERE request_id = ?1")
|
||||
.bind(request_id)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_active_share_access_mode(
|
||||
&self,
|
||||
transfer_id: u64,
|
||||
|
||||
135
crates/vnidrop/src/runtime/delivery.rs
Normal file
135
crates/vnidrop/src/runtime/delivery.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use std::{str::FromStr, sync::Arc, time::Duration};
|
||||
|
||||
use iroh_blobs::ticket::BlobTicket;
|
||||
use serde_json::json;
|
||||
|
||||
use super::CoreInner;
|
||||
use crate::{
|
||||
handshake::{DeliveryReceipt, DeliveryReceiptResponse, HandshakeService},
|
||||
repository::PendingDeliveryReceipt,
|
||||
};
|
||||
|
||||
const DELIVERY_RECEIPT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const DELIVERY_RECEIPT_RETRY_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const DELIVERY_RECEIPT_MAX_RETRY_INTERVAL: Duration = Duration::from_secs(5 * 60);
|
||||
|
||||
impl CoreInner {
|
||||
pub(super) async fn spawn_delivery_receipt_task(self: &Arc<Self>) {
|
||||
let core = Arc::downgrade(self);
|
||||
let task = tokio::spawn(async move {
|
||||
let mut retry_interval = DELIVERY_RECEIPT_RETRY_INTERVAL;
|
||||
loop {
|
||||
let Some(core) = core.upgrade() else {
|
||||
break;
|
||||
};
|
||||
let has_pending = core.deliver_pending_receipts().await;
|
||||
if has_pending {
|
||||
let notified = tokio::select! {
|
||||
() = core.delivery_receipt_notify.notified() => true,
|
||||
() = tokio::time::sleep(retry_interval) => false,
|
||||
};
|
||||
if notified {
|
||||
retry_interval = DELIVERY_RECEIPT_RETRY_INTERVAL;
|
||||
} else {
|
||||
retry_interval = retry_interval
|
||||
.saturating_mul(2)
|
||||
.min(DELIVERY_RECEIPT_MAX_RETRY_INTERVAL);
|
||||
}
|
||||
} else {
|
||||
core.delivery_receipt_notify.notified().await;
|
||||
retry_interval = DELIVERY_RECEIPT_RETRY_INTERVAL;
|
||||
}
|
||||
}
|
||||
});
|
||||
*self.delivery_receipt_task.lock().await = Some(task);
|
||||
}
|
||||
|
||||
async fn deliver_pending_receipts(&self) -> bool {
|
||||
let receipts = match self.repository.list_pending_delivery_receipts().await {
|
||||
Ok(receipts) => receipts,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "failed to load pending delivery receipts");
|
||||
return true;
|
||||
}
|
||||
};
|
||||
let has_pending = !receipts.is_empty();
|
||||
for receipt in receipts {
|
||||
self.deliver_pending_receipt(receipt).await;
|
||||
}
|
||||
has_pending
|
||||
}
|
||||
|
||||
async fn deliver_pending_receipt(&self, pending: PendingDeliveryReceipt) {
|
||||
let blob_ticket = match BlobTicket::from_str(&pending.sender_blob_ticket) {
|
||||
Ok(ticket) => ticket,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, request_id = %pending.request_id, "discarded invalid pending delivery receipt");
|
||||
let _ = self
|
||||
.repository
|
||||
.delete_pending_delivery_receipt(&pending.request_id)
|
||||
.await;
|
||||
self.emit_transfer(
|
||||
pending.local_transfer_id,
|
||||
"receive",
|
||||
"delivery",
|
||||
"receipt-rejected",
|
||||
json!({ "reason": "invalid-sender-ticket" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let client = HandshakeService::client(self.endpoint.clone(), blob_ticket.addr().clone());
|
||||
let receipt = DeliveryReceipt {
|
||||
request_id: pending.request_id.clone(),
|
||||
transfer_id: pending.sender_transfer_id,
|
||||
token: pending.token,
|
||||
};
|
||||
match tokio::time::timeout(DELIVERY_RECEIPT_TIMEOUT, client.report_delivery(receipt)).await
|
||||
{
|
||||
Ok(Ok(DeliveryReceiptResponse::Recorded)) => {
|
||||
if let Err(error) = self
|
||||
.repository
|
||||
.delete_pending_delivery_receipt(&pending.request_id)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(%error, request_id = %pending.request_id, "failed to clear recorded delivery receipt");
|
||||
return;
|
||||
}
|
||||
self.emit_transfer(
|
||||
pending.local_transfer_id,
|
||||
"receive",
|
||||
"delivery",
|
||||
"receipt-recorded",
|
||||
json!({ "sender_transfer_id": pending.sender_transfer_id }),
|
||||
);
|
||||
}
|
||||
Ok(Ok(DeliveryReceiptResponse::Rejected { reason })) => {
|
||||
let _ = self
|
||||
.repository
|
||||
.delete_pending_delivery_receipt(&pending.request_id)
|
||||
.await;
|
||||
self.emit_transfer(
|
||||
pending.local_transfer_id,
|
||||
"receive",
|
||||
"delivery",
|
||||
"receipt-rejected",
|
||||
json!({ "reason": reason }),
|
||||
);
|
||||
}
|
||||
Ok(Err(error)) => self.emit_transfer(
|
||||
pending.local_transfer_id,
|
||||
"receive",
|
||||
"delivery",
|
||||
"receipt-failed",
|
||||
json!({ "reason": error.to_string() }),
|
||||
),
|
||||
Err(_) => self.emit_transfer(
|
||||
pending.local_transfer_id,
|
||||
"receive",
|
||||
"delivery",
|
||||
"receipt-failed",
|
||||
json!({ "reason": "delivery receipt timed out" }),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,6 +187,10 @@ impl CoreInner {
|
||||
// Flush before stopping the router so the app can show the shutdown
|
||||
// event even if the process exits soon after Compose disposes the core.
|
||||
self.event_hub.flush().await;
|
||||
if let Some(task) = self.delivery_receipt_task.lock().await.take() {
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
}
|
||||
if let Err(error) = self.router.shutdown().await {
|
||||
self.emit_endpoint(
|
||||
"shutdown",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! - [`lifecycle`] — cancel/delete/shutdown/status/access
|
||||
//! - [`provider`] — blob provider events and per-connection send progress
|
||||
|
||||
mod delivery;
|
||||
mod facade;
|
||||
mod lifecycle;
|
||||
mod provider;
|
||||
@@ -38,7 +39,7 @@ use iroh_blobs::{
|
||||
};
|
||||
use serde_json::json;
|
||||
use tokio::{
|
||||
sync::{oneshot, Mutex as TokioMutex, Semaphore},
|
||||
sync::{oneshot, Mutex as TokioMutex, Notify, Semaphore},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
@@ -77,6 +78,8 @@ pub(super) struct CoreInner {
|
||||
pub(super) hash_to_transfer: TokioMutex<HashMap<String, HashSet<u64>>>,
|
||||
pub(super) connection_endpoints: TokioMutex<HashMap<u64, String>>,
|
||||
pub(super) provider_task: TokioMutex<Option<JoinHandle<()>>>,
|
||||
pub(super) delivery_receipt_notify: Notify,
|
||||
pub(super) delivery_receipt_task: TokioMutex<Option<JoinHandle<()>>>,
|
||||
pub(super) shutdown_started: AtomicBool,
|
||||
}
|
||||
|
||||
@@ -244,6 +247,8 @@ impl CoreInner {
|
||||
hash_to_transfer: TokioMutex::new(restored_hashes),
|
||||
connection_endpoints: TokioMutex::new(HashMap::new()),
|
||||
provider_task: TokioMutex::new(None),
|
||||
delivery_receipt_notify: Notify::new(),
|
||||
delivery_receipt_task: TokioMutex::new(None),
|
||||
shutdown_started: AtomicBool::new(false),
|
||||
});
|
||||
|
||||
@@ -257,6 +262,7 @@ impl CoreInner {
|
||||
}),
|
||||
);
|
||||
inner.spawn_provider_event_task(event_rx).await;
|
||||
inner.spawn_delivery_receipt_task().await;
|
||||
Ok(inner)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ use crate::{
|
||||
validated_relative_string, wait_for_writer, write_stream_to_blocking_writer,
|
||||
AtomicOutputFile,
|
||||
},
|
||||
handshake::{DeliveryReceipt, DeliveryReceiptResponse, HandshakeResponse, HandshakeService},
|
||||
repository::{ReceivedArtifactInsert, TransferUpsert},
|
||||
handshake::{DeliveryReceipt, HandshakeResponse, HandshakeService},
|
||||
repository::{PendingDeliveryReceiptInsert, ReceivedArtifactInsert, TransferUpsert},
|
||||
ticket::{parse_transfer_ticket_with_limits, ParsedTransferTicket},
|
||||
transfer_state::{TransferDirection, TransferStatus},
|
||||
};
|
||||
@@ -271,6 +271,7 @@ impl CoreInner {
|
||||
.map_err(VnidropError::filesystem)?;
|
||||
}
|
||||
let sender_addr = parsed.blob_ticket.addr().clone();
|
||||
let sender_blob_ticket = parsed.blob_ticket.to_string();
|
||||
|
||||
self.emit_transfer(transfer_id, "receive", "network", "connecting", json!({}));
|
||||
// Every VniDrop ticket carries metadata and must complete the handshake.
|
||||
@@ -352,40 +353,18 @@ impl CoreInner {
|
||||
self.export_collection(transfer_id, total_files, target, collection)
|
||||
.await?;
|
||||
self.repository
|
||||
.transition_transfer_status(
|
||||
transfer_id,
|
||||
TransferStatus::Receiving,
|
||||
TransferStatus::Done,
|
||||
)
|
||||
.complete_receive_with_pending_receipt(PendingDeliveryReceiptInsert {
|
||||
local_transfer_id: transfer_id,
|
||||
sender_blob_ticket: &sender_blob_ticket,
|
||||
request_id: &delivery_receipt.request_id,
|
||||
sender_transfer_id: delivery_receipt.transfer_id,
|
||||
token: &delivery_receipt.token,
|
||||
})
|
||||
.await
|
||||
.map_err(VnidropError::repository)?;
|
||||
drop(download_tag);
|
||||
self.emit_transfer(transfer_id, "receive", "lifecycle", "done", json!({}));
|
||||
let sender_transfer_id = delivery_receipt.transfer_id;
|
||||
let client = HandshakeService::client(self.endpoint.clone(), sender_addr);
|
||||
match client.report_delivery(delivery_receipt).await {
|
||||
Ok(DeliveryReceiptResponse::Recorded) => self.emit_transfer(
|
||||
transfer_id,
|
||||
"receive",
|
||||
"delivery",
|
||||
"receipt-recorded",
|
||||
json!({ "sender_transfer_id": sender_transfer_id }),
|
||||
),
|
||||
Ok(DeliveryReceiptResponse::Rejected { reason }) => self.emit_transfer(
|
||||
transfer_id,
|
||||
"receive",
|
||||
"delivery",
|
||||
"receipt-rejected",
|
||||
json!({ "reason": reason }),
|
||||
),
|
||||
Err(error) => self.emit_transfer(
|
||||
transfer_id,
|
||||
"receive",
|
||||
"delivery",
|
||||
"receipt-failed",
|
||||
json!({ "reason": error.to_string() }),
|
||||
),
|
||||
}
|
||||
self.delivery_receipt_notify.notify_one();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::{
|
||||
api::{CoreEvent, ReceivedLocatorKind},
|
||||
repository::{ReceivedArtifactInsert, ReceiverRequestInsert, Repository, TransferUpsert},
|
||||
repository::{
|
||||
PendingDeliveryReceiptInsert, ReceivedArtifactInsert, ReceiverRequestInsert, Repository,
|
||||
TransferUpsert,
|
||||
},
|
||||
transfer_state::{ReceiverRequestStatus, TransferDirection, TransferStatus},
|
||||
};
|
||||
|
||||
@@ -63,7 +66,7 @@ async fn received_artifacts_survive_history_deletion() {
|
||||
async fn persists_transfers_and_events_across_reopen() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let repository = Repository::open(temp.path()).await.unwrap();
|
||||
assert_eq!(repository.schema_version().await.unwrap(), 5);
|
||||
assert_eq!(repository.schema_version().await.unwrap(), 6);
|
||||
repository
|
||||
.insert_transfer(transfer(
|
||||
7,
|
||||
@@ -112,6 +115,57 @@ async fn persists_transfers_and_events_across_reopen() {
|
||||
assert_eq!(events[0].id, "event-1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn receive_completion_persists_delivery_receipt_until_recorded() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let repository = Repository::open(temp.path()).await.unwrap();
|
||||
repository
|
||||
.start_receive(transfer(
|
||||
93,
|
||||
TransferDirection::Receive,
|
||||
TransferStatus::Receiving,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
repository
|
||||
.complete_receive_with_pending_receipt(PendingDeliveryReceiptInsert {
|
||||
local_transfer_id: 93,
|
||||
sender_blob_ticket: "blob-ticket",
|
||||
request_id: "request-93",
|
||||
sender_transfer_id: 39,
|
||||
token: "receipt-token",
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let transfer = repository
|
||||
.list_transfers()
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|transfer| transfer.transfer_id == 93)
|
||||
.unwrap();
|
||||
assert_eq!(transfer.status, "done");
|
||||
drop(repository);
|
||||
|
||||
let reopened = Repository::open(temp.path()).await.unwrap();
|
||||
let pending = reopened.list_pending_delivery_receipts().await.unwrap();
|
||||
assert_eq!(pending.len(), 1);
|
||||
assert_eq!(pending[0].local_transfer_id, 93);
|
||||
assert_eq!(pending[0].sender_transfer_id, 39);
|
||||
assert_eq!(pending[0].request_id, "request-93");
|
||||
assert_eq!(pending[0].token, "receipt-token");
|
||||
reopened
|
||||
.delete_pending_delivery_receipt("request-93")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(reopened
|
||||
.list_pending_delivery_receipts()
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn receiver_request_can_only_be_resolved_once() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
@@ -527,7 +581,7 @@ async fn migrates_schema_v2_identity_without_losing_transfer() {
|
||||
pool.close().await;
|
||||
|
||||
let repository = Repository::open(temp.path()).await.unwrap();
|
||||
assert_eq!(repository.schema_version().await.unwrap(), 5);
|
||||
assert_eq!(repository.schema_version().await.unwrap(), 6);
|
||||
let stored = repository.list_transfers().await.unwrap().remove(0);
|
||||
assert_eq!(stored.transfer_id, 7);
|
||||
assert_eq!(stored.local_id, "legacy-7-send");
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use iroh_blobs::Hash;
|
||||
|
||||
use crate::{
|
||||
repository::{Repository, TransferUpsert},
|
||||
repository::{PendingDeliveryReceiptInsert, Repository, TransferUpsert},
|
||||
transfer_state::{TransferDirection, TransferStatus},
|
||||
CoreEvent, CoreEventSink, VnidropCore, VnidropError,
|
||||
};
|
||||
@@ -98,6 +98,63 @@ fn startup_recovers_interrupted_transfer_and_persists_event() {
|
||||
core.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_processes_persisted_delivery_receipts() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let preparation_runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
preparation_runtime.block_on(async {
|
||||
let repository = Repository::open(temp.path()).await.unwrap();
|
||||
repository
|
||||
.start_receive(TransferUpsert {
|
||||
transfer_id: 94,
|
||||
peer_id: None,
|
||||
direction: TransferDirection::Receive,
|
||||
status: TransferStatus::Receiving,
|
||||
transfer_name: Some("completed receive"),
|
||||
content_hash: Some("hash"),
|
||||
ticket: None,
|
||||
file_count: 1,
|
||||
total_size: 5,
|
||||
access_mode: "approval_required",
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
repository
|
||||
.complete_receive_with_pending_receipt(PendingDeliveryReceiptInsert {
|
||||
local_transfer_id: 94,
|
||||
sender_blob_ticket: "invalid-ticket",
|
||||
request_id: "request-94",
|
||||
sender_transfer_id: 49,
|
||||
token: "receipt-token",
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
drop(preparation_runtime);
|
||||
|
||||
let core = VnidropCore::initialize(
|
||||
temp.path().to_string_lossy().to_string(),
|
||||
Arc::new(TestSink),
|
||||
)
|
||||
.unwrap();
|
||||
let started = std::time::Instant::now();
|
||||
loop {
|
||||
if core.list_events(Some(94)).unwrap().iter().any(|event| {
|
||||
event.phase == "delivery"
|
||||
&& event.kind == "receipt-rejected"
|
||||
&& event.data_json.contains("invalid-sender-ticket")
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
started.elapsed() < std::time::Duration::from_secs(2),
|
||||
"startup did not process the persisted delivery receipt"
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
core.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_fails_persisted_share_when_root_blob_is_missing() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
mod support;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use support::{
|
||||
receive_with_response, share_path, wait_for_receiver_request, CoreGuard, RecordingSink,
|
||||
@@ -8,6 +11,24 @@ use support::{
|
||||
};
|
||||
use vnidrop::{CoreLimits, ShareMetadataInput, ShareSource, SourceKind, TransferAccessMode};
|
||||
|
||||
fn wait_for_completed_delivery(
|
||||
sender: &vnidrop::VnidropCore,
|
||||
transfer_id: u64,
|
||||
) -> Vec<vnidrop::ReceiverRequest> {
|
||||
let started = Instant::now();
|
||||
loop {
|
||||
let requests = sender.list_receiver_requests(transfer_id).unwrap();
|
||||
if requests.iter().any(|request| request.status == "completed") {
|
||||
return requests;
|
||||
}
|
||||
assert!(
|
||||
started.elapsed() < Duration::from_secs(5),
|
||||
"delivery receipt was not recorded"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_share_receives_without_sender_approval() {
|
||||
let source_dir = tempfile::tempdir().unwrap();
|
||||
@@ -47,10 +68,7 @@ fn public_share_receives_without_sender_approval() {
|
||||
std::fs::read(output_dir.path().join("public.txt")).unwrap(),
|
||||
b"public content"
|
||||
);
|
||||
let deliveries = sender
|
||||
.core
|
||||
.list_receiver_requests(share.transfer_id)
|
||||
.unwrap();
|
||||
let deliveries = wait_for_completed_delivery(&sender.core, share.transfer_id);
|
||||
assert_eq!(deliveries.len(), 1);
|
||||
assert_eq!(deliveries[0].receiver_name.as_deref(), Some("Receiver"));
|
||||
assert_eq!(deliveries[0].status, "completed");
|
||||
@@ -104,10 +122,7 @@ fn approval_required_denies_then_allows_receiver() {
|
||||
std::fs::read(allowed_output.path().join("private.txt")).unwrap(),
|
||||
b"approved content"
|
||||
);
|
||||
let completed = sender
|
||||
.core
|
||||
.list_receiver_requests(share.transfer_id)
|
||||
.unwrap();
|
||||
let completed = wait_for_completed_delivery(&sender.core, share.transfer_id);
|
||||
assert!(completed
|
||||
.iter()
|
||||
.any(|request| request.status == "completed"));
|
||||
|
||||
Reference in New Issue
Block a user