forked from torrust/torrust-index
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: isolated environments for integration testing
It allows integration test to run a custom env (with a custom configuration) and totally isolated from other tests. The test env used a socket address assigned from the OS (free port). You can do that by setting the port number to 0 in the config.toml file: ``` [net] port = 0 ```
- Loading branch information
1 parent
2211871
commit 6d5e002
Showing
11 changed files
with
257 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,7 @@ NCCA | |
nilm | ||
nocapture | ||
Oberhachingerstr | ||
oneshot | ||
ppassword | ||
reqwest | ||
Roadmap | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,5 @@ pub mod client; | |
pub mod connection_info; | ||
pub mod contexts; | ||
pub mod http; | ||
pub mod random; | ||
pub mod responses; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
//! Random data generators for testing. | ||
use rand::distributions::Alphanumeric; | ||
use rand::{thread_rng, Rng}; | ||
|
||
/// Returns a random alphanumeric string of a certain size. | ||
/// | ||
/// It is useful for generating random names, IDs, etc for testing. | ||
pub fn string(size: usize) -> String { | ||
thread_rng().sample_iter(&Alphanumeric).take(size).map(char::from).collect() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
use std::net::SocketAddr; | ||
|
||
use log::info; | ||
use tokio::sync::{oneshot, RwLock}; | ||
use torrust_index_backend::app; | ||
use torrust_index_backend::config::{Configuration, TorrustConfig}; | ||
|
||
/// It launches the app and provides a way to stop it. | ||
pub struct AppStarter { | ||
configuration: TorrustConfig, | ||
/// The application binary state (started or not): | ||
/// - `None`: if the app is not started, | ||
/// - `RunningState`: if the app was started. | ||
running_state: Option<RunningState>, | ||
} | ||
|
||
impl AppStarter { | ||
pub fn with_custom_configuration(configuration: TorrustConfig) -> Self { | ||
Self { | ||
configuration, | ||
running_state: None, | ||
} | ||
} | ||
|
||
pub async fn start(&mut self) { | ||
let configuration = Configuration { | ||
settings: RwLock::new(self.configuration.clone()), | ||
}; | ||
|
||
// Open a channel to communicate back with this function | ||
let (tx, rx) = oneshot::channel::<AppStarted>(); | ||
|
||
// Launch the app in a separate task | ||
let app_handle = tokio::spawn(async move { | ||
let app = app::run(configuration).await; | ||
|
||
// Send the socket address back to the main thread | ||
tx.send(AppStarted { | ||
socket_addr: app.socket_address, | ||
}) | ||
.expect("the app should not be dropped"); | ||
|
||
app.api_server.await | ||
}); | ||
|
||
// Wait until the app is started | ||
let socket_addr = match rx.await { | ||
Ok(msg) => msg.socket_addr, | ||
Err(e) => panic!("the app was dropped: {e}"), | ||
}; | ||
|
||
let running_state = RunningState { app_handle, socket_addr }; | ||
|
||
info!("Test environment started. Listening on {}", running_state.socket_addr); | ||
|
||
// Update the app state | ||
self.running_state = Some(running_state); | ||
} | ||
|
||
pub fn stop(&mut self) { | ||
match &self.running_state { | ||
Some(running_state) => { | ||
running_state.app_handle.abort(); | ||
self.running_state = None; | ||
} | ||
None => {} | ||
} | ||
} | ||
|
||
pub fn server_socket_addr(&self) -> Option<SocketAddr> { | ||
self.running_state.as_ref().map(|running_state| running_state.socket_addr) | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
pub struct AppStarted { | ||
pub socket_addr: SocketAddr, | ||
} | ||
|
||
/// Stores the app state when it is running. | ||
pub struct RunningState { | ||
app_handle: tokio::task::JoinHandle<std::result::Result<(), std::io::Error>>, | ||
pub socket_addr: SocketAddr, | ||
} | ||
|
||
impl Drop for AppStarter { | ||
/// Child threads spawned with `tokio::spawn()` and tasks spawned with | ||
/// `async { }` blocks will not be automatically killed when the owner of | ||
/// the struct that spawns them goes out of scope. | ||
/// | ||
/// The `tokio::spawn()` function and `async { }` blocks create an | ||
/// independent task that runs on a separate thread or the same thread, | ||
/// respectively. The task will continue to run until it completes, even if | ||
/// the owner of the struct that spawned it goes out of scope. | ||
/// | ||
/// However, it's important to note that dropping the owner of the struct | ||
/// may cause the task to be orphaned, which means that the task is no | ||
/// longer associated with any parent task or thread. Orphaned tasks can | ||
/// continue running in the background, consuming system resources, and may | ||
/// eventually cause issues if left unchecked. | ||
/// | ||
/// To avoid orphaned tasks, we ensure that the app ois stopped when the | ||
/// owner of the struct goes out of scope. | ||
/// | ||
/// This avoids having to call `TestEnv::stop()` explicitly at the end of | ||
/// each test. | ||
fn drop(&mut self) { | ||
// Stop the app when the owner of the struct goes out of scope | ||
self.stop(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,24 @@ | ||
use crate::common::asserts::{assert_response_title, assert_text_ok}; | ||
use crate::integration::environment::TestEnv; | ||
|
||
#[tokio::test] | ||
#[ignore] | ||
async fn it_should_load_the_about_page_with_information_about_the_api() { | ||
// todo: launch isolated API server for this test | ||
let env = TestEnv::running().await; | ||
let client = env.unauthenticated_client(); | ||
|
||
let response = client.about().await; | ||
|
||
assert_text_ok(&response); | ||
assert_response_title(&response, "About"); | ||
} | ||
|
||
#[tokio::test] | ||
async fn it_should_load_the_license_page_at_the_api_entrypoint() { | ||
let env = TestEnv::running().await; | ||
let client = env.unauthenticated_client(); | ||
|
||
let response = client.license().await; | ||
|
||
assert_text_ok(&response); | ||
assert_response_title(&response, "Licensing"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
use tempfile::TempDir; | ||
use torrust_index_backend::config::{TorrustConfig, FREE_PORT}; | ||
|
||
use super::app_starter::AppStarter; | ||
use crate::common::client::Client; | ||
use crate::common::connection_info::{anonymous_connection, authenticated_connection}; | ||
use crate::common::random; | ||
|
||
pub struct TestEnv { | ||
pub app_starter: AppStarter, | ||
pub temp_dir: TempDir, | ||
} | ||
|
||
impl TestEnv { | ||
/// Provides a running app instance for integration tests. | ||
pub async fn running() -> Self { | ||
let mut env = TestEnv::with_test_configuration(); | ||
env.start().await; | ||
env | ||
} | ||
|
||
/// Provides a test environment with a default configuration for testing | ||
/// application. | ||
pub fn with_test_configuration() -> Self { | ||
let temp_dir = TempDir::new().expect("failed to create a temporary directory"); | ||
|
||
let configuration = ephemeral(&temp_dir); | ||
|
||
let app_starter = AppStarter::with_custom_configuration(configuration); | ||
|
||
Self { app_starter, temp_dir } | ||
} | ||
|
||
/// Starts the app. | ||
pub async fn start(&mut self) { | ||
self.app_starter.start().await; | ||
} | ||
|
||
/// Provides an unauthenticated client for integration tests. | ||
pub fn unauthenticated_client(&self) -> Client { | ||
Client::new(anonymous_connection( | ||
&self | ||
.server_socket_addr() | ||
.expect("app should be started to get the server socket address"), | ||
)) | ||
} | ||
|
||
/// Provides an authenticated client for integration tests. | ||
pub fn _authenticated_client(&self, token: &str) -> Client { | ||
Client::new(authenticated_connection( | ||
&self | ||
.server_socket_addr() | ||
.expect("app should be started to get the server socket address"), | ||
token, | ||
)) | ||
} | ||
|
||
/// Provides the API server socket address. | ||
fn server_socket_addr(&self) -> Option<String> { | ||
self.app_starter.server_socket_addr().map(|addr| addr.to_string()) | ||
} | ||
} | ||
|
||
/// Provides a configuration with ephemeral data for testing. | ||
fn ephemeral(temp_dir: &TempDir) -> TorrustConfig { | ||
let mut configuration = TorrustConfig::default(); | ||
|
||
// Ephemeral API port | ||
configuration.net.port = FREE_PORT; | ||
|
||
// Ephemeral SQLite database | ||
configuration.database.connect_url = format!("sqlite://{}?mode=rwc", random_database_file_path_in(temp_dir)); | ||
|
||
configuration | ||
} | ||
|
||
fn random_database_file_path_in(temp_dir: &TempDir) -> String { | ||
let random_db_id = random::string(16); | ||
let db_file_name = format!("data_{random_db_id}.db"); | ||
temp_dir.path().join(db_file_name).to_string_lossy().to_string() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
pub mod app_starter; | ||
mod contexts; | ||
pub mod environment; |