dove-core 0.1.1

The shared library behind dove — client-side-encrypted, expiring file sharing from a cloud you own.
Documentation
//! Regression test for the trust-banner ordering fix: `SelfHosted::get` must
//! report the sender's name/message via `Progress::field` *before* it starts
//! streaming the download (`Progress::bytes`) — the whole point of the trust
//! callout is deciding whether to pull the file at all, so it has to land
//! before any bytes move.
//!
//! `SelfHosted::get` has no injected HTTP client, so exercising the real
//! ordering means driving it against a real (if minimal) HTTP server. This
//! hand-rolls one over `std::net` rather than pulling in a mocking
//! dependency: two canned responses, served in the order `get` is known to
//! request them (`/meta/<id>` — the metadata pre-fetch — always precedes
//! `/dl/<id>` — the actual download — because `fetch_meta` is a blocking
//! call that completes before the download request is even built).

use dove_core::backend::SelfHosted;
use dove_core::progress::Progress;
use dove_core::transfer::{GetRequest, Transfer};
use std::cell::RefCell;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};

/// Records every `Progress` callback, in order, so the test can assert on
/// relative ordering (not just "was it called").
#[derive(Default)]
struct RecordingProgress {
    events: RefCell<Vec<String>>,
}

impl Progress for RecordingProgress {
    fn step(&self, label: &str) {
        self.events.borrow_mut().push(format!("step:{label}"));
    }
    fn done(&self, label: &str) {
        self.events.borrow_mut().push(format!("done:{label}"));
    }
    fn field(&self, key: &str, value: &str) {
        self.events
            .borrow_mut()
            .push(format!("field:{key}={value}"));
    }
    fn bytes(&self, _uploaded: u64, _total: u64) {
        self.events.borrow_mut().push("bytes".to_string());
    }
}

/// Write a bare-bones `HTTP/1.1 200 OK` response with the given body, then
/// close the connection (`Connection: close` — each canned response is
/// served on its own accepted connection; closing tells a well-behaved
/// client, like `ureq`, not to pool this socket for the next request, so the
/// two responses land on two separate `accept()`s in the order they're
/// written below).
fn respond(mut stream: TcpStream, body: &[u8]) {
    // Drain whatever the client sent (we don't need to parse it — the mock
    // only ever serves one canned reply per connection, in a known order)
    // before writing the response, so the socket doesn't reset mid-write.
    let mut buf = [0u8; 4096];
    let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(500)));
    let _ = stream.read(&mut buf);

    let header = format!(
        "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
        body.len()
    );
    let _ = stream.write_all(header.as_bytes());
    let _ = stream.write_all(body);
    let _ = stream.flush();
}

#[test]
fn from_and_message_are_reported_before_the_first_byte_downloads() {
    // A share made without a PIN: the content key *is* the fragment secret,
    // the same secret the metadata blob is encrypted with.
    let secret = dove_core::crypto::gen_key();
    let fragment = dove_core::crypto::key_to_fragment(&secret);

    let meta_json = serde_json::json!({
        "name": "report.pdf",
        "from": "Alex",
        "msg": "the codes",
    })
    .to_string();
    let meta_blob = dove_core::crypto::encrypt_meta(&secret, meta_json.as_bytes());
    let meta_body = serde_json::json!({ "meta": meta_blob }).to_string();

    // Not a valid dove container — `get` only needs to attempt at least one
    // read of it (which fires `Progress::bytes`) before failing integrity;
    // the ordering claim doesn't require the download to succeed.
    let dl_body = b"not-a-dove-container".to_vec();

    let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock server");
    let port = listener.local_addr().unwrap().port();
    let server = std::thread::spawn(move || {
        // Exactly two connections, served in order: /meta first (get()
        // fetches it before ever building the download request), then /dl.
        let (meta_conn, _) = listener.accept().expect("accept /meta connection");
        respond(meta_conn, meta_body.as_bytes());
        let (dl_conn, _) = listener.accept().expect("accept /dl connection");
        respond(dl_conn, &dl_body);
    });

    let url = format!("http://127.0.0.1:{port}/d/abc123/report.pdf#{fragment}");
    let out = std::env::temp_dir().join(format!("dove-core-get-ordering-{port}.bin"));
    let req = GetRequest {
        url,
        out: Some(out.clone()),
        pin: None,
    };
    let progress = RecordingProgress::default();

    let backend = SelfHosted::adhoc();
    let result = backend.get(req, &progress);
    server.join().expect("mock server thread panicked");
    let _ = std::fs::remove_file(&out);

    // The download's ciphertext is garbage, so this is expected to fail
    // integrity — that's fine, the test is about ordering, not success.
    // (`Fetched` has no `Debug` impl, so report the error side only.)
    assert!(
        result.is_err(),
        "expected a decrypt/integrity error, got Ok"
    );
    if let Err(e) = &result {
        assert!(
            matches!(e, dove_core::error::Error::Integrity),
            "expected Error::Integrity, got {e:?}"
        );
    }

    let events = progress.events.borrow();
    let from_idx = events
        .iter()
        .position(|e| e == "field:from=Alex")
        .unwrap_or_else(|| panic!("no 'from' field reported: {events:?}"));
    let message_idx = events
        .iter()
        .position(|e| e == "field:message=the codes")
        .unwrap_or_else(|| panic!("no 'message' field reported: {events:?}"));
    let first_bytes_idx = events
        .iter()
        .position(|e| e == "bytes")
        .unwrap_or_else(|| panic!("download never reported any bytes: {events:?}"));

    assert!(
        from_idx < first_bytes_idx,
        "the 'from' trust field must be reported before any download bytes: {events:?}"
    );
    assert!(
        message_idx < first_bytes_idx,
        "the 'message' trust field must be reported before any download bytes: {events:?}"
    );
}