added unit tests

This commit is contained in:
2025-07-04 17:19:36 +08:00
parent bdad0511f2
commit b11533e819
7 changed files with 402 additions and 32 deletions

3
.env
View File

@@ -4,3 +4,6 @@ PGUSER=postgres
PGHOST=127.0.0.1
PGPASSWORD=password
INTRASYS_LOG=trace
#Only use for tests
DATABASE_URL=postgres://postgres:password@localhost:5432

233
Cargo.lock generated
View File

@@ -47,6 +47,22 @@ dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[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 = "atoi"
version = "2.0.0"
@@ -56,6 +72,12 @@ dependencies = [
"num-traits",
]
[[package]]
name = "auto-future"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373"
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -116,6 +138,36 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-test"
version = "17.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac"
dependencies = [
"anyhow",
"assert-json-diff",
"auto-future",
"axum",
"bytes",
"bytesize",
"cookie",
"http",
"http-body-util",
"hyper",
"hyper-util",
"mime",
"pretty_assertions",
"reserve-port",
"rust-multipart-rfc7578_2",
"serde",
"serde_json",
"serde_urlencoded",
"smallvec",
"tokio",
"tower",
"url",
]
[[package]]
name = "backtrace"
version = "0.3.75"
@@ -193,6 +245,12 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "bytesize"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba"
[[package]]
name = "cc"
version = "1.2.27"
@@ -235,6 +293,16 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"time",
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -301,6 +369,21 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deranged"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
dependencies = [
"powerfmt",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "digest"
version = "0.10.7"
@@ -638,6 +721,7 @@ dependencies = [
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
@@ -647,13 +731,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
"libc",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
]
[[package]]
@@ -802,6 +891,7 @@ name = "intrasys"
version = "0.1.0"
dependencies = [
"axum",
"axum-test",
"bigdecimal",
"dotenv",
"serde",
@@ -983,11 +1073,17 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"rand",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
@@ -1131,6 +1227,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1140,6 +1242,16 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
@@ -1171,8 +1283,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
]
[[package]]
@@ -1182,7 +1304,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
@@ -1194,6 +1326,15 @@ dependencies = [
"getrandom 0.2.16",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "redox_syscall"
version = "0.5.13"
@@ -1247,6 +1388,15 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reserve-port"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356"
dependencies = [
"thiserror",
]
[[package]]
name = "rsa"
version = "0.9.8"
@@ -1260,13 +1410,28 @@ dependencies = [
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rust-multipart-rfc7578_2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41"
dependencies = [
"bytes",
"futures-core",
"futures-util",
"http",
"mime",
"rand 0.9.1",
"thiserror",
]
[[package]]
name = "rustc-demangle"
version = "0.1.25"
@@ -1398,7 +1563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
@@ -1563,7 +1728,7 @@ dependencies = [
"memchr",
"once_cell",
"percent-encoding",
"rand",
"rand 0.8.5",
"rsa",
"serde",
"sha1",
@@ -1604,7 +1769,7 @@ dependencies = [
"memchr",
"num-bigint",
"once_cell",
"rand",
"rand 0.8.5",
"serde",
"serde_json",
"sha2",
@@ -1721,6 +1886,37 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.1"
@@ -1878,6 +2074,12 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.18.0"
@@ -1957,6 +2159,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[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"
@@ -2290,6 +2501,12 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
version = "0.8.0"

View File

@@ -15,3 +15,9 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
dotenv = "0.15"
bigdecimal = { version = "0.4", features = ["serde"] }
[lib]
crate-type = ["bin"]
[dev-dependencies]
axum-test = "17.3"

View File

@@ -26,7 +26,7 @@ Intrasys is an internal financial transfers application built with Rust and Axum
### 1. Provision PostgreSQL Database
Run a PostgreSQL container with Docker:
The easiest way is to run a PostgreSQL container with Docker:
```bash
docker run --name intrasys-pg -d -e POSTGRES_PASSWORD=password -p 127.0.0.1:5432:5432 postgres:alpine
@@ -101,14 +101,6 @@ The application will be available at `http://localhost:3000`
sqlx migrate run
```
## Testing
Run the test suite with:
```bash
cargo test
```
## Assumptions
- All accounts use the same currency

View File

@@ -1,8 +1,9 @@
use std::str::FromStr;
use axum::{
Json,
Json, Router,
extract::{Path, State},
routing::{get, post},
};
use bigdecimal::BigDecimal;
use sqlx::types::BigDecimal as SqlxBigDecimal;
@@ -14,7 +15,7 @@ use crate::{
models::{Account, CreateAccount, TransactionRequest},
};
pub(crate) async fn create_account(
async fn create_account(
State(pool): State<DbPool>,
Json(payload): Json<CreateAccount>,
) -> Result<(), AppError> {
@@ -48,7 +49,7 @@ pub(crate) async fn create_account(
Ok(())
}
pub(crate) async fn get_account(
async fn get_account(
State(pool): State<DbPool>,
Path(account_id): Path<i64>,
) -> Result<Json<Account>, AppError> {
@@ -74,7 +75,7 @@ pub(crate) async fn get_account(
Ok(Json(account))
}
pub(crate) async fn process_transaction(
async fn process_transaction(
State(pool): State<DbPool>,
Json(payload): Json<TransactionRequest>,
) -> Result<(), AppError> {
@@ -165,3 +166,11 @@ pub(crate) async fn process_transaction(
Ok(())
}
pub(crate) fn create_router(pool: DbPool) -> Router {
Router::new()
.route("/accounts", post(create_account))
.route("/accounts/{account_id}", get(get_account))
.route("/transactions", post(process_transaction))
.with_state(pool)
}

View File

@@ -1,10 +1,5 @@
use std::env;
use axum::{
Router,
routing::{get, post},
};
use dotenv::dotenv;
use std::env;
use tracing::info;
use tracing_subscriber::layer::SubscriberExt;
@@ -34,11 +29,7 @@ async fn main() {
.expect("Failed to run migrations");
// Build the application's routes
let app = Router::new()
.route("/accounts", post(handlers::create_account))
.route("/accounts/{account_id}", get(handlers::get_account))
.route("/transactions", post(handlers::process_transaction))
.with_state(pool);
let app = handlers::create_router(pool);
// Run the server
let host = env::var("INTRASYS_HOST").unwrap_or(String::from("127.0.0.1"));
@@ -49,3 +40,6 @@ async fn main() {
info!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
#[cfg(test)]
mod tests;

149
src/tests.rs Normal file
View File

@@ -0,0 +1,149 @@
use std::str::FromStr;
use crate::handlers::create_router;
use axum::http::StatusCode;
use axum_test::TestServer;
use bigdecimal::BigDecimal;
use serde_json::{Value, json};
use sqlx::{Pool, Postgres};
#[sqlx::test]
async fn test_get_account(pool: Pool<Postgres>) {
let app = create_router(pool.clone());
let server = TestServer::new(app).unwrap();
let response = server.get("/accounts/3").await;
//Account does not exist yet
assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
// Create test account
sqlx::query("INSERT INTO accounts (account_id, balance) VALUES (3, 150.50)")
.execute(&pool)
.await
.unwrap();
let response = server.get("/accounts/3").await;
assert_eq!(response.status_code(), StatusCode::OK);
let account: Value = response.json();
assert_eq!(3, account["account_id"]);
assert_eq!(
BigDecimal::from_str("150.50"),
BigDecimal::from_str(account["balance"].as_str().unwrap())
);
let response = server.get("/accounts/4").await;
//Account does not exist
assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
}
#[sqlx::test]
async fn test_create_account_success(pool: Pool<Postgres>) {
let app = create_router(pool.clone());
let server = TestServer::new(app).unwrap();
let response = server
.post("/accounts")
.json(&json!({
"account_id": 1,
"initial_balance": "100.00"
}))
.await;
assert_eq!(response.status_code(), StatusCode::OK);
// Verify account was created
let account = sqlx::query_as::<_, (i64, BigDecimal)>(
"SELECT account_id, balance FROM accounts WHERE account_id = 1",
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(account.0, 1);
assert_eq!(account.1, BigDecimal::from(100));
}
#[sqlx::test]
async fn test_process_transaction_success(pool: Pool<Postgres>) {
let app = create_router(pool.clone());
let server = TestServer::new(app).unwrap();
// Create test accounts
sqlx::query("INSERT INTO accounts (account_id, balance) VALUES (4, 200.00), (5, 50.00)")
.execute(&pool)
.await
.unwrap();
let response = server
.post("/transactions")
.json(&json!({
"source_account_id": 4,
"destination_account_id": 5,
"amount": "75.25"
}))
.await;
assert_eq!(response.status_code(), StatusCode::OK);
// Verify balances were updated
let source = sqlx::query_as::<_, (i64, BigDecimal)>(
"SELECT account_id, balance FROM accounts WHERE account_id = 4",
)
.fetch_one(&pool)
.await
.unwrap();
let destination = sqlx::query_as::<_, (i64, BigDecimal)>(
"SELECT account_id, balance FROM accounts WHERE account_id = 5",
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(source.1, BigDecimal::from_str("124.75").unwrap());
assert_eq!(destination.1, BigDecimal::from_str("125.25").unwrap());
}
#[sqlx::test]
async fn test_process_transaction_insufficient_funds(pool: Pool<Postgres>) {
let app = create_router(pool.clone());
let server = TestServer::new(app).unwrap();
// Create test accounts
sqlx::query("INSERT INTO accounts (account_id, balance) VALUES (6, 50.00), (7, 100.00)")
.execute(&pool)
.await
.unwrap();
let response = server
.post("/transactions")
.json(&json!({
"source_account_id": 6,
"destination_account_id": 7,
"amount": "75.00"
}))
.await;
assert_eq!(response.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(response.text(), "Insufficient funds");
// Verify balances were not changed
let source = sqlx::query_as::<_, (i64, BigDecimal)>(
"SELECT account_id, balance FROM accounts WHERE account_id = 6",
)
.fetch_one(&pool)
.await
.unwrap();
let destination = sqlx::query_as::<_, (i64, BigDecimal)>(
"SELECT account_id, balance FROM accounts WHERE account_id = 7",
)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(source.1, BigDecimal::from_str("50.00").unwrap());
assert_eq!(destination.1, BigDecimal::from_str("100.00").unwrap());
}