nextcloud-versions 0.1.3

WebDAV file versions client for the Nextcloud Rust SDK
Documentation
use std::future::Future;
use std::sync::Mutex;
use std::task::{Context, Poll, Waker};

use nextcloud_sdk_core::{
    AuthProvider, Error, Headers, HttpMethod, Request, Response, StatusCode, Transport,
};

use super::VersionsClient;
use nextcloud_dav::DESTINATION_HEADER;

struct FakeTransport {
    status: StatusCode,
    body: Vec<u8>,
    seen: Mutex<Vec<Request>>,
}

impl FakeTransport {
    fn new(status: u16, body: impl Into<Vec<u8>>) -> Self {
        Self {
            status: StatusCode::new(status),
            body: body.into(),
            seen: Mutex::new(Vec::new()),
        }
    }

    fn last_request(&self) -> Request {
        let seen = self.seen.lock().expect("test mutex not poisoned");
        seen.last().cloned().expect("a request was made")
    }
}

impl Transport for FakeTransport {
    async fn execute(&self, request: Request) -> Result<Response, Error> {
        self.seen
            .lock()
            .expect("test mutex not poisoned")
            .push(request);
        Ok(Response::new(
            self.status,
            Headers::new(),
            self.body.clone(),
        ))
    }
}

struct FakeAuth;

impl AuthProvider for FakeAuth {
    fn authenticate(&self, headers: &mut Headers) {
        headers.set("Authorization", "Bearer token-abc");
    }
}

fn block_on<F: Future>(future: F) -> F::Output {
    let mut future = std::pin::pin!(future);
    let waker = Waker::noop();
    let mut context = Context::from_waker(waker);
    loop {
        if let Poll::Ready(value) = future.as_mut().poll(&mut context) {
            return value;
        }
    }
}

const SAMPLE_MULTISTATUS: &str = r#"<?xml version="1.0" encoding="utf-8"?>
<d:multistatus xmlns:d="DAV:">
  <d:response>
    <d:href>/remote.php/dav/versions/alice/versions/42/</d:href>
    <d:propstat>
      <d:prop>
        <d:resourcetype><d:collection/></d:resourcetype>
      </d:prop>
      <d:status>HTTP/1.1 200 OK</d:status>
    </d:propstat>
  </d:response>
  <d:response>
    <d:href>/remote.php/dav/versions/alice/versions/42/1609459200</d:href>
    <d:propstat>
      <d:prop>
        <d:getlastmodified>Fri, 01 Jan 2021 00:00:00 GMT</d:getlastmodified>
        <d:getcontentlength>512</d:getcontentlength>
        <d:getetag>"rev-etag-1"</d:getetag>
      </d:prop>
      <d:status>HTTP/1.1 200 OK</d:status>
    </d:propstat>
  </d:response>
</d:multistatus>"#;

#[test]
fn list_success() {
    let transport = FakeTransport::new(207, SAMPLE_MULTISTATUS);
    let auth = FakeAuth;
    let client = VersionsClient::new(&transport, &auth);

    let versions = block_on(client.list("alice", 42)).expect("list succeeds");
    assert_eq!(versions.len(), 1);
    assert_eq!(versions[0].revision_id, "1609459200");
    assert_eq!(versions[0].content_length, Some(512));
    assert_eq!(versions[0].etag.as_deref(), Some("\"rev-etag-1\""));
    assert_eq!(
        versions[0].last_modified.as_deref(),
        Some("Fri, 01 Jan 2021 00:00:00 GMT")
    );

    let req = transport.last_request();
    assert_eq!(req.method(), HttpMethod::Propfind);
    assert_eq!(
        req.path().as_str(),
        "/remote.php/dav/versions/alice/versions/42"
    );
    assert_eq!(req.headers().get("Depth"), Some("1"));
    assert_eq!(req.headers().get("Authorization"), Some("Bearer token-abc"));
    assert!(req.headers().get("Content-Type").unwrap().contains("xml"));
}

#[test]
fn list_status_error() {
    let transport = FakeTransport::new(404, "Not Found");
    let auth = FakeAuth;
    let client = VersionsClient::new(&transport, &auth);

    let err = block_on(client.list("alice", 42)).unwrap_err();
    match err {
        Error::Status(status_err) => {
            assert_eq!(status_err.status().as_u16(), 404);
            assert_eq!(status_err.method(), "PROPFIND");
        }
        other => panic!("expected status error, got {other:?}"),
    }
}

#[test]
fn list_invalid_user() {
    let transport = FakeTransport::new(207, SAMPLE_MULTISTATUS);
    let auth = FakeAuth;
    let client = VersionsClient::new(&transport, &auth);

    let err = block_on(client.list("alice/danger", 42)).unwrap_err();
    assert!(matches!(err, Error::Url(_)));
}

#[test]
fn get_success() {
    let file_bytes = b"revision content bytes";
    let transport = FakeTransport::new(200, file_bytes.to_vec());
    let auth = FakeAuth;
    let client = VersionsClient::new(&transport, &auth);

    let bytes = block_on(client.get("alice", 42, "1609459200")).expect("get succeeds");
    assert_eq!(bytes, file_bytes);

    let req = transport.last_request();
    assert_eq!(req.method(), HttpMethod::Get);
    assert_eq!(
        req.path().as_str(),
        "/remote.php/dav/versions/alice/versions/42/1609459200"
    );
    assert_eq!(req.headers().get("Authorization"), Some("Bearer token-abc"));
}

#[test]
fn get_status_error() {
    let transport = FakeTransport::new(404, "Not Found");
    let auth = FakeAuth;
    let client = VersionsClient::new(&transport, &auth);

    let err = block_on(client.get("alice", 42, "1609459200")).unwrap_err();
    match err {
        Error::Status(status_err) => {
            assert_eq!(status_err.status().as_u16(), 404);
            assert_eq!(status_err.method(), "GET");
        }
        other => panic!("expected status error, got {other:?}"),
    }
}

#[test]
fn get_invalid_args() {
    let transport = FakeTransport::new(200, b"".to_vec());
    let auth = FakeAuth;
    let client = VersionsClient::new(&transport, &auth);

    assert!(matches!(
        block_on(client.get("../alice", 42, "1609459200")),
        Err(Error::Url(_))
    ));
    assert!(matches!(
        block_on(client.get("alice", 42, "1609459200/extra")),
        Err(Error::Url(_))
    ));
}

#[test]
fn restore_success() {
    let transport = FakeTransport::new(204, "");
    let auth = FakeAuth;
    let client = VersionsClient::new(&transport, &auth);

    block_on(client.restore("alice", 42, "1609459200")).expect("restore succeeds");

    let req = transport.last_request();
    assert_eq!(req.method(), HttpMethod::Move);
    assert_eq!(
        req.path().as_str(),
        "/remote.php/dav/versions/alice/versions/42/1609459200"
    );
    assert_eq!(
        req.headers().get(DESTINATION_HEADER),
        Some("/remote.php/dav/versions/alice/restore/42")
    );
    assert_eq!(req.headers().get("Authorization"), Some("Bearer token-abc"));
}

#[test]
fn restore_status_error() {
    let transport = FakeTransport::new(409, "Conflict");
    let auth = FakeAuth;
    let client = VersionsClient::new(&transport, &auth);

    let err = block_on(client.restore("alice", 42, "1609459200")).unwrap_err();
    match err {
        Error::Status(status_err) => {
            assert_eq!(status_err.status().as_u16(), 409);
            assert_eq!(status_err.method(), "MOVE");
        }
        other => panic!("expected status error, got {other:?}"),
    }
}

#[test]
fn restore_invalid_args() {
    let transport = FakeTransport::new(204, "");
    let auth = FakeAuth;
    let client = VersionsClient::new(&transport, &auth);

    assert!(matches!(
        block_on(client.restore("alice?", 42, "1609459200")),
        Err(Error::Url(_))
    ));
    assert!(matches!(
        block_on(client.restore("alice", 42, "..")),
        Err(Error::Url(_))
    ));
}

#[test]
fn client_accessors() {
    let transport = FakeTransport::new(200, "");
    let auth = FakeAuth;
    let client = VersionsClient::new(&transport, &auth);

    assert_eq!(client.transport().status.as_u16(), 200);
    let mut headers = Headers::new();
    client.auth().authenticate(&mut headers);
    assert_eq!(headers.get("Authorization"), Some("Bearer token-abc"));
}