mirror of
https://github.com/kristoferssolo/kristofersxyz-rs.git
synced 2026-02-04 06:42:06 +00:00
test: add health check test
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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>()?;
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
6
server/src/routes/v1/health_check.rs
Normal file
6
server/src/routes/v1/health_check.rs
Normal 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
|
||||
}
|
||||
13
server/src/routes/v1/mod.rs
Normal file
13
server/src/routes/v1/mod.rs
Normal 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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
17
server/tests/api/health_check.rs
Normal file
17
server/tests/api/health_check.rs
Normal 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());
|
||||
}
|
||||
85
server/tests/api/helpers.rs
Normal file
85
server/tests/api/helpers.rs
Normal 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
2
server/tests/api/main.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod health_check;
|
||||
mod helpers;
|
||||
Reference in New Issue
Block a user