Skip to main content

omry_integration/
full_stack.rs

1//! "Stack" that includes a simple Omry client app, to facilitate integration tests
2//! with a containerised Omry server.
3use crate::flora_stack::{FloraStack, FloraStackOptions};
4use anyhow::Result;
5use std::sync::Arc;
6use std::time::Duration;
7pub use test_app::App;
8use tracing::instrument;
9
10/// Omry client app for tests.
11mod test_app {
12    use anyhow::{Context, Result};
13    use omry_service::OmryApi;
14    use omry_service::search::{SearchParameters, SearchResponse, SortBy};
15    use std::sync::Arc;
16    use tokio::time::{self, Duration};
17    use tracing::instrument;
18
19    use crate::bridge::ApiBridge;
20    use crate::flora_stack::{self, FloraStack};
21
22    /// Client "app" for testing.
23    #[derive(Debug, Clone)]
24    pub struct App {
25        /// Bridge API for the flora tarpc backend.
26        api: OmryApi,
27    }
28
29    impl App {
30        /// Initializes the app state.
31        #[must_use]
32        pub fn new(api: OmryApi) -> Self {
33            Self { api }
34        }
35
36        /// Constructs [`SearchParameters`] with the given text query,
37        /// and sends a search request to the Omry backend.
38        ///
39        /// Adds `sort_by` to the search parameters to sort hits by recency.
40        ///
41        /// ## Errors
42        /// Returns [`anyhow::Error`] if the search RPC fails.
43        #[instrument]
44        pub async fn search(&self, query: String, page: Option<usize>) -> Result<SearchResponse> {
45            const BUCKETS: u32 = 5;
46            const QUERY_BY: &[&str] = &["title", "text-body"];
47
48            let query_by = QUERY_BY
49                .iter()
50                .map(std::string::ToString::to_string)
51                .collect::<Vec<_>>();
52
53            let sort_by = SortBy::relevance_and_recency(BUCKETS);
54            let page = page.unwrap_or(1);
55            let params = SearchParameters::new_query(query, query_by)
56                .num_typos(2)
57                .sort_by(sort_by)
58                .page(page);
59
60            ApiBridge::search(self, params.into()).await
61        }
62    }
63
64    impl ApiBridge for App {
65        fn api(&self) -> &OmryApi {
66            &self.api
67        }
68    }
69
70    async fn ping(api: &OmryApi) -> Result<String> {
71        let rx = api.ping().await?;
72        Ok(rx.await?)
73    }
74
75    /// Initializes a test client app, and checks that it can communicate
76    /// with the Omry server.
77    #[instrument]
78    pub(super) async fn init_app(flora_stack: Arc<FloraStack>) -> Result<App> {
79        const MAX_CONN_ATTEMPTS: usize = 5;
80        const BACKOFF_MULTIPLIER: u32 = 2;
81
82        tokio::spawn(async move {
83            let mut attempts_so_far = 0;
84            let mut backoff_interval = Duration::from_secs(1);
85
86            // Attempt to first create an Omry RPC client, then use that client
87            // to initialize our RPC bridge.
88            //
89            // We need to recreate the client on every iteration of the loop
90            // even if we initialized it successfully in a previous iteration
91            // because the client's connection to the tarpc server can shut down
92            // when we use tokio::time::sleep.
93
94            loop {
95                match flora_stack::api_container(&flora_stack).await {
96                    Ok(api) => match ping(&api).await {
97                        Ok(_) => break Ok(App::new(api)),
98                        Err(error) => {
99                            tracing::warn!("error while initializing the RPC client: {error}");
100                        }
101                    },
102                    Err(error) => {
103                        tracing::warn!("error while creating the flora client: {error}");
104                    }
105                }
106
107                if attempts_so_far == MAX_CONN_ATTEMPTS {
108                    anyhow::bail!("couldn't succeed after {MAX_CONN_ATTEMPTS} attempts");
109                }
110
111                time::sleep(backoff_interval).await;
112                backoff_interval *= BACKOFF_MULTIPLIER;
113                attempts_so_far += 1;
114            }
115        })
116        .await
117        .context("Error initializing the test app")?
118    }
119}
120
121/// Flora test stack.
122#[derive(Debug)]
123pub struct OmryFullStack {
124    /// Containerised Omry stack.
125    backend: Arc<FloraStack>,
126}
127
128impl OmryFullStack {
129    /// Creates a new test stack that includes an Omry backend (`omry-server` and Typesense),
130    /// and a client app to facilitate integration testing.
131    ///
132    /// ## Errors
133    ///
134    /// Returns [`anyhow::Error`] if we fail to:
135    /// - Initialize the backend stack
136    /// - Initialize the client app and connect it to that backend stack
137    #[instrument]
138    pub async fn new() -> Result<Self> {
139        let stack_options = FloraStackOptions {
140            search_sync_interval: Duration::from_secs(5),
141            ..Default::default()
142        };
143
144        let backend = Arc::new(FloraStack::new(Some(stack_options)).await?);
145        tracing::info!("successfully initialized the test bridge");
146        Ok(Self { backend })
147    }
148
149    /// Returns a reference to the Omry backend stack.
150    #[must_use]
151    pub fn backend(&self) -> &Arc<FloraStack> {
152        &self.backend
153    }
154
155    /// Returns a reference to the test client app.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`anyhow::Error`] if the test app can't be initialized
160    /// (usually because it can't connect to the test backend server).    
161    pub async fn init_app(&self) -> Result<App> {
162        test_app::init_app(Arc::clone(self.backend())).await
163    }
164
165    /// Attempts to stop ("kill") this Omry stack, shutting down
166    /// the connection(s) with the Omry backend.
167    ///
168    /// ## Errors
169    /// Returns [`anyhow::Error`] if we can't stop the Omry backend.
170    pub async fn kill_flora_rpc(&mut self) -> Result<()> {
171        self.backend.stop().await?;
172        Ok(())
173    }
174}