btdt-server 0.4.3

Server component for "been there, done that" - a tool for flexible CI caching
Documentation
use biscuit_auth::macros::biscuit;
use biscuit_auth::{KeyPair, UnverifiedBiscuit};
use btdt::cache::remote::RemoteCache;
use btdt::cache::remote::http::HttpClient;
use btdt::pipeline::Pipeline;
use btdt_server_lib::test_server::BtdtTestServer;
use serial_test::serial;
use std::collections::BTreeMap;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf;
use std::thread::sleep;
use std::time::Duration;
use tempfile::tempdir;

struct BtdtTestServerWithAuthorizedClient {
    _key_dir: tempfile::TempDir,
    _server: BtdtTestServer,
    client: Pipeline<RemoteCache>,
}

impl Default for BtdtTestServerWithAuthorizedClient {
    fn default() -> Self {
        Self::new(BTreeMap::new())
    }
}

impl BtdtTestServerWithAuthorizedClient {
    fn new(mut env: BTreeMap<String, String>) -> Self {
        let key_dir = tempdir().unwrap();
        let key_path = key_dir.path().join("private_key.pem");
        let key_pair = KeyPair::new();
        let mut keyfile = OpenOptions::new()
            .mode(0o600)
            .create_new(true)
            .write(true)
            .open(&key_path)
            .unwrap();
        keyfile
            .write_all(key_pair.to_private_key_pem().unwrap().as_bytes())
            .unwrap();
        let token =
            UnverifiedBiscuit::from(&biscuit!("").build(&key_pair).unwrap().to_vec().unwrap())
                .unwrap();

        env.insert(
            "BTDT_AUTH_PRIVATE_KEY".into(),
            key_path.to_str().unwrap().to_string(),
        );
        let server = BtdtTestServer::new(&env).wait_until_ready().unwrap();

        let client = Pipeline::new(
            RemoteCache::new(
                server.base_url().join("api/caches/test-cache").unwrap(),
                HttpClient::default().unwrap(),
                token,
            )
            .unwrap(),
        );

        Self {
            client,
            _key_dir: key_dir,
            _server: server,
        }
    }
}

struct TestData {
    _tempdir: tempfile::TempDir,
    path: PathBuf,
}

impl Default for TestData {
    fn default() -> Self {
        let tempdir = tempdir().unwrap();
        let path = tempdir.path().join("source-root");
        fs::create_dir(&path).unwrap();
        fs::write(path.join("file.txt"), "Hello, world!").unwrap();
        Self {
            _tempdir: tempdir,
            path,
        }
    }
}

#[test]
#[serial]
fn test_health_endpoint() {
    let server = BtdtTestServer::default().wait_until_ready().unwrap();
    let response = server.get("/api/health").send().unwrap();
    assert!(
        response.status().is_success(),
        "unexpected status: {}",
        response.status()
    );
}

#[test]
#[serial]
fn test_health_check_fails_without_running_server() {
    let mut process = BtdtTestServer::run_health_check("http://example.invalid", None);
    assert!(!process.wait().unwrap().success());
}

#[test]
#[serial]
fn test_health_check_succeeds_with_running_server() {
    let server = BtdtTestServer::default().wait_until_ready().unwrap();
    let mut process = BtdtTestServer::run_health_check(server.base_url().as_str(), None);
    assert!(process.wait().unwrap().success());
}

#[test]
#[serial]
fn test_roundtrip() {
    let server_with_client = BtdtTestServerWithAuthorizedClient::default();
    let mut client = server_with_client.client;
    let test_data = TestData::default();

    client.store(&["key1", "key2"], &test_data.path).unwrap();

    let tempdir = tempdir().unwrap();
    let destination_path1 = tempdir.path().join("destination-root-1");
    client
        .restore(&["non-existent", "key1"], &destination_path1)
        .unwrap();

    let destination_path2 = tempdir.path().join("destination-root-2");
    client.restore(&["key2"], &destination_path2).unwrap();

    assert_eq!(
        fs::read_to_string(destination_path1.join("file.txt")).unwrap(),
        "Hello, world!"
    );
    assert_eq!(
        fs::read_to_string(destination_path2.join("file.txt")).unwrap(),
        "Hello, world!"
    );
}

#[test]
#[serial]
fn test_cleanup() {
    let server_with_client = BtdtTestServerWithAuthorizedClient::new(BTreeMap::from([
        ("BTDT_CLEANUP__MAX_CACHE_SIZE".to_string(), "0B".to_string()),
        (
            "BTDT_CLEANUP__CACHE_EXPIRATION".to_string(),
            "1s".to_string(),
        ),
        ("BTDT_CLEANUP__INTERVAL".to_string(), "1s".to_string()),
    ]));
    let mut client = server_with_client.client;
    let test_data = TestData::default();

    client.store(&["key"], &test_data.path).unwrap();
    sleep(Duration::from_secs(2));

    let tempdir = tempdir().unwrap();
    assert_eq!(client.restore(&["key"], &tempdir).unwrap(), None);
}