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