feat: add tests for upstream and upstream target handlers

This commit is contained in:
GW_MC
2025-12-29 17:44:41 +08:00
parent 6eb0d9060b
commit 41023939db
4 changed files with 471 additions and 0 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}