feat: add tests for upstream and upstream target handlers
This commit is contained in:
147
Cargo.lock
generated
147
Cargo.lock
generated
@@ -292,6 +292,35 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-test"
|
||||
version = "18.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3290e73c56c5cc4701cdd7d46b9ced1b4bd61c7e9f9c769a9e9e87ff617d75d2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"bytes",
|
||||
"bytesize",
|
||||
"cookie",
|
||||
"expect-json",
|
||||
"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 = "base16ct"
|
||||
version = "0.2.0"
|
||||
@@ -509,6 +538,12 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "bytesize"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.45"
|
||||
@@ -994,6 +1029,12 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -1123,6 +1164,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email_address"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
@@ -1191,6 +1241,35 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "expect-json"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "422e7906e79941e5ac58c64dfd2da03e6ae3de62227f87606fbbe125d91080f9"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"email_address",
|
||||
"expect-json-macros",
|
||||
"num",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"typetag",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "expect-json-macros"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6b515b7f10f1e61bfd938522e9884509b82060af2016153f5b3d6f44d6da89c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
@@ -1925,6 +2004,15 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -2752,6 +2840,16 @@ dependencies = [
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[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 = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@@ -3126,6 +3224,15 @@ dependencies = [
|
||||
"webpki-roots 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reserve-port"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21918d6644020c6f6ef1993242989bf6d4952d2e025617744f184c02df51c356"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
@@ -3223,6 +3330,21 @@ dependencies = [
|
||||
"ordered-multimap",
|
||||
]
|
||||
|
||||
[[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.2",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.39.0"
|
||||
@@ -4695,6 +4817,30 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "typetag"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf"
|
||||
dependencies = [
|
||||
"erased-serde",
|
||||
"inventory",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"typetag-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typetag-impl"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.7"
|
||||
@@ -5444,6 +5590,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-extra",
|
||||
"axum-test",
|
||||
"chrono",
|
||||
"clap",
|
||||
"config",
|
||||
|
||||
@@ -35,6 +35,7 @@ optfield = { version = "0.4.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
axum-test = "18.4.1"
|
||||
|
||||
[lints.clippy]
|
||||
unwrap_used = "deny"
|
||||
|
||||
@@ -153,3 +153,202 @@ pub async fn get_upstream(
|
||||
//
|
||||
Ok(Json(upstream_info.into()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||
|
||||
use database::generated::entities::{upstream, upstream_target};
|
||||
|
||||
use crate::configs::{FromConfig, ProgramSettings};
|
||||
|
||||
use crate::routes::api::restricted::nginx::upstream::get_upstream_router;
|
||||
use crate::services::get_app_service;
|
||||
|
||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||
let program_settings = ProgramSettings::mock();
|
||||
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
||||
let state = Arc::new(crate::routes::AppState {
|
||||
database_connection: Arc::new(db),
|
||||
service: Arc::new(app_service),
|
||||
config: Arc::new(program_settings),
|
||||
});
|
||||
get_upstream_router(state)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_upstream_list_returns_list() {
|
||||
let u1 = upstream::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: "u1".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "rr".to_string(),
|
||||
sticky_session: false,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
let u2 = upstream::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: "u2".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "rr".to_string(),
|
||||
sticky_session: false,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![u1.clone(), u2.clone()]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server.get("/upstreams").await;
|
||||
res.assert_status_ok();
|
||||
let body = res.json::<UpstreamListResponse>();
|
||||
assert_eq!(body.items.len(), 2);
|
||||
assert_eq!(body.pagination.current_page, 1u32);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_upstream_with_targets_returns_targets() {
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
|
||||
let up_model = upstream::Model {
|
||||
id: up_id,
|
||||
name: "with_targets".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "least_conn".to_string(),
|
||||
sticky_session: true,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let target_model = upstream_target::Model {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
upstream_id: up_id,
|
||||
target_host: "127.0.0.1".to_string(),
|
||||
target_port: 8080,
|
||||
weight: 1,
|
||||
is_backup: false,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
// find_by_id -> returns upstream model
|
||||
.append_query_results(vec![vec![up_model.clone()]])
|
||||
// find targets -> returns the target(s)
|
||||
.append_query_results(vec![vec![target_model.clone()]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let url = format!("/upstreams/{}?include_targets=true", up_id);
|
||||
let res = server.get(&url).await;
|
||||
res.assert_status_ok();
|
||||
let body = res.json::<UpstreamInfoResponse>();
|
||||
assert_eq!(body.id, up_id);
|
||||
assert_eq!(body.upstream_targets.len(), 1);
|
||||
assert_eq!(body.upstream_targets[0].target_host, "127.0.0.1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn extractor_pagination_validation_rejects_bad_values() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
// page = 0 should be rejected
|
||||
let res = server.get("/upstreams?page=0&per_page=10").await;
|
||||
res.assert_status(StatusCode::BAD_REQUEST);
|
||||
|
||||
// per_page out of range should be rejected
|
||||
let res = server.get("/upstreams?page=1&per_page=0").await;
|
||||
res.assert_status(StatusCode::BAD_REQUEST);
|
||||
|
||||
// valid values accepted
|
||||
let res = server.get("/upstreams?page=2&per_page=5").await;
|
||||
res.assert_status_ok();
|
||||
let body = res.json::<UpstreamListResponse>();
|
||||
assert_eq!(body.pagination.current_page, 2u32);
|
||||
assert_eq!(body.pagination.per_page, 5u32);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_upstream_not_found_returns_service_error() {
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let url = format!("/upstreams/{}?include_targets=false", up_id);
|
||||
let res = server.get(&url).await;
|
||||
res.assert_status(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_upstream_without_targets_returns_info() {
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
|
||||
let up_model = upstream::Model {
|
||||
id: up_id,
|
||||
name: "simple_up".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "rr".to_string(),
|
||||
sticky_session: false,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
// find_by_id -> returns upstream model
|
||||
.append_query_results(vec![vec![up_model.clone()]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
// include_targets omitted -> should not include targets
|
||||
let url = format!("/upstreams/{}", up_id);
|
||||
let res = server.get(&url).await;
|
||||
res.assert_status_ok();
|
||||
let body = res.json::<UpstreamInfoResponse>();
|
||||
assert_eq!(body.id, up_id);
|
||||
assert!(body.upstream_targets.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_upstream_list_empty_returns_empty_items() {
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let res = server.get("/upstreams?page=3&per_page=10").await;
|
||||
res.assert_status_ok();
|
||||
let body = res.json::<UpstreamListResponse>();
|
||||
assert_eq!(body.items.len(), 0);
|
||||
assert_eq!(body.pagination.current_page, 3u32);
|
||||
assert_eq!(body.pagination.per_page, 10u32);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,3 +97,127 @@ pub async fn get_upstream_target(
|
||||
.await?;
|
||||
Ok(Json(upstream_target_info.into()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer;
|
||||
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
|
||||
|
||||
use database::generated::entities::{upstream, upstream_target};
|
||||
|
||||
use crate::configs::{FromConfig, ProgramSettings};
|
||||
|
||||
use crate::routes::api::restricted::nginx::upstream::get_upstream_router;
|
||||
use crate::services::get_app_service;
|
||||
|
||||
fn get_router_with_state(db: DatabaseConnection) -> axum::Router {
|
||||
let program_settings = ProgramSettings::mock();
|
||||
let app_service = get_app_service(&Arc::new(db.clone()), &program_settings);
|
||||
let state = Arc::new(crate::routes::AppState {
|
||||
database_connection: Arc::new(db),
|
||||
service: Arc::new(app_service),
|
||||
config: Arc::new(program_settings),
|
||||
});
|
||||
get_upstream_router(state)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_upstream_target_with_upstream_returns_upstream() {
|
||||
let up_id = uuid::Uuid::new_v4();
|
||||
|
||||
let up_model = upstream::Model {
|
||||
id: up_id,
|
||||
name: "with_targets".to_string(),
|
||||
protocol: "http".to_string(),
|
||||
algorithm: "least_conn".to_string(),
|
||||
sticky_session: true,
|
||||
created_by: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let target_id = uuid::Uuid::new_v4();
|
||||
let target_model = upstream_target::Model {
|
||||
id: target_id,
|
||||
upstream_id: up_id,
|
||||
target_host: "127.0.0.1".to_string(),
|
||||
target_port: 8080,
|
||||
weight: 1,
|
||||
is_backup: false,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
// query returns joined (upstream_target, upstream)
|
||||
.append_query_results(vec![vec![(target_model.clone(), Some(up_model.clone()))]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let url = format!("/upstream_targets/{}?include_upstream=true", target_id);
|
||||
let res = server.get(&url).await;
|
||||
res.assert_status_ok();
|
||||
let text = res.text();
|
||||
println!("response body: {}", text);
|
||||
let body: UpstreamTargetInfo = serde_json::from_str(&text).expect("failed to parse json");
|
||||
assert_eq!(body.upstream_id, up_id);
|
||||
assert!(body.upstream.is_some());
|
||||
let upstream = body.upstream.expect("upstream to be present");
|
||||
assert_eq!(upstream.id, up_id);
|
||||
assert_eq!(upstream.name, "with_targets");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_upstream_target_without_upstream_returns_info() {
|
||||
let target_id = uuid::Uuid::new_v4();
|
||||
|
||||
let target_model = upstream_target::Model {
|
||||
id: target_id,
|
||||
upstream_id: uuid::Uuid::new_v4(),
|
||||
target_host: "10.0.0.1".to_string(),
|
||||
target_port: 9090,
|
||||
weight: 5,
|
||||
is_backup: false,
|
||||
enabled: true,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![vec![target_model.clone()]])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let url = format!("/upstream_targets/{}", target_id);
|
||||
let res = server.get(&url).await;
|
||||
res.assert_status_ok();
|
||||
let text = res.text();
|
||||
let body: UpstreamTargetInfo = serde_json::from_str(&text).expect("failed to parse json");
|
||||
assert_eq!(body.id, target_id);
|
||||
assert!(body.upstream.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn handler_get_upstream_target_not_found_returns_service_error() {
|
||||
let target_id = uuid::Uuid::new_v4();
|
||||
let db = MockDatabase::new(DatabaseBackend::Sqlite)
|
||||
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
|
||||
.into_connection();
|
||||
|
||||
let router = get_router_with_state(db.clone());
|
||||
let server = TestServer::new(router).expect("failed to create test server");
|
||||
|
||||
let url = format!("/upstream_targets/{}?include_upstream=false", target_id);
|
||||
let res = server.get(&url).await;
|
||||
res.assert_status(StatusCode::NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user