cinema 0.1.0

HTTP record-replay proxy for Rust tests
Documentation
use crate::prelude::*;
use std::sync::{Arc, RwLock};

mod store;
pub use store::*;

pub struct RecordReplay {
    proxy: Proxy,
    store: Arc<RwLock<dyn RecordReplayStore + Send + Sync>>,
}

impl RecordReplay {
    pub async fn start<Store>(store: Store) -> Result<Self, CinemaError>
    where
        Store: RecordReplayStore + Send + Sync + 'static,
    {
        let store = Arc::new(RwLock::new(store));
        let proxy = {
            let store = store.clone();
            Proxy::start_with(Arc::new(move |payload| {
                let mut store = store
                    .write()
                    .map_err(|_| CinemaError::Lock("record replay store for writing"))?;
                store.handle(payload)
            }))
            .await?
        };

        Ok(RecordReplay { proxy, store })
    }

    pub fn add_origin<Str: Into<String> + Copy>(&mut self, origin: Str) -> String {
        let store = Arc::clone(&self.store);
        self.proxy.forward_with(
            origin,
            Arc::new(move |payload| {
                let record = RecordReplayRecord {
                    method: payload.request.method().clone(),
                    uri: payload.request.uri().clone(),
                    // [TODO] Record headers and body?
                    headers: None,
                    body: None,
                    response: RecordReplayRecordResponse {
                        status: payload.status,
                        headers: payload.headers.cloned(),
                        body: payload.body.clone(),
                    },
                };

                let mut store = store
                    .write()
                    .map_err(|_| CinemaError::Lock("record replay store for writing"))?;
                store.record(record)?;

                Ok(())
            }),
        )
    }

    pub async fn commit(&mut self) {
        let store = self.store.clone();
        let store = store
            .read()
            .map_err(|_| CinemaError::Lock("record replay store for reading"))
            .unwrap();
        store.commit().await.unwrap();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;
    use reqwest::Client;
    use tokio::fs;

    #[tokio::test]
    async fn test_replay() {
        let store = RecordReplayFileStore::from("records/replay.toml")
            .await
            .unwrap();
        let mut replay = RecordReplay::start(store).await.unwrap();
        let mocked_origin = replay.add_origin("https://example.com");

        let client = Client::new();
        let response = client
            .get(mocked_origin)
            .send()
            .await
            .expect("Failed to send request");
        let text = response.text().await.expect("Failed to read response");
        assert_eq!(text, "<!doctype html><title>Hello, cruel world!</title>");
    }

    #[tokio::test]
    async fn test_record() {
        let fixture_name = "records/record.toml";
        let _ = fs::remove_file(fixture_name).await.unwrap_or_else(|_| ());
        let store = RecordReplayFileStore::from(fixture_name).await.unwrap();
        let mut replay = RecordReplay::start(store).await.unwrap();
        let mocked_origin = replay.add_origin("https://example.com");

        let client = Client::new();
        let response = client
            .get(mocked_origin)
            .send()
            .await
            .expect("Failed to send request");
        let text = response.text().await.expect("Failed to read response");

        replay.commit().await;

        let records_str = fs::read_to_string(fixture_name)
            .await
            .expect("Failed to read file");
        let records: RecordReplayRecords =
            toml::from_str(&records_str).expect("Failed to parse TOML");

        assert_eq!(records.requests.len(), 1);

        let request = records.requests.get(0).expect("Failed to find request");
        assert_eq!(request.method, "GET");
        assert_eq!(request.uri, "https://example.com/");
        assert_eq!(request.response.status, 200);
        assert_eq!(request.response.body, text);

        let _ = fs::remove_file(fixture_name).await.unwrap();
    }
}