feat: Implement ProxyHost and Location services with CRUD operations

- Added `ProxyHostInfo`, `ProxyHostCreateInfo`, and `UpdateProxyHostInfo` structs to manage proxy host data.
- Created `ProxyService` and `ProxyServiceImpl` for handling proxy host operations including create, read, update, and delete.
- Implemented `LocationService` and `LocationServiceImpl` for managing locations associated with proxy hosts.
- Introduced database transaction handling for creating proxies and locations.
- Added tests for all service methods to ensure functionality and correctness.
This commit is contained in:
GW_MC
2026-01-07 15:57:44 +08:00
parent ab840126b3
commit 1c0053207c
7 changed files with 1684 additions and 6 deletions

View File

@@ -2,6 +2,8 @@ pub mod builder;
pub mod info;
pub mod traits;
pub mod location;
pub mod proxy_host;
pub mod upstream;
use std::sync::Arc;
@@ -14,6 +16,8 @@ use crate::{
agent_client::AgentService,
nginx::{
builder::{NginxConfigBuilder, NginxConfigProvider},
location::{LocationService, LocationServiceImpl},
proxy_host::{ProxyService, ProxyServiceImpl},
upstream::{UpstreamService, UpstreamServiceImpl},
},
},
@@ -23,7 +27,12 @@ pub struct NginxService {
#[allow(dead_code)]
connection: Arc<DatabaseConnection>,
//
#[allow(dead_code)]
upstream_service: Arc<dyn UpstreamService>,
#[allow(dead_code)]
proxy_service: Arc<dyn ProxyService>,
#[allow(dead_code)]
location_service: Arc<dyn LocationService>,
}
impl NginxService {
@@ -32,6 +41,8 @@ impl NginxService {
connection: connection.clone(),
//
upstream_service: Arc::new(UpstreamServiceImpl::new(connection.clone())),
proxy_service: Arc::new(ProxyServiceImpl::new(connection.clone())),
location_service: Arc::new(LocationServiceImpl::new(connection.clone())),
}
}
@@ -39,6 +50,14 @@ impl NginxService {
self.upstream_service.clone()
}
pub fn get_proxy_service(&self) -> Arc<dyn ProxyService> {
self.proxy_service.clone()
}
pub fn get_location_service(&self) -> Arc<dyn LocationService> {
self.location_service.clone()
}
#[allow(dead_code)]
pub async fn validate_config(
&self,
@@ -69,7 +88,7 @@ impl NginxService {
.generate_config(&mut builder, tx)
.await?;
Ok(builder.to_nginx_config(None))
builder.to_nginx_config(None)
}
pub async fn regenerate_and_apply_config(

View File

@@ -1,9 +1,9 @@
use crate::services::nginx::info::upstream::UpstreamInfo;
use crate::{errors::service_error::ServiceError, services::nginx::info::upstream::UpstreamInfo};
pub const INDENT_SIZE: usize = 2;
pub trait NginxConfigProvider {
fn to_nginx_config(&self, indent: Option<usize>) -> String;
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError>;
}
#[derive(Default)]
@@ -24,7 +24,7 @@ impl NginxConfigBuilder {
}
impl NginxConfigProvider for NginxConfigBuilder {
fn to_nginx_config(&self, indent: Option<usize>) -> String {
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
let mut config = format!(
"# Nginx Config Generated by YANPM at {}",
chrono::Utc::now()
@@ -32,12 +32,12 @@ impl NginxConfigProvider for NginxConfigBuilder {
for upstream in &self.upstreams {
config.push('\n');
config.push_str(&upstream.to_nginx_config(indent));
config.push_str(&upstream.to_nginx_config(indent)?);
}
// TODO: Add other sections like servers, locations, etc.
// trailing newline for file ending
config.push('\n');
config
Ok(config)
}
}

View File

@@ -1,2 +1,4 @@
pub mod location;
pub mod proxy_host;
pub mod upstream;
pub mod upstream_target;

View File

@@ -0,0 +1,296 @@
use chrono::{DateTime, Utc};
use database::generated::entities::{location, proxy_host, upstream};
use sea_orm::ActiveValue::{Set, Unchanged};
use tracing::warn;
use uuid::Uuid;
use crate::{
errors::service_error::ServiceError,
services::nginx::{builder::NginxConfigProvider, traits::indentable::Indentable},
set_if_some,
};
use serde_json::Value as JsonValue;
#[derive(Clone)]
pub struct ProxyPassInfo {
pub protocol: String,
pub host: String,
pub port: i64,
}
#[derive(Clone)]
pub struct LocationInfo {
pub id: Uuid,
pub host_id: Uuid,
pub path: String,
pub match_type: String,
pub order: i64,
pub upstream_id: Option<Uuid>,
pub proxy_pass_info: Option<ProxyPassInfo>,
pub preserve_host_header: Option<bool>,
pub allowed_methods: Option<Vec<String>>,
pub custom_config: Option<String>,
pub enabled: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
//
pub upstream: Option<super::upstream::UpstreamInfo>,
pub proxy_host: Option<super::proxy_host::ProxyHostInfo>,
}
pub struct CreateLocationInfo {
pub host_id: Uuid,
pub path: String,
pub match_type: String,
pub order: i64,
pub upstream_id: Option<Uuid>,
pub proxy_pass_protocol: Option<String>,
pub proxy_pass_host: Option<String>,
pub proxy_pass_port: Option<i64>,
pub preserve_host_header: Option<bool>,
pub allowed_methods: Option<Vec<String>>,
pub custom_config: Option<String>,
pub enabled: bool,
}
#[derive(Clone)]
pub struct UpdateLocationInfo {
pub path: Option<String>,
pub match_type: Option<String>,
pub order: Option<i64>,
pub upstream_id: Option<Option<Uuid>>,
pub proxy_pass_protocol: Option<Option<String>>,
pub proxy_pass_host: Option<Option<String>>,
pub proxy_pass_port: Option<Option<i64>>,
pub preserve_host_header: Option<Option<bool>>,
pub allowed_methods: Option<Option<Vec<String>>>,
pub custom_config: Option<Option<String>>,
pub enabled: Option<bool>,
}
impl From<location::Model> for LocationInfo {
fn from(model: location::Model) -> Self {
let allowed_methods: Option<Vec<String>> = match model.allowed_methods {
Some(JsonValue::Array(arr)) => {
let v: Vec<String> = arr
.into_iter()
.filter_map(|val| val.as_str().map(|s| s.to_string()))
.collect();
if v.is_empty() { None } else { Some(v) }
}
_ => None,
};
Self {
id: model.id,
host_id: model.host_id,
path: model.path,
match_type: model.match_type,
order: model.order,
upstream_id: model.upstream_id,
proxy_pass_info: match (
model.proxy_pass_protocol,
model.proxy_pass_host,
model.proxy_pass_port,
) {
(Some(protocol), Some(host), Some(port)) => Some(ProxyPassInfo {
protocol,
host,
port,
}),
(Some(_), _, _) | (_, Some(_), _) | (_, _, Some(_)) => {
warn!("Incomplete proxy_pass_info for location {}", model.id);
None
}
_ => None,
},
preserve_host_header: model.preserve_host_header,
allowed_methods,
custom_config: model.custom_config,
enabled: model.enabled,
created_at: model.created_at,
updated_at: model.updated_at,
upstream: None,
proxy_host: None,
}
}
}
impl From<(location::Model, Option<proxy_host::Model>)> for LocationInfo {
fn from(data: (location::Model, Option<proxy_host::Model>)) -> Self {
let (location_model, proxy_host_model_opt) = data;
(location_model, proxy_host_model_opt, None).into()
}
}
impl
From<(
location::Model,
Option<proxy_host::Model>,
Option<upstream::Model>,
)> for LocationInfo
{
fn from(
data: (
location::Model,
Option<proxy_host::Model>,
Option<upstream::Model>,
),
) -> Self {
let (location_model, proxy_host_model_opt, upstream_model_opt) = data;
let mut location_info = LocationInfo::from(location_model);
if let Some(upstream_model) = upstream_model_opt {
location_info.upstream = Some(super::upstream::UpstreamInfo::from(upstream_model));
}
if let Some(proxy_host_model) = proxy_host_model_opt {
location_info.proxy_host =
Some(super::proxy_host::ProxyHostInfo::from(proxy_host_model));
}
location_info
}
}
impl From<CreateLocationInfo> for location::ActiveModel {
fn from(val: CreateLocationInfo) -> Self {
location::ActiveModel {
id: Set(Uuid::new_v4()),
host_id: Set(val.host_id),
path: Set(val.path),
match_type: Set(val.match_type),
order: Set(val.order),
upstream_id: Set(val.upstream_id),
proxy_pass_protocol: Set(val.proxy_pass_protocol),
proxy_pass_host: Set(val.proxy_pass_host),
proxy_pass_port: Set(val.proxy_pass_port),
preserve_host_header: Set(val.preserve_host_header),
allowed_methods: Set(val
.allowed_methods
.map(|v| JsonValue::Array(v.into_iter().map(JsonValue::String).collect()))),
custom_config: Set(val.custom_config),
enabled: Set(val.enabled),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
}
}
}
impl UpdateLocationInfo {
pub fn apply_to_model(self, current_model: location::Model) -> location::ActiveModel {
location::ActiveModel {
id: Unchanged(current_model.id),
host_id: Unchanged(current_model.host_id),
path: set_if_some!(self.path),
match_type: set_if_some!(self.match_type),
order: set_if_some!(self.order),
upstream_id: match self.upstream_id {
Some(inner) => Set(inner),
None => Unchanged(current_model.upstream_id),
},
proxy_pass_protocol: match self.proxy_pass_protocol {
Some(inner) => Set(inner),
None => Unchanged(current_model.proxy_pass_protocol),
},
proxy_pass_host: match self.proxy_pass_host {
Some(inner) => Set(inner),
None => Unchanged(current_model.proxy_pass_host),
},
proxy_pass_port: match self.proxy_pass_port {
Some(inner) => Set(inner),
None => Unchanged(current_model.proxy_pass_port),
},
preserve_host_header: match self.preserve_host_header {
Some(inner) => Set(inner),
None => Unchanged(current_model.preserve_host_header),
},
allowed_methods: match self.allowed_methods {
Some(inner) => {
let json_opt = inner
.map(|v| JsonValue::Array(v.into_iter().map(JsonValue::String).collect()));
Set(json_opt)
}
None => Unchanged(current_model.allowed_methods),
},
custom_config: match self.custom_config {
Some(inner) => Set(inner),
None => Unchanged(current_model.custom_config),
},
enabled: set_if_some!(self.enabled),
created_at: Unchanged(current_model.created_at),
updated_at: Set(chrono::Utc::now()),
}
}
}
impl NginxConfigProvider for LocationInfo {
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
let indent = indent.unwrap_or(0);
let selector = match self.match_type.as_str() {
"exact" => format!("location = {} ", self.path),
"regex" => format!("location ~ {} ", self.path),
_ => format!("location {} ", self.path),
};
let mut body_lines: Vec<String> = Vec::new();
if let Some(methods) = &self.allowed_methods
&& !methods.is_empty()
{
body_lines.push(format!(
"limit_except {} {{ deny all; }}",
methods.join(" ")
));
}
if let Some(upstream) = &self.upstream {
body_lines.push(format!(
"proxy_pass {}://{};",
upstream.protocol, upstream.name
));
} else if let Some(host) = &self.proxy_pass_info {
body_lines.push(format!(
"proxy_pass {}://{}:{};",
host.protocol, host.host, host.port
));
} else {
warn!(
"Location {} has neither upstream nor proxy_pass_host defined",
self.id
);
return Err(ServiceError::InternalError(
"Location must have either an upstream or a proxy_pass_host defined".to_string(),
));
}
if let Some(preserve) = self.preserve_host_header {
if preserve {
body_lines.push("proxy_set_header Host $host;".to_string());
} else {
body_lines.push("proxy_set_header Host $proxy_host;".to_string());
}
}
if let Some(cfg) = &self.custom_config
&& !cfg.trim().is_empty()
{
body_lines.push(cfg.clone());
}
let inner = if body_lines.is_empty() {
"# location has no config".to_string()
} else {
body_lines
.into_iter()
.map(|l| l.indent(indent + 2))
.collect::<Vec<String>>()
.join("\n")
};
Ok(format!("{}{{\n{}\n}}", selector.trim_end(), inner).indent(indent))
}
}

View File

@@ -0,0 +1,251 @@
use chrono::{DateTime, Utc};
use database::generated::entities::{location, proxy_host};
use sea_orm::ActiveValue::{Set, Unchanged};
use serde_json::Value as JsonValue;
use uuid::Uuid;
use crate::{
errors::service_error::ServiceError,
services::nginx::{
builder::{INDENT_SIZE, NginxConfigProvider},
traits::indentable::Indentable,
},
set_if_some,
};
#[derive(Clone)]
pub struct ProxyHostInfo {
pub id: Uuid,
pub name: Option<String>,
pub domain: String,
pub scheme: String,
pub listen_port: i64,
pub forward_scheme: String,
pub forward_host: Option<String>,
pub forward_port: Option<i64>,
pub preserve_host_header: bool,
pub enable_websocket: bool,
pub meta: Option<JsonValue>,
pub enabled: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
//
pub upstream: Option<super::upstream::UpstreamInfo>,
pub locations: Vec<super::location::LocationInfo>,
}
pub struct ProxyHostCreateInfo {
pub name: Option<String>,
pub domain: String,
pub scheme: String,
pub listen_port: i64,
pub forward_scheme: String,
pub forward_host: Option<String>,
pub forward_port: Option<i64>,
pub preserve_host_header: bool,
pub enable_websocket: bool,
pub enabled: bool,
pub meta: Option<JsonValue>,
pub default_upstream_id: Option<Uuid>,
pub created_by: Option<Uuid>,
//
pub locations: Vec<super::location::CreateLocationInfo>,
}
#[derive(Clone)]
pub struct UpdateProxyHostInfo {
pub name: Option<Option<String>>,
pub domain: Option<String>,
pub scheme: Option<String>,
pub listen_port: Option<i64>,
pub forward_scheme: Option<String>,
pub forward_host: Option<Option<String>>,
pub forward_port: Option<Option<i64>>,
pub preserve_host_header: Option<bool>,
pub enable_websocket: Option<bool>,
pub enabled: Option<bool>,
pub meta: Option<Option<JsonValue>>,
pub default_upstream_id: Option<Option<Uuid>>,
}
impl From<proxy_host::Model> for ProxyHostInfo {
fn from(model: proxy_host::Model) -> Self {
Self {
id: model.id,
name: model.name,
domain: model.domain,
scheme: model.scheme,
listen_port: model.listen_port,
forward_scheme: model.forward_scheme,
forward_host: model.forward_host,
forward_port: model.forward_port,
preserve_host_header: model.preserve_host_header,
enable_websocket: model.enable_websocket,
meta: model.meta,
enabled: model.enabled,
created_at: model.created_at,
updated_at: model.updated_at,
upstream: None,
locations: Vec::new(),
}
}
}
impl From<(proxy_host::Model, Vec<location::Model>)> for ProxyHostInfo {
fn from(data: (proxy_host::Model, Vec<location::Model>)) -> Self {
let (proxy_model, location_models) = data;
let mut proxy_info = ProxyHostInfo::from(proxy_model);
let locations_info: Vec<super::location::LocationInfo> =
location_models.into_iter().map(|m| m.into()).collect();
proxy_info.locations = locations_info;
proxy_info
}
}
impl From<ProxyHostCreateInfo> for (proxy_host::ActiveModel, Vec<location::ActiveModel>) {
fn from(val: ProxyHostCreateInfo) -> Self {
let proxy_host = proxy_host::ActiveModel {
id: Set(Uuid::new_v4()),
name: Set(val.name),
domain: Set(val.domain),
scheme: Set(val.scheme),
listen_port: Set(val.listen_port),
forward_scheme: Set(val.forward_scheme),
forward_host: Set(val.forward_host),
forward_port: Set(val.forward_port),
preserve_host_header: Set(val.preserve_host_header),
enable_websocket: Set(val.enable_websocket),
enabled: Set(val.enabled),
meta: Set(val.meta),
default_upstream_id: Set(val.default_upstream_id),
created_by: Set(val.created_by),
created_at: Set(chrono::Utc::now()),
updated_at: Set(chrono::Utc::now()),
};
let location_models = val.locations.into_iter().map(|loc| loc.into()).collect();
(proxy_host, location_models)
}
}
impl From<ProxyHostInfo> for proxy_host::ActiveModel {
fn from(val: ProxyHostInfo) -> Self {
proxy_host::ActiveModel {
id: Set(val.id),
name: Set(val.name),
domain: Set(val.domain),
scheme: Set(val.scheme),
listen_port: Set(val.listen_port),
forward_scheme: Set(val.forward_scheme),
forward_host: Set(val.forward_host),
forward_port: Set(val.forward_port),
preserve_host_header: Set(val.preserve_host_header),
enable_websocket: Set(val.enable_websocket),
enabled: Set(val.enabled),
meta: Set(val.meta),
default_upstream_id: Set(val.upstream.as_ref().map(|u| u.id)),
created_by: Set(None),
created_at: Set(val.created_at),
updated_at: Set(val.updated_at),
}
}
}
impl UpdateProxyHostInfo {
pub fn apply_to_model(self, current_model: proxy_host::Model) -> proxy_host::ActiveModel {
proxy_host::ActiveModel {
id: Unchanged(current_model.id),
name: match self.name {
Some(inner) => Set(inner),
None => Unchanged(current_model.name),
},
domain: set_if_some!(self.domain),
scheme: set_if_some!(self.scheme),
listen_port: set_if_some!(self.listen_port),
forward_scheme: set_if_some!(self.forward_scheme),
forward_host: match self.forward_host {
Some(inner) => Set(inner),
None => Unchanged(current_model.forward_host),
},
forward_port: match self.forward_port {
Some(inner) => Set(inner),
None => Unchanged(current_model.forward_port),
},
preserve_host_header: set_if_some!(self.preserve_host_header),
enable_websocket: set_if_some!(self.enable_websocket),
enabled: set_if_some!(self.enabled),
meta: set_if_some!(self.meta),
default_upstream_id: match self.default_upstream_id {
Some(inner) => Set(inner),
None => Unchanged(current_model.default_upstream_id),
},
created_by: Unchanged(current_model.created_by),
created_at: Unchanged(current_model.created_at),
updated_at: Set(chrono::Utc::now()),
}
}
}
impl NginxConfigProvider for ProxyHostInfo {
fn to_nginx_config(&self, indent: Option<usize>) -> Result<String, ServiceError> {
let indent = indent.unwrap_or(0);
let mut body: Vec<String> = Vec::new();
// default location or fallback
let default_pass = if let Some(up) = &self.upstream {
format!("proxy_pass http://{};", up.name)
} else if let Some(host) = &self.forward_host {
if let Some(port) = self.forward_port {
format!("proxy_pass http://{}:{};", host, port)
} else {
format!("proxy_pass http://{};", host)
}
} else {
String::new()
};
// get locations's index sorted by order to prevent mutable borrow issues
let mut index_list: Vec<usize> = (0..self.locations.len()).collect();
index_list.sort_by(|&a, &b| {
let order_a = self.locations[a].order;
let order_b = self.locations[b].order;
order_a.cmp(&order_b)
});
for &index in &index_list {
let loc = &self.locations[index];
body.push(loc.to_nginx_config(Some(indent + INDENT_SIZE))?);
}
// If there is a default proxy_pass and no root location for `/`, add it
if !default_pass.is_empty() {
body.insert(
0,
format!(
"location / {{\n{}\n}}",
default_pass.indent(indent + INDENT_SIZE)
),
);
}
if self.enable_websocket {
body.push("proxy_set_header Upgrade $http_upgrade;".to_string());
body.push("proxy_set_header Connection \"upgrade\";".to_string());
}
let inner = if body.is_empty() {
"# server has no config".to_string()
} else {
body.into_iter()
.map(|l| l.indent(indent + INDENT_SIZE))
.collect::<Vec<String>>()
.join("\n")
};
Ok(format!(
"server {{\n listen {};\n server_name {};\n{}\n}}",
self.listen_port, self.domain, inner
)
.indent(indent))
}
}

View File

@@ -0,0 +1,512 @@
use std::sync::Arc;
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction, EntityTrait,
ModelTrait, QueryFilter, QuerySelect, QueryTrait,
};
use database::generated::entities::{location, proxy_host, upstream};
use crate::{
errors::service_error::ServiceError,
helpers::database::PaginationFilter,
services::nginx::info::location::{CreateLocationInfo, LocationInfo, UpdateLocationInfo},
with_conn,
};
#[async_trait::async_trait]
pub trait LocationService: Send + Sync {
async fn create_location(
&self,
create_info: CreateLocationInfo,
tx: Option<&mut DatabaseTransaction>,
) -> Result<LocationInfo, ServiceError>;
#[allow(dead_code)]
async fn get_locations(
&self,
pagination: Option<PaginationFilter>,
options: Option<GetLocationOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<Vec<LocationInfo>, ServiceError>;
async fn get_location(
&self,
location_id: uuid::Uuid,
options: Option<GetLocationOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<LocationInfo, ServiceError>;
async fn update_location(
&self,
location_id: uuid::Uuid,
update: UpdateLocationInfo,
tx: Option<&mut DatabaseTransaction>,
) -> Result<LocationInfo, ServiceError>;
async fn delete_location(
&self,
location_id: uuid::Uuid,
tx: Option<&mut DatabaseTransaction>,
) -> Result<(), ServiceError>;
}
pub struct LocationServiceImpl {
connection: Arc<DatabaseConnection>,
}
#[allow(dead_code)]
pub struct LocationTotalCountOptions {}
#[derive(Default)]
pub struct GetLocationOptions {
pub include_upstream: bool,
#[allow(dead_code)]
pub filter_by_enabled: bool,
}
impl LocationServiceImpl {
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
Self { connection }
}
}
#[async_trait::async_trait]
impl LocationService for LocationServiceImpl {
async fn create_location(
&self,
create_info: CreateLocationInfo,
tx: Option<&mut DatabaseTransaction>,
) -> Result<LocationInfo, ServiceError> {
let model: location::ActiveModel = create_info.into();
let r = with_conn!(&*self.connection, tx, conn, { model.insert(*conn).await? });
Ok(r.into())
}
async fn get_locations(
&self,
pagination: Option<PaginationFilter>,
options: Option<GetLocationOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<Vec<LocationInfo>, ServiceError> {
let r = with_conn!(&*self.connection, tx, conn, {
let mut find_query = location::Entity::find();
if let Some(pagination) = pagination {
let (offset, limit) = pagination.get_offset_limit();
find_query = find_query.offset(offset).limit(limit);
}
let find_query = find_query
.apply_if(
options
.as_ref()
.is_some_and(|v| v.filter_by_enabled)
.then_some(true),
|q, _v| q.filter(location::Column::Enabled.eq(true)),
)
.find_also_related(proxy_host::Entity);
let r: Vec<LocationInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
find_query
.find_also_related(upstream::Entity)
.all(*conn)
.await?
.into_iter()
.map(|v| v.into())
.collect()
} else {
find_query
.all(*conn)
.await?
.into_iter()
.map(|m| m.into())
.collect()
};
r
});
Ok(r)
}
async fn get_location(
&self,
location_id: uuid::Uuid,
options: Option<GetLocationOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<LocationInfo, ServiceError> {
let r = with_conn!(&*self.connection, tx, conn, {
let find_query =
location::Entity::find_by_id(location_id).find_also_related(proxy_host::Entity);
let r: Option<LocationInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
find_query
.find_also_related(upstream::Entity)
.one(*conn)
.await?
.map(|v| v.into())
} else {
find_query.one(*conn).await?.map(|m| m.into())
};
r
});
Ok(r.ok_or(ServiceError::NotFound(format!(
"Location with id {} not found",
location_id
)))?)
}
async fn update_location(
&self,
location_id: uuid::Uuid,
update: UpdateLocationInfo,
tx: Option<&mut DatabaseTransaction>,
) -> Result<LocationInfo, ServiceError> {
let current_model = with_conn!(&*self.connection, tx, conn, {
location::Entity::find_by_id(location_id)
.one(*conn)
.await?
.ok_or(ServiceError::NotFound(format!(
"Location with id {} not found",
location_id
)))?
});
let active_model = update.apply_to_model(current_model);
let r = with_conn!(&*self.connection, tx, conn, {
active_model.update(*conn).await?
});
Ok(r.into())
}
async fn delete_location(
&self,
location_id: uuid::Uuid,
tx: Option<&mut DatabaseTransaction>,
) -> Result<(), ServiceError> {
let model = with_conn!(&*self.connection, tx, conn, {
location::Entity::find_by_id(location_id)
.one(*conn)
.await?
.ok_or(ServiceError::NotFound(format!(
"Location with id {} not found",
location_id
)))?
});
with_conn!(&*self.connection, tx, conn, {
model.delete(*conn).await?;
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use sea_orm::MockExecResult;
use sea_orm::{DatabaseBackend, MockDatabase};
use database::generated::entities::{location, proxy_host};
#[tokio::test]
async fn create_location_returns_info() {
let host_id = uuid::Uuid::new_v4();
let created = location::Model {
id: uuid::Uuid::new_v4(),
host_id,
path: "/test".to_string(),
match_type: "prefix".to_string(),
order: 0,
upstream_id: None,
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
enabled: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![created.clone()]])
.into_connection();
let svc = LocationServiceImpl::new(Arc::new(db));
let create_info = CreateLocationInfo {
host_id,
path: "/test".to_string(),
match_type: "prefix".to_string(),
order: 0,
upstream_id: None,
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
enabled: true,
};
let res = svc.create_location(create_info, None).await;
assert!(res.is_ok());
let info = res.expect("Failed to create location");
assert_eq!(info.path, "/test");
}
#[tokio::test]
async fn get_locations_returns_list() {
let host_id = uuid::Uuid::new_v4();
let l1 = location::Model {
id: uuid::Uuid::new_v4(),
host_id,
path: "/a".to_string(),
match_type: "prefix".to_string(),
order: 0,
upstream_id: None,
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
enabled: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let l2 = location::Model {
id: uuid::Uuid::new_v4(),
host_id,
path: "/b".to_string(),
match_type: "prefix".to_string(),
order: 1,
upstream_id: None,
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
enabled: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![
(l1.clone(), None::<proxy_host::Model>),
(l2.clone(), None::<proxy_host::Model>),
]])
.into_connection();
let svc = LocationServiceImpl::new(Arc::new(db));
let res = svc.get_locations(None, None, None).await;
assert!(res.is_ok());
let list = res.expect("Failed to get locations");
assert_eq!(list.len(), 2);
}
#[tokio::test]
async fn get_location_with_upstream_returns_upstream() {
let host_id = uuid::Uuid::new_v4();
let up_id = uuid::Uuid::new_v4();
let loc = location::Model {
id: uuid::Uuid::new_v4(),
host_id,
path: "/up".to_string(),
match_type: "prefix".to_string(),
order: 0,
upstream_id: Some(up_id),
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
enabled: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![(loc.clone(), None::<proxy_host::Model>)]])
.into_connection();
let svc = LocationServiceImpl::new(Arc::new(db));
let res = svc
.get_location(
loc.id,
Some(GetLocationOptions {
include_upstream: false,
filter_by_enabled: false,
}),
None,
)
.await;
assert!(res.is_ok());
let info = res.expect("Failed to get location");
assert_eq!(info.id, loc.id);
assert_eq!(info.upstream_id, Some(up_id));
}
#[tokio::test]
async fn get_location_not_found_returns_not_found() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
.into_connection();
let svc = LocationServiceImpl::new(Arc::new(db));
let res = svc.get_location(uuid::Uuid::new_v4(), None, None).await;
assert!(matches!(res, Err(ServiceError::NotFound(_))));
}
#[tokio::test]
async fn update_location_success() {
let id = uuid::Uuid::new_v4();
let host_id = uuid::Uuid::new_v4();
let existing = location::Model {
id,
host_id,
path: "/old".to_string(),
match_type: "prefix".to_string(),
order: 0,
upstream_id: None,
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
enabled: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let updated = location::Model {
id,
host_id,
path: "/new".to_string(),
match_type: "prefix".to_string(),
order: 0,
upstream_id: None,
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
enabled: true,
created_at: existing.created_at,
updated_at: chrono::Utc::now(),
};
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![existing.clone()]]) // find_by_id
.append_query_results(vec![vec![updated.clone()]]) // update result
.into_connection();
let svc = LocationServiceImpl::new(Arc::new(db));
let update_info = UpdateLocationInfo {
path: Some("/new".to_string()),
match_type: None,
order: None,
upstream_id: None,
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
enabled: None,
};
let res = svc.update_location(id, update_info, None).await;
assert!(res.is_ok());
let got = res.expect("Failed to update location");
assert_eq!(got.path, "/new");
}
#[tokio::test]
async fn update_location_not_found() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
.into_connection();
let svc = LocationServiceImpl::new(Arc::new(db));
let res = svc
.update_location(
uuid::Uuid::new_v4(),
UpdateLocationInfo {
path: None,
match_type: None,
order: None,
upstream_id: None,
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
enabled: None,
},
None,
)
.await;
assert!(matches!(res, Err(ServiceError::NotFound(_))));
}
#[tokio::test]
async fn delete_location_success() {
let id = uuid::Uuid::new_v4();
let existing = location::Model {
id,
host_id: uuid::Uuid::new_v4(),
path: "/del".to_string(),
match_type: "prefix".to_string(),
order: 0,
upstream_id: None,
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
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![MockExecResult {
rows_affected: 1,
last_insert_id: 0,
}])
.into_connection();
let svc = LocationServiceImpl::new(Arc::new(db));
let res = svc.delete_location(id, None).await;
assert!(res.is_ok());
}
#[tokio::test]
async fn delete_location_not_found() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
.into_connection();
let svc = LocationServiceImpl::new(Arc::new(db));
let res = svc.delete_location(uuid::Uuid::new_v4(), None).await;
assert!(matches!(res, Err(ServiceError::NotFound(_))));
}
}

View File

@@ -0,0 +1,598 @@
use std::sync::Arc;
use sea_orm::{
ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, DatabaseTransaction,
EntityTrait, FromQueryResult, JoinType, ModelTrait, QueryFilter, QuerySelect, QueryTrait,
RelationTrait, TransactionTrait,
};
use database::generated::entities::{location, proxy_host};
use crate::{
errors::service_error::ServiceError,
helpers::database::PaginationFilter,
services::nginx::info::proxy_host::{ProxyHostCreateInfo, ProxyHostInfo, UpdateProxyHostInfo},
with_conn,
};
#[async_trait::async_trait]
pub trait ProxyService: Send + Sync {
async fn create_proxy(
&self,
create_info: ProxyHostCreateInfo,
tx: Option<&mut DatabaseTransaction>,
) -> Result<ProxyHostInfo, ServiceError>;
async fn get_total_proxies(
&self,
options: Option<ProxyTotalCountOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<u64, ServiceError>;
async fn get_proxies(
&self,
pagination: Option<PaginationFilter>,
options: Option<ProxyHostListOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<Vec<ProxyHostInfo>, ServiceError>;
async fn get_proxy(
&self,
proxy_id: uuid::Uuid,
options: Option<ProxyHostGetOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<ProxyHostInfo, ServiceError>;
async fn update_proxy(
&self,
proxy_id: uuid::Uuid,
update: UpdateProxyHostInfo,
tx: Option<&mut DatabaseTransaction>,
) -> Result<ProxyHostInfo, ServiceError>;
async fn delete_proxy(
&self,
proxy_id: uuid::Uuid,
tx: Option<&mut DatabaseTransaction>,
) -> Result<(), ServiceError>;
}
pub struct ProxyServiceImpl {
connection: Arc<DatabaseConnection>,
}
#[allow(dead_code)]
pub struct ProxyTotalCountOptions {}
#[derive(Default)]
pub struct ProxyHostGetOptions {
pub include_upstream: bool,
pub filter_by_enabled: bool,
}
pub type ProxyHostListOptions = ProxyHostGetOptions;
impl ProxyServiceImpl {
pub fn new(connection: Arc<DatabaseConnection>) -> Self {
Self { connection }
}
}
#[async_trait::async_trait]
impl ProxyService for ProxyServiceImpl {
async fn create_proxy(
&self,
create_info: ProxyHostCreateInfo,
tx: Option<&mut DatabaseTransaction>,
) -> Result<ProxyHostInfo, ServiceError> {
let (proxy_host, location_models): (proxy_host::ActiveModel, Vec<location::ActiveModel>) =
create_info.into();
let mut maybe_owned_tx: Option<DatabaseTransaction> = None;
let tx_ref: Option<&mut DatabaseTransaction> = if let Some(tx) = tx {
Some(tx)
} else {
maybe_owned_tx = Some(self.connection.begin().await?);
maybe_owned_tx.as_mut()
};
let r = with_conn!(&*self.connection, tx_ref, conn, {
let inserted_proxy = proxy_host.insert(*conn).await?;
let mut inserted_location_models: Vec<location::Model> =
Vec::with_capacity(location_models.len());
for mut loc_model in location_models {
loc_model.host_id = Set(inserted_proxy.id);
let r = loc_model.insert(*conn).await?;
inserted_location_models.push(r);
}
(inserted_proxy, inserted_location_models)
});
if let Some(t) = maybe_owned_tx.take() {
t.commit().await?;
}
Ok(r.into())
}
async fn get_total_proxies(
&self,
_options: Option<ProxyTotalCountOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<u64, ServiceError> {
#[derive(Debug, FromQueryResult)]
struct CountResult {
count: i64,
}
let count_info = with_conn!(&*self.connection, tx, conn, {
proxy_host::Entity::find()
.select_only()
.column_as(proxy_host::Column::Id.count(), "count")
.into_model::<CountResult>()
.one(*conn)
.await?
});
Ok(count_info.map_or(0, |c| c.count) as u64)
}
async fn get_proxies(
&self,
pagination: Option<PaginationFilter>,
options: Option<ProxyHostListOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<Vec<ProxyHostInfo>, ServiceError> {
let r = with_conn!(&*self.connection, tx, conn, {
let mut find_query = proxy_host::Entity::find();
if let Some(pagination) = pagination {
let (offset, limit) = pagination.get_offset_limit();
find_query = find_query.offset(offset).limit(limit);
}
let find_query = find_query
.apply_if(
options
.as_ref()
.is_some_and(|v| v.filter_by_enabled)
.then_some(true),
|q, _v| q.filter(location::Column::Enabled.eq(true)),
)
.find_with_related(location::Entity);
let r: Vec<ProxyHostInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
find_query
.join(JoinType::LeftJoin, proxy_host::Relation::Upstream.def())
.all(*conn)
.await?
.into_iter()
.map(|v| v.into())
.collect()
} else {
find_query
.all(*conn)
.await?
.into_iter()
.map(|m| m.into())
.collect()
};
r
});
Ok(r)
}
async fn get_proxy(
&self,
proxy_id: uuid::Uuid,
options: Option<ProxyHostGetOptions>,
tx: Option<&mut DatabaseTransaction>,
) -> Result<ProxyHostInfo, ServiceError> {
let r: ProxyHostInfo = with_conn!(&*self.connection, tx, conn, {
let find_query = proxy_host::Entity::find_by_id(proxy_id)
.apply_if(
options
.as_ref()
.is_some_and(|v| v.filter_by_enabled)
.then_some(true),
|q, _v| q.filter(location::Column::Enabled.eq(true)),
)
.find_with_related(location::Entity);
let r: Option<ProxyHostInfo> = if options.as_ref().is_some_and(|v| v.include_upstream) {
find_query
.join(JoinType::LeftJoin, proxy_host::Relation::Upstream.def())
.all(*conn)
.await?
.into_iter()
.next()
.map(|v| v.into())
} else {
find_query
.all(*conn)
.await?
.into_iter()
.map(|m| m.into())
.next()
};
r.ok_or(ServiceError::NotFound(format!(
"Proxy host with id {} not found",
proxy_id
)))?
});
Ok(r)
}
async fn update_proxy(
&self,
proxy_id: uuid::Uuid,
update: UpdateProxyHostInfo,
tx: Option<&mut DatabaseTransaction>,
) -> Result<ProxyHostInfo, ServiceError> {
let current_model = with_conn!(&*self.connection, tx, conn, {
proxy_host::Entity::find_by_id(proxy_id)
.one(*conn)
.await?
.ok_or(ServiceError::NotFound(format!(
"Proxy host with id {} not found",
proxy_id
)))?
});
let active_model = update.apply_to_model(current_model);
let r = with_conn!(&*self.connection, tx, conn, {
active_model.update(*conn).await?
});
Ok(r.into())
}
async fn delete_proxy(
&self,
proxy_id: uuid::Uuid,
tx: Option<&mut DatabaseTransaction>,
) -> Result<(), ServiceError> {
let model = with_conn!(&*self.connection, tx, conn, {
proxy_host::Entity::find_by_id(proxy_id)
.one(*conn)
.await?
.ok_or(ServiceError::NotFound(format!(
"Proxy host with id {} not found",
proxy_id
)))?
});
with_conn!(&*self.connection, tx, conn, {
model.delete(*conn).await?;
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use sea_orm::MockExecResult;
use sea_orm::{DatabaseBackend, MockDatabase};
use database::generated::entities::{location, proxy_host};
#[tokio::test]
async fn create_proxy_returns_info() {
let id = uuid::Uuid::new_v4();
let created = proxy_host::Model {
id,
name: Some("test_proxy".to_string()),
domain: "example.com".to_string(),
scheme: "http".to_string(),
listen_port: 80,
forward_scheme: "http".to_string(),
forward_host: None,
forward_port: None,
preserve_host_header: false,
enable_websocket: false,
enabled: true,
meta: None,
default_upstream_id: None,
created_by: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let loc = location::Model {
id: uuid::Uuid::new_v4(),
host_id: id,
path: "/".to_string(),
match_type: "prefix".to_string(),
order: 0,
upstream_id: None,
proxy_pass_protocol: None,
proxy_pass_host: None,
proxy_pass_port: None,
preserve_host_header: None,
allowed_methods: None,
custom_config: None,
enabled: true,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![created.clone()]])
.append_query_results(vec![vec![loc.clone()]])
.into_connection();
let svc = ProxyServiceImpl::new(Arc::new(db));
let create_info = crate::services::nginx::info::proxy_host::ProxyHostCreateInfo {
name: Some("test_proxy".to_string()),
domain: "example.com".to_string(),
scheme: "http".to_string(),
listen_port: 80,
forward_scheme: "http".to_string(),
forward_host: None,
forward_port: None,
preserve_host_header: false,
enable_websocket: false,
enabled: true,
meta: None,
default_upstream_id: None,
created_by: None,
locations: Vec::new(),
};
let res = svc.create_proxy(create_info, None).await;
assert!(res.is_ok());
let info = res.expect("Failed to create proxy");
assert_eq!(info.domain, "example.com");
}
#[tokio::test]
async fn get_total_proxies_returns_count() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
.into_connection();
let svc = ProxyServiceImpl::new(Arc::new(db));
let res = svc
.get_total_proxies(None, None)
.await
.expect("Failed to get total proxies");
assert_eq!(res, 0u64);
}
#[tokio::test]
async fn get_proxies_returns_list() {
let p1 = proxy_host::Model {
id: uuid::Uuid::new_v4(),
name: Some("p1".to_string()),
domain: "d1".to_string(),
scheme: "http".to_string(),
listen_port: 80,
forward_scheme: "http".to_string(),
forward_host: None,
forward_port: None,
preserve_host_header: false,
enable_websocket: false,
enabled: true,
meta: None,
default_upstream_id: None,
created_by: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let p2 = proxy_host::Model {
id: uuid::Uuid::new_v4(),
name: Some("p2".to_string()),
domain: "d2".to_string(),
scheme: "http".to_string(),
listen_port: 80,
forward_scheme: "http".to_string(),
forward_host: None,
forward_port: None,
preserve_host_header: false,
enable_websocket: false,
enabled: true,
meta: None,
default_upstream_id: None,
created_by: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![
(p1.clone(), None::<location::Model>),
(p2.clone(), None::<location::Model>),
]])
.into_connection();
let svc = ProxyServiceImpl::new(Arc::new(db));
let res = svc.get_proxies(None, None, None).await;
assert!(res.is_ok());
let list = res.expect("Failed to get proxies");
assert_eq!(list.len(), 2);
}
#[tokio::test]
async fn get_proxy_returns_info() {
let id = uuid::Uuid::new_v4();
let p = proxy_host::Model {
id,
name: Some("proxy".to_string()),
domain: "ex.com".to_string(),
scheme: "http".to_string(),
listen_port: 80,
forward_scheme: "http".to_string(),
forward_host: None,
forward_port: None,
preserve_host_header: false,
enable_websocket: false,
enabled: true,
meta: None,
default_upstream_id: None,
created_by: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![(p.clone(), None::<location::Model>)]])
.into_connection();
let svc = ProxyServiceImpl::new(Arc::new(db));
let res = svc.get_proxy(id, None, None).await;
assert!(res.is_ok());
let got = res.expect("Failed to get proxy");
assert_eq!(got.id, id);
}
#[tokio::test]
async fn get_proxy_not_found() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
.into_connection();
let svc = ProxyServiceImpl::new(Arc::new(db));
let res = svc.get_proxy(uuid::Uuid::new_v4(), None, None).await;
assert!(matches!(res, Err(ServiceError::NotFound(_))));
}
#[tokio::test]
async fn update_proxy_success() {
let id = uuid::Uuid::new_v4();
let existing = proxy_host::Model {
id,
name: Some("old".to_string()),
domain: "d".to_string(),
scheme: "http".to_string(),
listen_port: 80,
forward_scheme: "http".to_string(),
forward_host: None,
forward_port: None,
preserve_host_header: false,
enable_websocket: false,
enabled: true,
meta: None,
default_upstream_id: None,
created_by: None,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let updated = proxy_host::Model {
id,
name: Some("new".to_string()),
domain: existing.domain.clone(),
scheme: existing.scheme.clone(),
listen_port: existing.listen_port,
forward_scheme: existing.forward_scheme.clone(),
forward_host: existing.forward_host.clone(),
forward_port: existing.forward_port,
preserve_host_header: existing.preserve_host_header,
enable_websocket: existing.enable_websocket,
enabled: existing.enabled,
meta: existing.meta.clone(),
default_upstream_id: existing.default_upstream_id,
created_by: existing.created_by,
created_at: existing.created_at,
updated_at: chrono::Utc::now(),
};
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![vec![existing.clone()]])
.append_query_results(vec![vec![updated.clone()]])
.into_connection();
let svc = ProxyServiceImpl::new(Arc::new(db));
let update_info = crate::services::nginx::info::proxy_host::UpdateProxyHostInfo {
name: None,
domain: None,
scheme: None,
listen_port: None,
forward_scheme: None,
forward_host: None,
forward_port: None,
preserve_host_header: None,
enable_websocket: None,
enabled: None,
meta: None,
default_upstream_id: None,
};
let res = svc.update_proxy(id, update_info, None).await;
assert!(res.is_ok());
let got = res.expect("Failed to update proxy");
assert_eq!(got.name.expect("Name should be present"), "new");
}
#[tokio::test]
async fn update_proxy_not_found() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
.into_connection();
let svc = ProxyServiceImpl::new(Arc::new(db));
let res = svc
.update_proxy(
uuid::Uuid::new_v4(),
crate::services::nginx::info::proxy_host::UpdateProxyHostInfo {
name: None,
domain: None,
scheme: None,
listen_port: None,
forward_scheme: None,
forward_host: None,
forward_port: None,
preserve_host_header: None,
enable_websocket: None,
enabled: None,
meta: None,
default_upstream_id: None,
},
None,
)
.await;
assert!(matches!(res, Err(ServiceError::NotFound(_))));
}
#[tokio::test]
async fn delete_proxy_success() {
let id = uuid::Uuid::new_v4();
let existing = proxy_host::Model {
id,
name: Some("to-delete".to_string()),
domain: "d".to_string(),
scheme: "http".to_string(),
listen_port: 80,
forward_scheme: "http".to_string(),
forward_host: None,
forward_port: None,
preserve_host_header: false,
enable_websocket: false,
enabled: true,
meta: None,
default_upstream_id: None,
created_by: None,
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![MockExecResult {
rows_affected: 1,
last_insert_id: 0,
}])
.into_connection();
let svc = ProxyServiceImpl::new(Arc::new(db));
let res = svc.delete_proxy(id, None).await;
assert!(res.is_ok());
}
#[tokio::test]
async fn delete_proxy_not_found() {
let db = MockDatabase::new(DatabaseBackend::Sqlite)
.append_query_results(vec![Vec::<sea_orm::MockRow>::new()])
.into_connection();
let svc = ProxyServiceImpl::new(Arc::new(db));
let res = svc.delete_proxy(uuid::Uuid::new_v4(), None).await;
assert!(matches!(res, Err(ServiceError::NotFound(_))));
}
}