feat: implement transaction handling for upstream and target operations

- Added transaction support in `add_upstream_target`, `remove_upstream`, `remove_upstream_target`, `update_upstream`, and `update_upstream_target` functions to ensure atomicity of operations.
- Updated the `NginxService` to include methods for validating and applying configurations using the agent service.
- Enhanced error handling in agent service interactions, returning appropriate internal server errors when agent communication fails.
- Introduced mock agent service for testing, allowing for simulation of agent interactions without actual network calls.
- Refactored tests to cover scenarios where agent operations fail, ensuring that internal server errors are returned as expected.
This commit is contained in:
GW_MC
2025-12-31 15:57:29 +08:00
parent 4f85d88380
commit 331b4e1e96
14 changed files with 975 additions and 71 deletions

View File

@@ -1,6 +1,7 @@
use std::sync::Arc;
use axum::{Json, extract::State, response::Result as AxumResult};
use sea_orm::TransactionTrait;
use crate::{
errors::api_error::ApiError,
@@ -104,7 +105,18 @@ pub async fn create_upstream(
.collect(),
};
let upstream_info = upstream_service.create_upstream(create_info, None).await?;
let mut tx = state.database_connection.begin().await?;
let upstream_info = upstream_service
.create_upstream(create_info, Some(&mut tx))
.await?;
state
.service
.nginx
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
.await?;
tx.commit().await?;
Ok(Json(upstream_info.into()))
}
@@ -126,12 +138,17 @@ mod tests {
create_upstream::{CreateUpstreamRequestBody, UpstreamTargetInfo as ReqTarget},
get_upstream_router,
},
services::get_app_service,
services::{agent_client::MockAgentService, get_app_service, get_mock_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 mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| Ok(()));
let mock_agent = Arc::new(mock);
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
@@ -174,6 +191,10 @@ mod tests {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![up_model.clone()]])
.append_query_results(vec![vec![target_model.clone()]])
// additional query result for regenerate_and_apply_config -> generate_config
// `find_with_related` returns rows of `(upstream, Option<target>)` which
// the mock DB expects as `(Model, Option<Model>)` per row.
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
.into_connection();
let router = get_router_with_state(db.clone());
@@ -218,6 +239,82 @@ mod tests {
res.assert_status(StatusCode::UNPROCESSABLE_ENTITY);
}
#[tokio::test]
async fn handler_create_upstream_agent_error_returns_internal() {
let up_id = uuid::Uuid::new_v4();
let up_model = upstream::Model {
id: up_id,
name: "new_upstream".to_string(),
protocol: "http".to_string(),
algorithm: "round_robin".to_string(),
sticky_session: false,
created_by: Some(uuid::Uuid::new_v4()),
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(),
};
// configure mock agent to error on apply
let mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| {
Err(crate::services::agent_client::AgentError::ApplicationFailed(
"internal".to_string(),
"Failed to communicate with the agent.".to_string(),
))
});
let mock_agent = Arc::new(mock);
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![up_model.clone()]])
.append_query_results(vec![vec![target_model.clone()]])
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
.into_connection();
let program_settings = ProgramSettings::mock();
let app_service = get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
config: Arc::new(program_settings),
});
let router = get_upstream_router(state).layer(axum::middleware::from_fn(
crate::middlewares::require_auth::mock::mock_require_auth,
));
let server = TestServer::new(router).expect("failed to create test server");
let payload = CreateUpstreamRequestBody {
name: "new_upstream".to_string(),
protocol: "http".to_string(),
algorithm: None,
sticky_session: None,
upstream_targets: vec![ReqTarget {
host: "127.0.0.1".to_string(),
port: 8080,
weight: None,
is_backup: None,
enabled: None,
}],
};
let res = server.post("/upstreams").json(&payload).await;
res.assert_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn handler_create_upstream_unauthenticated_returns_unauthorized() {
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();

View File

@@ -1,6 +1,7 @@
use std::sync::Arc;
use axum::{Json, extract::State, response::Result as AxumResult};
use sea_orm::TransactionTrait;
use crate::{
errors::api_error::ApiError,
@@ -61,10 +62,19 @@ pub async fn add_upstream_target(
upstream_id: concrete_payload.upstream_id,
};
let mut tx = state.database_connection.begin().await?;
let upstream_info = upstream_service
.create_upstream_target(create_info, None)
.create_upstream_target(create_info, Some(&mut tx))
.await?;
state
.service
.nginx
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
.await?;
tx.commit().await?;
Ok(Json(upstream_info.into()))
}
@@ -76,7 +86,7 @@ mod tests {
use axum_test::TestServer;
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
use database::generated::entities::upstream_target;
use database::generated::entities::{upstream, upstream_target};
use crate::{
configs::{FromConfig, ProgramSettings},
@@ -84,12 +94,17 @@ mod tests {
routes::api::restricted::nginx::upstream::{
create_upstream_target::CreateUpstreamTargetInfo, get_upstream_router,
},
services::get_app_service,
services::{agent_client::MockAgentService, get_mock_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 mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| Ok(()));
let mock_agent = Arc::new(mock);
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
@@ -100,6 +115,83 @@ mod tests {
))
}
#[tokio::test]
async fn handler_add_upstream_target_agent_error_returns_internal() {
let up_id = uuid::Uuid::new_v4();
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 up_model = upstream::Model {
id: up_id,
name: "test_upstream".to_string(),
protocol: "http".to_string(),
algorithm: "round_robin".to_string(),
sticky_session: false,
created_by: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
// configure mock agent to return an error on apply
let mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| {
Err(
crate::services::agent_client::AgentError::ApplicationFailed(
"internal".to_string(),
"Failed to communicate with the agent.".to_string(),
),
)
});
let mock_agent = Arc::new(mock);
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![target_model.clone()]])
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
.into_connection();
let program_settings = ProgramSettings::mock();
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
config: Arc::new(program_settings),
});
let router = get_upstream_router(state).layer(axum::middleware::from_fn(
crate::middlewares::require_auth::mock::mock_require_auth,
));
let server = TestServer::new(router).expect("failed to create test server");
let payload = CreateUpstreamTargetInfo {
upstream_id: up_id,
host: "127.0.0.1".to_string(),
port: 8080,
weight: None,
is_backup: None,
enabled: None,
};
let res = server
.post(&format!("/upstreams/{}/targets", up_id))
.json(&payload)
.await;
res.assert_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn handler_add_upstream_target_succeeds_returns_created() {
let up_id = uuid::Uuid::new_v4();
@@ -117,8 +209,21 @@ mod tests {
updated_at: chrono::Utc::now(),
};
let up_model = upstream::Model {
id: up_id,
name: "test_upstream".to_string(),
protocol: "http".to_string(),
algorithm: "round_robin".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![target_model.clone()]])
// additional query result for regenerate_and_apply_config -> generate_config
.append_query_results(vec![vec![(up_model.clone(), Some(target_model.clone()))]])
.into_connection();
let router = get_router_with_state(db.clone());

View File

@@ -5,6 +5,7 @@ use axum::{
extract::{Path, State},
response::Result as AxumResult,
};
use sea_orm::TransactionTrait;
use uuid::Uuid;
use crate::{
@@ -19,7 +20,18 @@ pub async fn remove_upstream(
) -> AxumResult<Json<()>, ApiError> {
let upstream_service = &state.service.nginx.get_upstream_service();
upstream_service.delete_upstream(upstream_id, None).await?;
let mut tx = state.database_connection.begin().await?;
upstream_service
.delete_upstream(upstream_id, Some(&mut tx))
.await?;
state
.service
.nginx
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
.await?;
tx.commit().await?;
Ok(Json(()))
}
@@ -32,18 +44,23 @@ mod tests {
use axum_test::TestServer;
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockExecResult};
use database::generated::entities::upstream;
use database::generated::entities::{upstream, upstream_target};
use crate::{
configs::{FromConfig, ProgramSettings},
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
routes::api::restricted::nginx::upstream::get_upstream_router,
services::get_app_service,
services::{agent_client::MockAgentService, get_mock_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 mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| Ok(()));
let mock_agent = Arc::new(mock);
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
@@ -69,6 +86,18 @@ mod tests {
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)
.append_query_results(vec![vec![existing.clone()]])
.append_exec_results(vec![
@@ -81,6 +110,8 @@ mod tests {
last_insert_id: 0,
},
])
// additional query result for regenerate_and_apply_config -> generate_config
.append_query_results(vec![vec![(existing.clone(), Some(target_model.clone()))]])
.into_connection();
let router = get_router_with_state(db.clone());
@@ -91,6 +122,78 @@ mod tests {
res.assert_status_ok();
}
#[tokio::test]
async fn handler_remove_upstream_agent_error_returns_internal() {
let up_id = uuid::Uuid::new_v4();
let existing = upstream::Model {
id: up_id,
name: "todelete".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 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 mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| {
Err(
crate::services::agent_client::AgentError::ApplicationFailed(
"internal".to_string(),
"Failed to communicate with the agent.".to_string(),
),
)
});
let mock_agent = Arc::new(mock);
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![existing.clone()]])
.append_exec_results(vec![
MockExecResult {
rows_affected: 1,
last_insert_id: 0,
},
MockExecResult {
rows_affected: 1,
last_insert_id: 0,
},
])
.append_query_results(vec![vec![(existing.clone(), Some(target_model.clone()))]])
.into_connection();
let program_settings = ProgramSettings::mock();
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
config: Arc::new(program_settings),
});
let router = get_upstream_router(state).layer(axum::middleware::from_fn(
crate::middlewares::require_auth::mock::mock_require_auth,
));
let server = TestServer::new(router).expect("failed to create test server");
let res = server.delete(&format!("/upstreams/{}", up_id)).await;
res.assert_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn handler_remove_upstream_unauthenticated_returns_unauthorized() {
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();

View File

@@ -5,6 +5,7 @@ use axum::{
extract::{Path, State},
response::Result as AxumResult,
};
use sea_orm::TransactionTrait;
use uuid::Uuid;
use crate::{
@@ -19,10 +20,19 @@ pub async fn remove_upstream_target(
) -> AxumResult<Json<()>, ApiError> {
let upstream_service = &state.service.nginx.get_upstream_service();
let mut tx = state.database_connection.begin().await?;
upstream_service
.delete_upstream_target(upstream_target_id, None)
.delete_upstream_target(upstream_target_id, Some(&mut tx))
.await?;
state
.service
.nginx
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
.await?;
tx.commit().await?;
Ok(Json(()))
}
@@ -34,18 +44,23 @@ mod tests {
use axum_test::TestServer;
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase, MockExecResult};
use database::generated::entities::upstream_target;
use database::generated::entities::{upstream, upstream_target};
use crate::{
configs::{FromConfig, ProgramSettings},
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
routes::api::restricted::nginx::upstream::get_upstream_router,
services::get_app_service,
services::{agent_client::MockAgentService, get_mock_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 mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| Ok(()));
let mock_agent = Arc::new(mock);
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
@@ -73,6 +88,17 @@ mod tests {
};
// first find_by_id, then delete (delete typically doesn't return models)
let up_model = upstream::Model {
id: current_model.upstream_id,
name: "test_upstream".to_string(),
protocol: "http".to_string(),
algorithm: "round_robin".to_string(),
sticky_session: false,
created_by: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(first)
@@ -80,6 +106,8 @@ mod tests {
rows_affected: 1,
last_insert_id: 0,
}])
// additional query result for regenerate_and_apply_config -> generate_config
.append_query_results(vec![vec![(up_model.clone(), Some(current_model.clone()))]])
.into_connection();
let router = get_router_with_state(db.clone());
@@ -90,6 +118,73 @@ mod tests {
res.assert_status_ok();
}
#[tokio::test]
async fn handler_remove_upstream_target_agent_error_returns_internal() {
let ut_id = uuid::Uuid::new_v4();
let current_model = upstream_target::Model {
id: ut_id,
upstream_id: uuid::Uuid::new_v4(),
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 up_model = upstream::Model {
id: current_model.upstream_id,
name: "test_upstream".to_string(),
protocol: "http".to_string(),
algorithm: "round_robin".to_string(),
sticky_session: false,
created_by: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| {
Err(
crate::services::agent_client::AgentError::ApplicationFailed(
"internal".to_string(),
"Failed to communicate with the agent.".to_string(),
),
)
});
let mock_agent = Arc::new(mock);
let first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(first)
.append_exec_results(vec![MockExecResult {
rows_affected: 1,
last_insert_id: 0,
}])
.append_query_results(vec![vec![(up_model.clone(), Some(current_model.clone()))]])
.into_connection();
let program_settings = ProgramSettings::mock();
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
config: Arc::new(program_settings),
});
let router = get_upstream_router(state).layer(axum::middleware::from_fn(
crate::middlewares::require_auth::mock::mock_require_auth,
));
let server = TestServer::new(router).expect("failed to create test server");
let res = server.delete(&format!("/upstream_targets/{}", ut_id)).await;
res.assert_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn handler_remove_upstream_target_unauthenticated_returns_unauthorized() {
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();

View File

@@ -5,6 +5,7 @@ use axum::{
extract::{Path, State},
response::Result as AxumResult,
};
use sea_orm::TransactionTrait;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -55,14 +56,22 @@ pub async fn update_upstream(
let upstream_service = &state.service.nginx.get_upstream_service();
let update_info: UpdateUpstreamInfo = payload.into();
let mut tx = state.database_connection.begin().await?;
let r = upstream_service
.update_upstream(upstream_id, update_info, None)
.update_upstream(upstream_id, update_info, Some(&mut tx))
.await?;
state
.service
.nginx
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
.await?;
tx.commit().await?;
Ok(Json(r.into()))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
@@ -71,19 +80,24 @@ mod tests {
use axum_test::TestServer;
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
use database::generated::entities::upstream;
use database::generated::entities::{upstream, upstream_target};
use super::UpdateUpstreamRequestBody;
use crate::{
configs::{FromConfig, ProgramSettings},
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
routes::api::restricted::nginx::upstream::get_upstream_router,
services::get_app_service,
services::{agent_client::MockAgentService, get_mock_app_service},
};
use super::UpdateUpstreamRequestBody;
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 mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| Ok(()));
let mock_agent = Arc::new(mock);
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
@@ -121,11 +135,17 @@ mod tests {
};
// first find_by_id, then update returns updated model
let up_model = current_model.clone();
let first: Vec<Vec<upstream::Model>> = vec![vec![current_model.clone()]];
let second: Vec<Vec<upstream::Model>> = vec![vec![updated_model.clone()]];
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(first)
.append_query_results(second)
// additional query result for regenerate_and_apply_config -> generate_config
.append_query_results(vec![vec![(
up_model.clone(),
Option::<upstream_target::Model>::None,
)]])
.into_connection();
let router = get_router_with_state(db.clone());
@@ -153,6 +173,87 @@ mod tests {
assert_eq!(body.name, "updated_upstream");
}
#[tokio::test]
async fn handler_update_upstream_agent_error_returns_internal() {
let up_id = uuid::Uuid::new_v4();
let current_model = upstream::Model {
id: up_id,
name: "old_upstream".to_string(),
protocol: "http".to_string(),
algorithm: "round_robin".to_string(),
sticky_session: false,
created_by: Some(uuid::Uuid::new_v4()),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let updated_model = upstream::Model {
id: up_id,
name: "updated_upstream".to_string(),
protocol: "http".to_string(),
algorithm: "round_robin".to_string(),
sticky_session: false,
created_by: Some(uuid::Uuid::new_v4()),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let up_model = current_model.clone();
let mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| {
Err(
crate::services::agent_client::AgentError::ApplicationFailed(
"internal".to_string(),
"Failed to communicate with the agent.".to_string(),
),
)
});
let mock_agent = Arc::new(mock);
let first: Vec<Vec<upstream::Model>> = vec![vec![current_model.clone()]];
let second: Vec<Vec<upstream::Model>> = vec![vec![updated_model.clone()]];
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(first)
.append_query_results(second)
.append_query_results(vec![vec![(
up_model.clone(),
Option::<upstream_target::Model>::None,
)]])
.into_connection();
let program_settings = ProgramSettings::mock();
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
config: Arc::new(program_settings),
});
let router = get_upstream_router(state).layer(axum::middleware::from_fn(
crate::middlewares::require_auth::mock::mock_require_auth,
));
let server = TestServer::new(router).expect("failed to create test server");
let payload = UpdateUpstreamRequestBody {
name: Some("updated_upstream".to_string()),
protocol: None,
algorithm: None,
sticky_session: None,
upstream_targets: None,
};
let res = server
.patch(&format!("/upstreams/{}", up_id))
.json(&payload)
.await;
res.assert_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
}
#[tokio::test]
async fn handler_update_upstream_unauthenticated_returns_unauthorized() {
let db = MockDatabase::new(DatabaseBackend::Sqlite).into_connection();

View File

@@ -5,6 +5,7 @@ use axum::{
extract::{Path, State},
response::Result as AxumResult,
};
use sea_orm::TransactionTrait;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -48,10 +49,19 @@ pub async fn update_upstream_target(
let upstream_service = &state.service.nginx.get_upstream_service();
let update_info: UpdateUpstreamTargetInfo = payload.into();
let mut tx = state.database_connection.begin().await?;
let r = upstream_service
.update_upstream_target(upstream_target_id, update_info, None)
.update_upstream_target(upstream_target_id, update_info, Some(&mut tx))
.await?;
state
.service
.nginx
.regenerate_and_apply_config(state.service.agent_client.clone(), Some(&mut tx))
.await?;
tx.commit().await?;
Ok(Json(r.into()))
}
@@ -64,18 +74,23 @@ mod tests {
use axum_test::TestServer;
use sea_orm::{DatabaseBackend, DatabaseConnection, MockDatabase};
use database::generated::entities::upstream_target;
use database::generated::entities::{upstream, upstream_target};
use super::UpdateUpstreamTargetRequestBody;
use crate::{
configs::{FromConfig, ProgramSettings},
middlewares::require_auth::mock::REQUEST_AUTH_USER_INVALID_HEADER,
services::get_app_service,
services::{agent_client::MockAgentService, get_mock_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 mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| Ok(()));
let mock_agent = Arc::new(mock);
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
@@ -121,12 +136,27 @@ mod tests {
updated_at: chrono::Utc::now(),
};
let up_model = upstream::Model {
id: current_model.upstream_id,
name: "test_upstream".to_string(),
protocol: "http".to_string(),
algorithm: "round_robin".to_string(),
sticky_session: false,
created_by: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
let second: Vec<Vec<upstream_target::Model>> = vec![vec![updated_model.clone()]];
// additional query result for regenerate_and_apply_config -> generate_config
let third: Vec<Vec<(upstream::Model, Option<upstream_target::Model>)>> =
vec![vec![(up_model.clone(), Some(updated_model.clone()))]];
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(first)
.append_query_results(second)
.append_query_results(third)
.into_connection();
let router = get_router_with_state(db.clone());
@@ -203,4 +233,103 @@ mod tests {
res.assert_status(StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn handler_update_upstream_target_agent_error_returns_internal() {
let target_id = uuid::Uuid::new_v4();
let current_model = upstream_target::Model {
id: target_id,
upstream_id: uuid::Uuid::new_v4(),
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 updated_model = upstream_target::Model {
id: target_id,
upstream_id: current_model.upstream_id,
target_host: "127.0.0.1".to_string(),
target_port: 8081,
weight: 2,
is_backup: false,
enabled: false,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let up_model = upstream::Model {
id: current_model.upstream_id,
name: "test_upstream".to_string(),
protocol: "http".to_string(),
algorithm: "round_robin".to_string(),
sticky_session: false,
created_by: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let mut mock = MockAgentService::new();
mock.expect_validate().returning(|_cfg| Ok(()));
mock.expect_apply().returning(|_cfg| {
Err(
crate::services::agent_client::AgentError::ApplicationFailed(
"internal".to_string(),
"Failed to communicate with the agent.".to_string(),
),
)
});
let mock_agent = Arc::new(mock);
let first: Vec<Vec<upstream_target::Model>> = vec![vec![current_model.clone()]];
let second: Vec<Vec<upstream_target::Model>> = vec![vec![updated_model.clone()]];
let third: Vec<Vec<(upstream::Model, Option<upstream_target::Model>)>> =
vec![vec![(up_model.clone(), Some(updated_model.clone()))]];
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(first)
.append_query_results(second)
.append_query_results(third)
.into_connection();
let program_settings = ProgramSettings::mock();
let app_service =
get_mock_app_service(&Arc::new(db.clone()), &program_settings, mock_agent);
let state = Arc::new(crate::routes::AppState {
database_connection: Arc::new(db),
service: Arc::new(app_service),
config: Arc::new(program_settings),
});
let router = axum::Router::new()
.route(
"/upstream_targets/{upstream_target_id}",
axum::routing::patch(crate::routes::api::restricted::nginx::upstream::update_upstream_target::update_upstream_target),
)
.with_state(state)
.layer(axum::middleware::from_fn(
crate::middlewares::require_auth::mock::mock_require_auth,
));
let server = TestServer::new(router).expect("failed to create test server");
let payload = UpdateUpstreamTargetRequestBody {
host: None,
port: Some(8081),
enabled: Some(false),
is_backup: None,
weight: Some(2),
};
let res = server
.patch(&format!("/upstream_targets/{}", target_id))
.json(&payload)
.await;
res.assert_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR);
}
}