test: add health check test

This commit is contained in:
2025-06-22 13:51:51 +03:00
parent 85765bb3b0
commit 813346a340
13 changed files with 715 additions and 34 deletions

View File

@@ -34,3 +34,13 @@ tracing = { version = "0.1", features = ["log"] }
tracing-bunyan-formatter = { version = "0.3", default-features = false }
tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
uuid.workspace = true
[dev-dependencies]
fake = "4.3"
once_cell = "1.21"
quickcheck = "1.0"
quickcheck_macros = "1.0"
reqwest = "0.12.20"
serde_json = "1"
wiremock = "0.6"

View File

@@ -5,7 +5,7 @@ use sqlx::{
ConnectOptions,
postgres::{PgConnectOptions, PgSslMode},
};
use std::{fmt::Display, str::FromStr};
use std::{env::current_dir, fmt::Display, path::PathBuf, str::FromStr};
use thiserror::Error;
/// Top-level application settings.
@@ -62,9 +62,11 @@ pub enum ConfigurationError {
///
/// # Errors
/// Returns a `ConfigurationError` if the configuration cannot be loaded.
pub fn get_config() -> Result<Settings, ConfigurationError> {
let base_path = std::env::current_dir().expect("Failed to determine current directory");
let config_directory = base_path.join("config");
pub fn get_config(path: Option<PathBuf>) -> Result<Settings, ConfigurationError> {
let config_directory = path.unwrap_or_else(|| {
let base_path = current_dir().expect("Failed to determine current directory");
base_path.join("config")
});
let env = std::env::var("APP_ENVIRONMENT")
.unwrap_or_else(|_| "local".into())
.parse::<Environment>()?;

View File

@@ -1,15 +1,14 @@
use server::{
configuration::get_config,
startup::Application,
startup::{Application, ApplicationError},
telemetry::{get_subscriber, init_subscriber},
};
use std::io::Error;
#[tokio::main]
async fn main() -> Result<(), Error> {
async fn main() -> Result<(), ApplicationError> {
let subscriber = get_subscriber("kristofersxyz", "info", std::io::stdout);
init_subscriber(subscriber);
let config = get_config().expect("Failed to read configuation.");
let config = get_config(None).expect("Failed to read configuation.");
let application = Application::build(&config).await?;
application.start().await?;
Ok(())

View File

@@ -1,6 +1,8 @@
mod v1;
use crate::startup::AppState;
use app::{App, shell};
use axum::{Router, extract::State, http::StatusCode, response::IntoResponse, routing::get};
use axum::Router;
use leptos_axum::{LeptosRoutes, generate_route_list};
pub fn route(state: AppState) -> Router {
@@ -15,13 +17,8 @@ pub fn route(state: AppState) -> Router {
.fallback(leptos_axum::file_and_error_handler(shell))
.with_state(leptos_options);
let api_router = Router::new()
.route("/health_check", get(health_check))
.with_state(state);
let v1_router = v1::route(state);
let api_router = Router::new().nest("/api", v1_router);
leptos_router.merge(api_router)
}
async fn health_check(State(_state): State<AppState>) -> impl IntoResponse {
StatusCode::OK
}

View File

@@ -0,0 +1,6 @@
use crate::startup::AppState;
use axum::{extract::State, http::StatusCode, response::IntoResponse};
pub async fn health_check(State(_state): State<AppState>) -> impl IntoResponse {
StatusCode::OK
}

View File

@@ -0,0 +1,13 @@
mod health_check;
use crate::startup::AppState;
use axum::{Router, routing::get};
pub fn route(state: AppState) -> Router {
Router::new()
.nest(
"/v1",
Router::new().route("/health_check", get(health_check::health_check)),
)
.with_state(state)
}

View File

@@ -4,20 +4,27 @@ use crate::{
};
use leptos::config::{LeptosOptions, get_configuration};
use sqlx::{PgPool, postgres::PgPoolOptions};
use std::{
io::{Error, ErrorKind},
net::SocketAddr,
ops::Deref,
sync::Arc,
};
use std::{io::Error, net::SocketAddr, ops::Deref, sync::Arc};
use thiserror::Error;
use tokio::{net::TcpListener, task::JoinHandle};
use tracing::{error, info};
#[derive(Debug, Error)]
pub enum ApplicationError {
#[error("Failed to get Leptos configuration: {0}")]
LeptosConfig(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Server error: {0}")]
Server(String),
}
/// Shared application state.
pub struct App {
pub pool: PgPool,
pub leptos_options: LeptosOptions,
pub addr: SocketAddr,
}
/// Type alias for the shared application state wrapped in `Arc`.
@@ -25,7 +32,8 @@ pub type AppState = Arc<App>;
/// Represents the application, including its server and configuration.
pub struct Application {
server: Option<JoinHandle<Result<(), Error>>>,
server: Option<JoinHandle<Result<(), ApplicationError>>>,
addr: SocketAddr,
}
impl Application {
@@ -33,7 +41,7 @@ impl Application {
///
/// This method initializes the database connection pool, binds the server
/// to the specified address, and prepares the application for startup.
pub async fn build(config: &Settings) -> Result<Self, Error> {
pub async fn build(config: &Settings) -> Result<Self, ApplicationError> {
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
@@ -46,7 +54,6 @@ impl Application {
let app_state = App {
pool,
leptos_options: leptos_options.clone(),
addr,
}
.into();
@@ -56,26 +63,58 @@ impl Application {
let server = tokio::spawn(async move {
axum::serve(listener, route(app_state)).await.map_err(|e| {
error!("Server error: {}", e);
e
Error::other(e.to_string()).into()
})
});
Ok(Self {
server: Some(server),
addr,
})
}
pub async fn start(self) -> Result<(), Error> {
self.server
.ok_or_else(|| Error::new(ErrorKind::Other, "Server was not initialized."))?
.await?
pub async fn start(self) -> Result<(), ApplicationError> {
let server = self
.server
.ok_or_else(|| ApplicationError::Server("Server was not initialized.".to_string()))?;
server
.await
.map_err(|e| ApplicationError::Server(e.to_string()))?
}
/// Returns the socket address the server is listening on.
///
/// This method provides access to the full socket address (IP and port) that the server is bound to.
///
/// # Returns
///
/// Returns a `SocketAddr` containing the server's bound address.
#[must_use]
#[inline]
pub fn addr(&self) -> SocketAddr {
self.addr
}
/// Returns the port number the server is listening on.
///
/// This is a convenience method that extracts just the port number from the server's socket address.
///
/// # Returns
///
/// Returns a `u16` containing the server's port number.
#[must_use]
#[inline]
pub fn port(&self) -> u16 {
self.addr.port()
}
}
/// Initialize the database connection pool.
///
/// This method creates a lazy connection pool using the provided database settings.
fn get_connection_pool(config: &DatabaseSettings) -> PgPool {
#[must_use]
pub fn get_connection_pool(config: &DatabaseSettings) -> PgPool {
PgPoolOptions::new().connect_lazy_with(config.with_db())
}

View File

@@ -0,0 +1,17 @@
use crate::helpers::spawn_app;
use reqwest::Client;
#[tokio::test]
async fn health_check() {
let app = spawn_app().await;
let url = format!("{}/health_check", &app.addr);
let client = Client::new();
let response = client
.get(&url)
.send()
.await
.expect("Failed to execute request");
assert!(response.status().is_success());
assert_eq!(Some(0), response.content_length());
}

View File

@@ -0,0 +1,85 @@
use once_cell::sync::Lazy;
use server::{
configuration::{DatabaseSettings, get_config},
startup::{Application, get_connection_pool},
telemetry::{get_subscriber, init_subscriber},
};
use sqlx::{Connection, Executor, PgConnection, PgPool};
use std::{env::current_dir, net::SocketAddr};
use uuid::Uuid;
static TRACING: Lazy<()> = Lazy::new(|| {
let default_filter_level = "trace";
let subscriber_name = "test";
if std::env::var("TEST_LOG").is_ok() {
let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::stdout);
init_subscriber(subscriber);
} else {
let subscriber = get_subscriber(default_filter_level, subscriber_name, std::io::sink);
init_subscriber(subscriber);
}
});
pub struct TestApp {
pub _pool: PgPool,
pub addr: SocketAddr,
}
pub async fn spawn_app() -> TestApp {
Lazy::force(&TRACING);
let config = {
let path = current_dir()
.expect("Failed to determine current directory")
.parent()
.map(|p| p.join("config"));
let mut c = get_config(path).expect("Failed to read configuration.");
c.database.database_name = Uuid::new_v4().to_string();
c.application.port = 0;
c
};
configure_database(&config.database).await;
let application = Application::build(&config)
.await
.expect("Failed to build application.");
let addr = application.addr();
let _ = tokio::spawn(application.start()).await;
TestApp {
_pool: get_connection_pool(&config.database),
addr,
}
}
async fn configure_database(config: &DatabaseSettings) -> PgPool {
let mut connection = PgConnection::connect_with(&config.without_db())
.await
.expect("Failed to connect to Postgres.");
connection
.execute(
format!(
r#"
CREATE DATABASE "{}"
"#,
config.database_name
)
.as_str(),
)
.await
.expect("Failed to create database.");
let pool = PgPool::connect_with(config.with_db())
.await
.expect("Failed to connect to Postgres.");
sqlx::migrate!("../migrations")
.run(&pool)
.await
.expect("Failed to migrate database");
pool
}

2
server/tests/api/main.rs Normal file
View File

@@ -0,0 +1,2 @@
mod health_check;
mod helpers;