Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
c067177dab
|
|||
|
cb047968c3
|
|||
|
9c4532f940
|
|||
|
84cf3359aa
|
|||
|
079047a0d5
|
|||
|
302f07f43f
|
|||
|
38fe2fa452
|
|||
|
e919167d22
|
|||
|
2059572344
|
|||
|
8abb3d660f
|
|||
|
2bfdf60e9e
|
|||
|
4fb8bda761
|
|||
|
2370462a3e
|
|||
|
5c200e1f89
|
|||
|
b31a5ccaf5
|
@@ -40,21 +40,9 @@ jobs:
|
|||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
skipPush: ${{ github.event_name == 'pull_request' }}
|
skipPush: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
- name: Format Check
|
|
||||||
shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}"
|
|
||||||
run: just format-check
|
|
||||||
|
|
||||||
- name: Audit
|
|
||||||
shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}"
|
|
||||||
run: just audit
|
|
||||||
|
|
||||||
- name: Lint
|
|
||||||
shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}"
|
|
||||||
run: just lint-report
|
|
||||||
|
|
||||||
- name: Coverage
|
- name: Coverage
|
||||||
shell: bash -c "nix develop --no-pure-eval --accept-flake-config --command {0}"
|
run: |
|
||||||
run: just coverage-ci
|
nix develop --no-pure-eval --accept-flake-config --command just coverage
|
||||||
|
|
||||||
- name: Sonar analysis
|
- name: Sonar analysis
|
||||||
uses: SonarSource/sonarqube-scan-action@v6
|
uses: SonarSource/sonarqube-scan-action@v6
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ backend/
|
|||||||
|
|
||||||
The contact form supports multiple SMTP configurations:
|
The contact form supports multiple SMTP configurations:
|
||||||
- **Implicit TLS (SMTPS)** - typically port 465
|
- **Implicit TLS (SMTPS)** - typically port 465
|
||||||
- **STARTTLS (Always)** - typically port 587
|
- **STARTTLS (Always/Opportunistic)** - typically port 587
|
||||||
- **Unencrypted** (for local dev) - with or without authentication
|
- **Unencrypted** (for local dev) - with or without authentication
|
||||||
|
|
||||||
The `SmtpTransport` is built dynamically from `EmailSettings` based on
|
The `SmtpTransport` is built dynamically from `EmailSettings` based on
|
||||||
|
|||||||
@@ -24,10 +24,6 @@ build-release:
|
|||||||
lint:
|
lint:
|
||||||
cargo clippy --all-targets
|
cargo clippy --all-targets
|
||||||
|
|
||||||
lint-report:
|
|
||||||
mkdir -p coverage
|
|
||||||
cargo clippy --all-targets --message-format=json > coverage/clippy.json
|
|
||||||
|
|
||||||
release-build:
|
release-build:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -24,7 +24,7 @@ pub mod startup;
|
|||||||
/// Logging and tracing setup
|
/// Logging and tracing setup
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
|
|
||||||
type MaybeListener = Option<std::net::TcpListener>;
|
type MaybeListener = Option<poem::listener::TcpListener<String>>;
|
||||||
|
|
||||||
fn prepare(listener: MaybeListener) -> startup::Application {
|
fn prepare(listener: MaybeListener) -> startup::Application {
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
@@ -70,8 +70,11 @@ pub async fn run(listener: MaybeListener) -> Result<(), std::io::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn make_random_tcp_listener() -> std::net::TcpListener {
|
fn make_random_tcp_listener() -> poem::listener::TcpListener<String> {
|
||||||
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind a random TCP listener")
|
let tcp_listener =
|
||||||
|
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind a random TCP listener");
|
||||||
|
let port = tcp_listener.local_addr().unwrap().port();
|
||||||
|
poem::listener::TcpListener::bind(format!("127.0.0.1:{port}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -6,11 +6,7 @@
|
|||||||
|
|
||||||
use std::{net::IpAddr, num::NonZeroU32, sync::Arc, time::Duration};
|
use std::{net::IpAddr, num::NonZeroU32, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use governor::{
|
use governor::{Quota, RateLimiter, clock::{Clock, DefaultClock}, state::keyed::DefaultKeyedStateStore};
|
||||||
Quota, RateLimiter,
|
|
||||||
clock::{Clock, DefaultClock},
|
|
||||||
state::keyed::DefaultKeyedStateStore,
|
|
||||||
};
|
|
||||||
use poem::{Endpoint, Error, IntoResponse, Middleware, Request, Response, Result};
|
use poem::{Endpoint, Error, IntoResponse, Middleware, Request, Response, Result};
|
||||||
|
|
||||||
type BakitRateLimiter = RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>;
|
type BakitRateLimiter = RateLimiter<IpAddr, DefaultKeyedStateStore<IpAddr>, DefaultClock>;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ impl Error for ContactError {}
|
|||||||
/// issues beyond the client's control.
|
/// issues beyond the client's control.
|
||||||
impl From<lettre::transport::smtp::Error> for ContactError {
|
impl From<lettre::transport::smtp::Error> for ContactError {
|
||||||
fn from(value: lettre::transport::smtp::Error) -> Self {
|
fn from(value: lettre::transport::smtp::Error) -> Self {
|
||||||
tracing::event!(target: "backend::contact", tracing::Level::ERROR, "SMTP Error details: {}", format!("{value:?}"));
|
tracing::event!(target: "contact", tracing::Level::ERROR, "SMTP Error details: {}", format!("{value:?}"));
|
||||||
Self::OtherError(value.to_string())
|
Self::OtherError(value.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ impl TryFrom<&EmailSettings> for SmtpTransport {
|
|||||||
Ok(builder.credentials(creds).build())
|
Ok(builder.credentials(creds).build())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Starttls::Always => {
|
Starttls::Opportunistic | Starttls::Always => {
|
||||||
// STARTTLS - typically port 587
|
// STARTTLS - typically port 587
|
||||||
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Using STARTTLS");
|
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Using STARTTLS");
|
||||||
let creds = Credentials::new(settings.user.clone(), settings.password.clone());
|
let creds = Credentials::new(settings.user.clone(), settings.password.clone());
|
||||||
@@ -186,6 +186,7 @@ impl ContactApi {
|
|||||||
remote_addr: Option<poem::web::Data<&poem::web::RemoteAddr>>,
|
remote_addr: Option<poem::web::Data<&poem::web::RemoteAddr>>,
|
||||||
) -> ContactApiResponse {
|
) -> ContactApiResponse {
|
||||||
let mut body = body.0;
|
let mut body = body.0;
|
||||||
|
body.sanitize();
|
||||||
if let Some(ref honeypot) = body.honeypot
|
if let Some(ref honeypot) = body.honeypot
|
||||||
&& !honeypot.trim().is_empty()
|
&& !honeypot.trim().is_empty()
|
||||||
{
|
{
|
||||||
@@ -197,7 +198,6 @@ impl ContactApi {
|
|||||||
);
|
);
|
||||||
return ContactApiResponse::Ok(ContactResponse::honeypot_response().into());
|
return ContactApiResponse::Ok(ContactResponse::honeypot_response().into());
|
||||||
}
|
}
|
||||||
body.sanitize();
|
|
||||||
if let Err(e) = body.validate() {
|
if let Err(e) = body.validate() {
|
||||||
return ContactApiResponse::BadRequest(
|
return ContactApiResponse::BadRequest(
|
||||||
<validator::ValidationErrors as std::convert::Into<ContactResponse>>::into(e)
|
<validator::ValidationErrors as std::convert::Into<ContactResponse>>::into(e)
|
||||||
@@ -251,7 +251,7 @@ impl ContactApi {
|
|||||||
.subject(format!("Contact Form: {}", request.name))
|
.subject(format!("Contact Form: {}", request.name))
|
||||||
.header(ContentType::TEXT_PLAIN)
|
.header(ContentType::TEXT_PLAIN)
|
||||||
.body(email_body)?;
|
.body(email_body)?;
|
||||||
tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Email to be sent: {}", format!("{email:?}"));
|
tracing::event!(target: "contact", tracing::Level::DEBUG, "Email to be sent: {}", format!("{email:?}"));
|
||||||
Ok(email)
|
Ok(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +429,23 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn smtp_transport_starttls_opportunistic() {
|
||||||
|
let settings = EmailSettings {
|
||||||
|
host: "smtp.example.com".to_string(),
|
||||||
|
port: 587,
|
||||||
|
user: "user@example.com".to_string(),
|
||||||
|
password: "password".to_string(),
|
||||||
|
from: "from@example.com".to_string(),
|
||||||
|
recipient: "to@example.com".to_string(),
|
||||||
|
tls: false,
|
||||||
|
starttls: Starttls::Opportunistic,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = SmtpTransport::try_from(&settings);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn smtp_transport_no_encryption_with_credentials() {
|
fn smtp_transport_no_encryption_with_credentials() {
|
||||||
let settings = EmailSettings {
|
let settings = EmailSettings {
|
||||||
@@ -1081,4 +1098,6 @@ mod tests {
|
|||||||
assert_eq!(request_uk.email, "oleksandr@example.com");
|
assert_eq!(request_uk.email, "oleksandr@example.com");
|
||||||
assert_eq!(request_uk.message, "Привіт");
|
assert_eq!(request_uk.message, "Привіт");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-5
@@ -163,9 +163,8 @@ impl EmailSettings {
|
|||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns a `ContactError` if the email address in the `from`
|
/// Returns a `ContactError` if the email address in the `from` field cannot be parsed
|
||||||
/// field of `recipient` cannot be parsed into a valid mailbox.
|
/// into a valid mailbox. This can occur if:
|
||||||
/// This can occur if:
|
|
||||||
/// - The email address format is invalid
|
/// - The email address format is invalid
|
||||||
/// - The email address contains invalid characters
|
/// - The email address contains invalid characters
|
||||||
/// - The email address structure is malformed
|
/// - The email address structure is malformed
|
||||||
@@ -197,6 +196,8 @@ pub enum Starttls {
|
|||||||
/// Never use STARTTLS (unencrypted connection)
|
/// Never use STARTTLS (unencrypted connection)
|
||||||
#[default]
|
#[default]
|
||||||
Never,
|
Never,
|
||||||
|
/// Use STARTTLS if available (opportunistic encryption)
|
||||||
|
Opportunistic,
|
||||||
/// Always use STARTTLS (required encryption)
|
/// Always use STARTTLS (required encryption)
|
||||||
Always,
|
Always,
|
||||||
}
|
}
|
||||||
@@ -207,9 +208,10 @@ impl TryFrom<&str> for Starttls {
|
|||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
match value.to_lowercase().as_str() {
|
match value.to_lowercase().as_str() {
|
||||||
"off" | "no" | "never" => Ok(Self::Never),
|
"off" | "no" | "never" => Ok(Self::Never),
|
||||||
|
"opportunistic" => Ok(Self::Opportunistic),
|
||||||
"yes" | "always" => Ok(Self::Always),
|
"yes" | "always" => Ok(Self::Always),
|
||||||
other => Err(format!(
|
other => Err(format!(
|
||||||
"{other} is not a supported option. Use either `yes` or `no`"
|
"{other} is not a supported option. Use either `yes`, `no`, or `opportunistic`"
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,6 +234,7 @@ impl std::fmt::Display for Starttls {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let self_str = match self {
|
let self_str = match self {
|
||||||
Self::Never => "never",
|
Self::Never => "never",
|
||||||
|
Self::Opportunistic => "opportunistic",
|
||||||
Self::Always => "always",
|
Self::Always => "always",
|
||||||
};
|
};
|
||||||
write!(f, "{self_str}")
|
write!(f, "{self_str}")
|
||||||
@@ -249,7 +252,7 @@ impl<'de> serde::Deserialize<'de> for Starttls {
|
|||||||
type Value = Starttls;
|
type Value = Starttls;
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
formatter.write_str("a string or boolean representing STARTTLS setting (e.g., 'yes', 'no', true, false)")
|
formatter.write_str("a string or boolean representing STARTTLS setting (e.g., 'yes', 'no', 'opportunistic', true, false)")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_str<E>(self, value: &str) -> Result<Starttls, E>
|
fn visit_str<E>(self, value: &str) -> Result<Starttls, E>
|
||||||
@@ -431,6 +434,13 @@ mod tests {
|
|||||||
assert_eq!(result, Starttls::Always);
|
assert_eq!(result, Starttls::Always);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn startls_deserialize_from_string_opportunistic() {
|
||||||
|
let json = r#""opportunistic""#;
|
||||||
|
let result: Starttls = serde_json::from_str(json).unwrap();
|
||||||
|
assert_eq!(result, Starttls::Opportunistic);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn startls_deserialize_from_bool() {
|
fn startls_deserialize_from_bool() {
|
||||||
let json = "true";
|
let json = "true";
|
||||||
@@ -472,6 +482,18 @@ mod tests {
|
|||||||
assert_eq!(Starttls::try_from("Yes").unwrap(), Starttls::Always);
|
assert_eq!(Starttls::try_from("Yes").unwrap(), Starttls::Always);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn startls_try_from_str_opportunistic() {
|
||||||
|
assert_eq!(
|
||||||
|
Starttls::try_from("opportunistic").unwrap(),
|
||||||
|
Starttls::Opportunistic
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Starttls::try_from("OPPORTUNISTIC").unwrap(),
|
||||||
|
Starttls::Opportunistic
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn startls_try_from_str_invalid() {
|
fn startls_try_from_str_invalid() {
|
||||||
let result = Starttls::try_from("invalid");
|
let result = Starttls::try_from("invalid");
|
||||||
@@ -495,6 +517,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn startls_try_from_string_opportunistic() {
|
||||||
|
assert_eq!(
|
||||||
|
Starttls::try_from("opportunistic".to_string()).unwrap(),
|
||||||
|
Starttls::Opportunistic
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn startls_try_from_string_invalid() {
|
fn startls_try_from_string_invalid() {
|
||||||
let result = Starttls::try_from("invalid".to_string());
|
let result = Starttls::try_from("invalid".to_string());
|
||||||
@@ -523,6 +553,12 @@ mod tests {
|
|||||||
assert_eq!(startls.to_string(), "always");
|
assert_eq!(startls.to_string(), "always");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn startls_display_opportunistic() {
|
||||||
|
let startls = Starttls::Opportunistic;
|
||||||
|
assert_eq!(startls.to_string(), "opportunistic");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rate_limit_settings_default() {
|
fn rate_limit_settings_default() {
|
||||||
let settings = RateLimitSettings::default();
|
let settings = RateLimitSettings::default();
|
||||||
|
|||||||
+29
-44
@@ -6,7 +6,6 @@
|
|||||||
//! - Configuring CORS
|
//! - Configuring CORS
|
||||||
//! - Starting the HTTP server
|
//! - Starting the HTTP server
|
||||||
|
|
||||||
use poem::listener::{Listener, TcpAcceptor};
|
|
||||||
use poem::middleware::{AddDataEndpoint, Cors, CorsEndpoint};
|
use poem::middleware::{AddDataEndpoint, Cors, CorsEndpoint};
|
||||||
use poem::{EndpointExt, Route};
|
use poem::{EndpointExt, Route};
|
||||||
use poem_openapi::OpenApiService;
|
use poem_openapi::OpenApiService;
|
||||||
@@ -20,21 +19,10 @@ use crate::{
|
|||||||
|
|
||||||
use crate::middleware::rate_limit::RateLimitEndpoint;
|
use crate::middleware::rate_limit::RateLimitEndpoint;
|
||||||
|
|
||||||
type Server = poem::Server<poem::listener::BoxListener, std::convert::Infallible>;
|
type Server = poem::Server<poem::listener::TcpListener<String>, std::convert::Infallible>;
|
||||||
|
|
||||||
/// The configured application with rate limiting, CORS, and settings data.
|
/// The configured application with rate limiting, CORS, and settings data.
|
||||||
pub type App = AddDataEndpoint<CorsEndpoint<RateLimitEndpoint<Route>>, Settings>;
|
pub type App = AddDataEndpoint<CorsEndpoint<RateLimitEndpoint<Route>>, Settings>;
|
||||||
|
|
||||||
struct PreBoundListener(std::net::TcpListener);
|
|
||||||
|
|
||||||
impl Listener for PreBoundListener {
|
|
||||||
type Acceptor = TcpAcceptor;
|
|
||||||
|
|
||||||
async fn into_acceptor(self) -> std::io::Result<Self::Acceptor> {
|
|
||||||
TcpAcceptor::from_std(self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Application builder that holds the server configuration before running.
|
/// Application builder that holds the server configuration before running.
|
||||||
pub struct Application {
|
pub struct Application {
|
||||||
server: Server,
|
server: Server,
|
||||||
@@ -96,12 +84,10 @@ impl From<Application> for RunnableApplication {
|
|||||||
let cors = if value.settings.debug {
|
let cors = if value.settings.debug {
|
||||||
Cors::new()
|
Cors::new()
|
||||||
} else {
|
} else {
|
||||||
if !cfg!(test) {
|
assert!(
|
||||||
assert!(
|
!cfg!(test) || !frontend_url.is_empty(),
|
||||||
!frontend_url.is_empty(),
|
"CORS: frontend_url must be configured in production"
|
||||||
"CORS: frontend_url must be configured in production"
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
Cors::new().allow_origin(frontend_url)
|
Cors::new().allow_origin(frontend_url)
|
||||||
};
|
};
|
||||||
let app = value
|
let app = value
|
||||||
@@ -125,10 +111,10 @@ impl Application {
|
|||||||
)
|
)
|
||||||
.url_prefix("/api");
|
.url_prefix("/api");
|
||||||
let ui = api_service.swagger_ui();
|
let ui = api_service.swagger_ui();
|
||||||
let mut route = poem::Route::new().nest("/api", api_service.clone());
|
let mut route = poem::Route::new().nest("/", ui);
|
||||||
if settings.debug {
|
if settings.debug {
|
||||||
route = route
|
route = route
|
||||||
.nest("/", ui)
|
.nest("/api", api_service.clone())
|
||||||
.nest("/specs", api_service.spec_endpoint_yaml());
|
.nest("/specs", api_service.spec_endpoint_yaml());
|
||||||
}
|
}
|
||||||
route
|
route
|
||||||
@@ -147,33 +133,30 @@ impl Application {
|
|||||||
|
|
||||||
fn setup_server(
|
fn setup_server(
|
||||||
settings: &Settings,
|
settings: &Settings,
|
||||||
tcp_listener: Option<std::net::TcpListener>,
|
tcp_listener: Option<poem::listener::TcpListener<String>>,
|
||||||
) -> (Server, u16, String) {
|
) -> Server {
|
||||||
tcp_listener.map_or_else(
|
let tcp_listener = tcp_listener.unwrap_or_else(|| {
|
||||||
|| {
|
let address = format!(
|
||||||
let port = settings.application.port;
|
"{}:{}",
|
||||||
let host = settings.application.host.clone();
|
settings.application.host, settings.application.port
|
||||||
let address = format!("{host}:{port}");
|
);
|
||||||
let server = poem::Server::new(poem::listener::TcpListener::bind(address).boxed());
|
poem::listener::TcpListener::bind(address)
|
||||||
(server, port, host)
|
});
|
||||||
},
|
poem::Server::new(tcp_listener)
|
||||||
|listener| {
|
|
||||||
let addr = listener.local_addr().expect("Failed to get bound address");
|
|
||||||
let port = addr.port();
|
|
||||||
let host = addr.ip().to_string();
|
|
||||||
let server = poem::Server::new(PreBoundListener(listener).boxed());
|
|
||||||
(server, port, host)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a new application with the given settings and optional TCP listener.
|
/// Builds a new application with the given settings and optional TCP listener.
|
||||||
///
|
///
|
||||||
/// If no listener is provided, one will be created based on the settings.
|
/// If no listener is provided, one will be created based on the settings.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build(settings: Settings, tcp_listener: Option<std::net::TcpListener>) -> Self {
|
pub fn build(
|
||||||
let (server, port, host) = Self::setup_server(&settings, tcp_listener);
|
settings: Settings,
|
||||||
|
tcp_listener: Option<poem::listener::TcpListener<String>>,
|
||||||
|
) -> Self {
|
||||||
|
let port = settings.application.port;
|
||||||
|
let host = settings.application.host.clone();
|
||||||
let app = Self::setup_app(&settings);
|
let app = Self::setup_app(&settings);
|
||||||
|
let server = Self::setup_server(&settings, tcp_listener);
|
||||||
Self {
|
Self {
|
||||||
server,
|
server,
|
||||||
app,
|
app,
|
||||||
@@ -258,11 +241,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn application_with_custom_listener() {
|
fn application_with_custom_listener() {
|
||||||
let settings = create_test_settings();
|
let settings = create_test_settings();
|
||||||
let listener =
|
let tcp_listener =
|
||||||
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
|
std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
|
||||||
let expected_port = listener.local_addr().unwrap().port();
|
let port = tcp_listener.local_addr().unwrap().port();
|
||||||
|
let listener = poem::listener::TcpListener::bind(format!("127.0.0.1:{port}"));
|
||||||
|
|
||||||
let app = Application::build(settings, Some(listener));
|
let app = Application::build(settings, Some(listener));
|
||||||
assert_eq!(app.host(), "127.0.0.1");
|
assert_eq!(app.host(), "127.0.0.1");
|
||||||
assert_eq!(app.port(), expected_port);
|
assert_eq!(app.port(), 8080);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user