From 813346a3403ab454a284e7ed4aab38e5e1a85955 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Sun, 22 Jun 2025 13:51:51 +0300 Subject: [PATCH] test: add health check test --- Cargo.lock | 459 +++++++++++++++++++++++++++ Cargo.toml | 10 +- scripts/init_db | 46 +++ server/Cargo.toml | 10 + server/src/configuration.rs | 10 +- server/src/main.rs | 7 +- server/src/routes/mod.rs | 13 +- server/src/routes/v1/health_check.rs | 6 + server/src/routes/v1/mod.rs | 13 + server/src/startup.rs | 71 ++++- server/tests/api/health_check.rs | 17 + server/tests/api/helpers.rs | 85 +++++ server/tests/api/main.rs | 2 + 13 files changed, 715 insertions(+), 34 deletions(-) create mode 100755 scripts/init_db create mode 100644 server/src/routes/v1/health_check.rs create mode 100644 server/src/routes/v1/mod.rs create mode 100644 server/tests/api/health_check.rs create mode 100644 server/tests/api/helpers.rs create mode 100644 server/tests/api/main.rs diff --git a/Cargo.lock b/Cargo.lock index aea5af9..d5d5e18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,16 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-compression" version = "0.4.25" @@ -153,6 +163,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "attribute-derive" version = "0.10.3" @@ -491,6 +507,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -575,6 +601,24 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "deadpool" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + [[package]] name = "der" version = "0.7.10" @@ -606,6 +650,12 @@ dependencies = [ "syn", ] +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + [[package]] name = "digest" version = "0.10.7" @@ -669,6 +719,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -681,6 +741,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472" +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -713,6 +783,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fake" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5f203b70a419cb8880d1cfe6bebe488add0a0307d404e9f24021e5fd864b80" +dependencies = [ + "deunicode", + "rand 0.9.1", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "flate2" version = "1.1.2" @@ -746,6 +832,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -949,6 +1050,25 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" +[[package]] +name = "h2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1116,6 +1236,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1124,6 +1245,39 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -1132,14 +1286,24 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ + "base64", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration", "tokio", "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -1298,6 +1462,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "iri-string" version = "0.7.8" @@ -1602,6 +1772,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" version = "0.8.0" @@ -1731,6 +1907,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "next_tuple" version = "0.1.0" @@ -1844,6 +2037,50 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "or_poisoned" version = "0.1.0" @@ -2069,6 +2306,28 @@ dependencies = [ "yansi", ] +[[package]] +name = "quickcheck" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" +dependencies = [ + "env_logger", + "log", + "rand 0.8.5", +] + +[[package]] +name = "quickcheck_macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71ee38b42f8459a88d3362be6f9b841ad2d5421844f61eb1c59c11bff3ac14a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.40" @@ -2270,6 +2529,46 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -2340,6 +2639,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rustls" version = "0.23.28" @@ -2395,6 +2707,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2411,6 +2732,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.26" @@ -2529,12 +2873,18 @@ dependencies = [ "app", "axum", "config", + "fake", "leptos", "leptos_axum", "log", + "once_cell", + "quickcheck", + "quickcheck_macros", + "reqwest", "secrecy", "serde", "serde-aux", + "serde_json", "simple_logger", "sqlx", "thiserror 2.0.12", @@ -2545,6 +2895,8 @@ dependencies = [ "tracing-bunyan-formatter", "tracing-log 0.2.0", "tracing-subscriber", + "uuid", + "wiremock", ] [[package]] @@ -2974,6 +3326,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2986,6 +3341,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tachys" version = "0.2.3" @@ -3021,6 +3397,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3165,6 +3554,26 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -3388,6 +3797,12 @@ dependencies = [ "tracing-log 0.2.0", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tungstenite" version = "0.26.2" @@ -3550,6 +3965,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3765,6 +4189,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-registry" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -3940,6 +4375,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "wiremock" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b8b99d4cdbf36b239a9532e31fe4fb8acc38d1897c1761e161550a7dc78e6a" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/Cargo.toml b/Cargo.toml index 1208180..15f283d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,10 +24,16 @@ serde = { version = "1", features = ["derive"] } simple_logger = "5.0.0" thiserror = "2" tokio = { version = "1.45", features = ["rt", "macros", "rt-multi-thread"] } -tower = { version = "0.5.2", features = ["full"] } -tower-http = { version = "0.6.4", features = ["full"] } +tower = { version = "0.5", features = ["full"] } +tower-http = { version = "0.6", features = ["full"] } +uuid = { version = "1.17", features = ["v4"] } wasm-bindgen = "=0.2.100" +[workspace.lints.clippy] +pedantic = "warn" +nursery = "warn" +unwrap_used = "warn" + # See https://github.com/leptos-rs/cargo-leptos for documentation of all the parameters. # A leptos project defines which workspace members diff --git a/scripts/init_db b/scripts/init_db new file mode 100755 index 0000000..6b9eaa7 --- /dev/null +++ b/scripts/init_db @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -eo pipefail + +if ! [ -x "$(command -v psql)" ]; then + echo >&2 "Error: psql is not installed." + exit 1 +fi + +if ! [ -x "$(command -v sqlx)" ]; then + echo >&2 "Error: sqlx is not installed." + echo >&2 "Use:" + echo >&2 " cargo install sqlx-cli --no-default-features --features rustls,postgres" + echo >&2 "to install it." + exit 1 +fi + +DB_USER="${POSTGRES_USER:=postgres}" +DB_PASSWORD="${POSTGRES_PASSWORD:=password}" +DB_NAME="${POSTGRES_DB:=kristofersxyz}" +DB_PORT="${POSTGRES_PORT:=5432}" +DB_HOST="${POSTGRES_HOST:=localhost}" + +if [[ -z "${SKIP_DOCKER}" ]]; then + docker run\ + -e POSTGRES_USER=${DB_USER}\ + -e POSTGRES_PASSWORD=${DB_PASSWORD}\ + -e POSTGRES_DB=${DB_NAME}\ + -p "${DB_PORT}":5432\ + -d postgres\ + postgres -N 1000 + # Increase max number of connections for testing purposes +fi + +# Keep pinging Postgres until it's ready to accept commands +export PGPASSWORD="${DB_PASSWORD}" +until psql -h "${DB_HOST}" -U "${DB_USER}" -p "${DB_PORT}" -d "postgres" -c '\q'; do + >&2 echo "Postgres is still unavailable - sleeping" + sleep 1 +done + +>&2 echo "Postgres is still up and running on port ${DB_PORT} - runing migrations now!" + +DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}" +export DATABASE_URL +sqlx database create +sqlx migrate run diff --git a/server/Cargo.toml b/server/Cargo.toml index 329bd6e..133fb1b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -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" diff --git a/server/src/configuration.rs b/server/src/configuration.rs index afd6508..a5f5a6f 100644 --- a/server/src/configuration.rs +++ b/server/src/configuration.rs @@ -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 { - 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) -> Result { + 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::()?; diff --git a/server/src/main.rs b/server/src/main.rs index b49f3fb..3d94c2f 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -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(()) diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index 225bfca..fff9ea8 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -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) -> impl IntoResponse { - StatusCode::OK -} diff --git a/server/src/routes/v1/health_check.rs b/server/src/routes/v1/health_check.rs new file mode 100644 index 0000000..4afb431 --- /dev/null +++ b/server/src/routes/v1/health_check.rs @@ -0,0 +1,6 @@ +use crate::startup::AppState; +use axum::{extract::State, http::StatusCode, response::IntoResponse}; + +pub async fn health_check(State(_state): State) -> impl IntoResponse { + StatusCode::OK +} diff --git a/server/src/routes/v1/mod.rs b/server/src/routes/v1/mod.rs new file mode 100644 index 0000000..7c3cb89 --- /dev/null +++ b/server/src/routes/v1/mod.rs @@ -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) +} diff --git a/server/src/startup.rs b/server/src/startup.rs index a87cbac..7fda6d2 100644 --- a/server/src/startup.rs +++ b/server/src/startup.rs @@ -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; /// Represents the application, including its server and configuration. pub struct Application { - server: Option>>, + server: Option>>, + 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 { + pub async fn build(config: &Settings) -> Result { 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()) } diff --git a/server/tests/api/health_check.rs b/server/tests/api/health_check.rs new file mode 100644 index 0000000..70f5c6f --- /dev/null +++ b/server/tests/api/health_check.rs @@ -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()); +} diff --git a/server/tests/api/helpers.rs b/server/tests/api/helpers.rs new file mode 100644 index 0000000..9f69f51 --- /dev/null +++ b/server/tests/api/helpers.rs @@ -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 +} diff --git a/server/tests/api/main.rs b/server/tests/api/main.rs new file mode 100644 index 0000000..c76d49f --- /dev/null +++ b/server/tests/api/main.rs @@ -0,0 +1,2 @@ +mod health_check; +mod helpers;