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