confirmation emails (hacky)
This commit is contained in:
parent
486fe68d3b
commit
ca21a84725
10 changed files with 334 additions and 44 deletions
150
Cargo.lock
generated
150
Cargo.lock
generated
|
@ -148,6 +148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"axum-macros",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
|
@ -219,6 +220,17 @@ dependencies = [
|
|||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-macros"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.75"
|
||||
|
@ -387,6 +399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -452,7 +465,7 @@ dependencies = [
|
|||
"aes-gcm",
|
||||
"base64 0.22.1",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"subtle",
|
||||
"time",
|
||||
"version_check",
|
||||
|
@ -544,7 +557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
|
@ -695,6 +708,8 @@ dependencies = [
|
|||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1475,6 +1490,20 @@ version = "0.4.27"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "maik"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba8f0b47dc7cc74760332828e060ec705339b5e863894f575d81691546bbc836"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"lazy_static",
|
||||
"native-tls",
|
||||
"rcgen",
|
||||
"regex",
|
||||
"wg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
|
@ -1580,7 +1609,7 @@ dependencies = [
|
|||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
]
|
||||
|
@ -1737,6 +1766,16 @@ version = "0.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
|
@ -1868,6 +1907,21 @@ dependencies = [
|
|||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
|
@ -1954,8 +2008,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1965,7 +2029,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1977,6 +2051,28 @@ dependencies = [
|
|||
"getrandom 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"time",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.12"
|
||||
|
@ -2115,7 +2211,7 @@ dependencies = [
|
|||
"num-traits",
|
||||
"pkcs1",
|
||||
"pkcs8",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
"signature",
|
||||
"spki",
|
||||
"subtle",
|
||||
|
@ -2388,7 +2484,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"rand_core",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2556,7 +2652,7 @@ dependencies = [
|
|||
"memchr",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"rsa",
|
||||
"serde",
|
||||
"sha1",
|
||||
|
@ -2596,7 +2692,7 @@ dependencies = [
|
|||
"md-5",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
|
@ -3082,6 +3178,16 @@ version = "0.1.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
|
@ -3314,6 +3420,18 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wg"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7aafc5e81e847f05d6770e074faf7b1cd4a5dec9a0e88eac5d55e20fdfebee9a"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"parking_lot",
|
||||
"triomphe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.6.0"
|
||||
|
@ -3682,6 +3800,15 @@ dependencies = [
|
|||
"hashlink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
|
@ -3719,7 +3846,10 @@ dependencies = [
|
|||
"http-body-util",
|
||||
"hyper",
|
||||
"lettre",
|
||||
"maik",
|
||||
"pin-project",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -9,13 +9,14 @@ license = "MIT OR Apache-2.0"
|
|||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.71", features = ["backtrace"] }
|
||||
axum = {version = "0.8", features = ["tokio", "http1", "http2"] }
|
||||
axum = { version = "0.8", features = ["tokio", "http1", "http2", "macros"] }
|
||||
axum-extra = { version = "0.10", features = ["cookie-private", "typed-header"] }
|
||||
config = { version = "0.15", features = ["toml"] }
|
||||
futures-util = "0.3"
|
||||
hyper = "1.1"
|
||||
lettre = { version = "0.11.17", features = ["tokio1", "tokio1-native-tls", "tracing", "web"] }
|
||||
pin-project = "1.1.0"
|
||||
rand = "0.9.1"
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
serde_json = "1.0.99"
|
||||
sqlx = { version = "0.8.5", features = ["runtime-tokio", "macros", "postgres", "uuid", "chrono", "migrate"] }
|
||||
|
@ -33,6 +34,8 @@ uuid = { version = "1.16.0", features = ["v4"] }
|
|||
[dev-dependencies]
|
||||
bollard = { git = "https://github.com/fussybeaver/bollard.git", rev = "50a25a0" }
|
||||
http-body-util = "0.1.3"
|
||||
maik = "0.2.0"
|
||||
regex = "1.11.1"
|
||||
reqwest = { version = "0.12", features = ["cookies"] }
|
||||
test-log = { version = "0.2.12", default-features = false, features = ["trace"] }
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
BEGIN;
|
||||
|
||||
UPDATE subscriptions
|
||||
SET status = 'confirmed'
|
||||
where status IS NULL;
|
||||
|
||||
ALTER TABLE subscriptions ALTER COLUMN status SET NOT NULL;
|
||||
|
||||
COMMIT;
|
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE subscription_tokens(
|
||||
token TEXT NOT NULL,
|
||||
subscriber uuid NOT NULL
|
||||
REFERENCES subscriptions (id),
|
||||
PRIMARY KEY (token)
|
||||
);
|
|
@ -16,9 +16,11 @@ pub struct App {
|
|||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Email {
|
||||
pub server: String,
|
||||
pub port: Option<u16>,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub sender: String,
|
||||
pub cert: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
|
|
@ -3,7 +3,10 @@ use std::time::Duration;
|
|||
use anyhow::{Context as _, Result};
|
||||
use lettre::{
|
||||
message::{header::ContentType, Mailbox},
|
||||
transport::smtp::authentication::Credentials,
|
||||
transport::smtp::{
|
||||
authentication::Credentials,
|
||||
client::{Certificate, TlsParametersBuilder},
|
||||
},
|
||||
AsyncSmtpTransport, AsyncTransport as _, Message, Tokio1Executor,
|
||||
};
|
||||
use tokio::time::timeout;
|
||||
|
@ -20,21 +23,38 @@ pub(crate) enum EmailClient {
|
|||
}
|
||||
|
||||
impl EmailClient {
|
||||
pub fn new(conf: Option<conf::Email>) -> EmailClient {
|
||||
pub fn new(conf: Option<conf::Email>) -> Result<EmailClient> {
|
||||
let Some(conf) = conf else {
|
||||
return EmailClient::Disabled;
|
||||
return Ok(EmailClient::Disabled);
|
||||
};
|
||||
|
||||
let inner: AsyncSmtpTransport<Tokio1Executor> =
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay(&conf.server)
|
||||
.unwrap()
|
||||
.credentials(Credentials::new(
|
||||
conf.username.clone(),
|
||||
conf.password.clone(),
|
||||
))
|
||||
.build();
|
||||
let mut inner = AsyncSmtpTransport::<Tokio1Executor>::relay(&conf.server).unwrap();
|
||||
|
||||
EmailClient::Enabled { inner, conf }
|
||||
if let Some(port) = conf.port {
|
||||
inner = inner.port(port);
|
||||
}
|
||||
|
||||
if let Some(ref cert) = conf.cert {
|
||||
let cert =
|
||||
Certificate::from_pem(cert.as_bytes()).context("parse certificate as PEM")?;
|
||||
let tls_param = TlsParametersBuilder::new(conf.server.clone())
|
||||
.add_root_certificate(cert)
|
||||
.build()
|
||||
.context("build tls params")?;
|
||||
|
||||
inner = inner.tls(lettre::transport::smtp::client::Tls::Opportunistic(
|
||||
tls_param,
|
||||
));
|
||||
}
|
||||
|
||||
let inner: AsyncSmtpTransport<Tokio1Executor> = inner
|
||||
.credentials(Credentials::new(
|
||||
conf.username.clone(),
|
||||
conf.password.clone(),
|
||||
))
|
||||
.build();
|
||||
|
||||
Ok(EmailClient::Enabled { inner, conf })
|
||||
}
|
||||
|
||||
pub async fn send_email(&self, to: Mailbox, subject: String, body: String) -> Result<()> {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
pub mod routes;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use axum::extract::FromRef;
|
||||
use axum_extra::extract::cookie::Key;
|
||||
use pin_project::pin_project;
|
||||
|
@ -47,7 +47,8 @@ impl ZeroToAxum {
|
|||
.connect(&conf.database.url)
|
||||
.await?;
|
||||
|
||||
let email_client = email_client::EmailClient::new(conf.email.clone());
|
||||
let email_client =
|
||||
email_client::EmailClient::new(conf.email.clone()).context("build email client")?;
|
||||
|
||||
let app_state = AppState {
|
||||
conf: Arc::new(conf.clone()),
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
use anyhow::Context;
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::post, Form, Router};
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use rand::{distr::Alphanumeric, rng, Rng as _};
|
||||
use serde::Deserialize;
|
||||
use sqlx::types::chrono::Utc;
|
||||
use tracing::{debug, error, info};
|
||||
|
@ -8,7 +15,9 @@ use uuid::Uuid;
|
|||
use crate::server::AppState;
|
||||
|
||||
pub fn build() -> Router<AppState> {
|
||||
Router::new().route("/", post(subscribe))
|
||||
Router::new()
|
||||
.route("/", post(subscribe))
|
||||
.route("/confirm/", get(confirm))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -29,31 +38,56 @@ pub async fn subscribe(
|
|||
return Err(SubscribeError::InvalidEmail);
|
||||
}
|
||||
|
||||
debug!("email sent");
|
||||
|
||||
let mut txn = db.begin().await.context("start database transaction")?;
|
||||
|
||||
let subscriber_id = Uuid::new_v4();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO subscriptions (id, email, name, subscribed_at, status)
|
||||
VALUES ($1, $2, $3, $4, 'pending_confirmation')
|
||||
"#,
|
||||
subscriber_id,
|
||||
form.email,
|
||||
form.name,
|
||||
Utc::now()
|
||||
)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.context("insert subscription into database")?;
|
||||
|
||||
let token: String = {
|
||||
let mut rng = rng();
|
||||
std::iter::repeat_with(|| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(25)
|
||||
.collect()
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO subscription_tokens (subscriber, token)
|
||||
VALUES ($1, $2)
|
||||
"#,
|
||||
subscriber_id,
|
||||
token
|
||||
)
|
||||
.execute(&mut *txn)
|
||||
.await
|
||||
.context("insert subscription token into database")?;
|
||||
|
||||
email_client
|
||||
.send_email(
|
||||
form.email
|
||||
.parse()
|
||||
.map_err(|_| SubscribeError::InvalidEmail)?,
|
||||
"Test".to_owned(),
|
||||
"This was a test.".to_owned(),
|
||||
format!("Please confirm your subscription: https://example.com/subscriptions/confirm/?token={token}"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
debug!("email sent");
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO subscriptions (id, email, name, subscribed_at, status)
|
||||
VALUES ($1, $2, $3, $4, 'confirmed')
|
||||
"#,
|
||||
Uuid::new_v4(),
|
||||
form.email,
|
||||
form.name,
|
||||
Utc::now()
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
.context("insert subscription into database")?;
|
||||
txn.commit().await.context("commit transaction")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -78,3 +112,57 @@ impl IntoResponse for SubscribeError {
|
|||
.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ConfirmQuery {
|
||||
token: String,
|
||||
}
|
||||
|
||||
pub async fn confirm(
|
||||
State(AppState { db, .. }): State<AppState>,
|
||||
Query(query): Query<ConfirmQuery>,
|
||||
) -> Result<(), ConfirmError> {
|
||||
info!(query.token, "confirm attempt");
|
||||
|
||||
if query.token.is_empty() {
|
||||
return Err(ConfirmError::InvalidToken);
|
||||
}
|
||||
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
UPDATE subscriptions SET status = 'confirmed' FROM subscription_tokens
|
||||
WHERE status = 'pending_confirmation' AND subscription_tokens.subscriber = id AND subscription_tokens.token = $1;
|
||||
"#,
|
||||
query.token
|
||||
)
|
||||
.execute(&db)
|
||||
.await
|
||||
.context("insert subscription into database")?.rows_affected();
|
||||
|
||||
if rows < 1 {
|
||||
return Err(ConfirmError::InvalidToken);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ConfirmError {
|
||||
#[error("Invalid Confirmation Token")]
|
||||
InvalidToken,
|
||||
#[error("Unknown Error: {0}")]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl IntoResponse for ConfirmError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
ConfirmError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid Confirmation Token"),
|
||||
ConfirmError::Unknown(e) => {
|
||||
error!(?e, "returning INTERNAL SERVER ERROR");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Unknown Error")
|
||||
}
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ pub struct TestServer {
|
|||
server_task_handle: JoinHandle<()>,
|
||||
addr: SocketAddr,
|
||||
db: Arc<TestDb>,
|
||||
pub mock_smtp_server: maik::MockServer,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
|
@ -42,13 +43,26 @@ impl TestServer {
|
|||
let db = get_shared_db().await;
|
||||
let url = dbg!(db.get_url());
|
||||
|
||||
let mock_smtp_server = maik::MockServer::builder()
|
||||
.add_mailbox("bot@example.com", "1234")
|
||||
.assert_after_n_emails(1)
|
||||
.build();
|
||||
mock_smtp_server.start();
|
||||
|
||||
let server = ZeroToAxum::serve(Conf {
|
||||
app: conf::App {
|
||||
listen: "[::]:0".parse().unwrap(),
|
||||
},
|
||||
database: conf::Database { url },
|
||||
debug: true,
|
||||
email: None,
|
||||
email: Some(conf::Email {
|
||||
server: mock_smtp_server.host().to_string(),
|
||||
port: Some(mock_smtp_server.port()),
|
||||
username: "bot@example.com".to_owned(),
|
||||
password: "1234".to_owned(),
|
||||
sender: "bot@example.com".to_owned(),
|
||||
cert: Some(String::from_utf8_lossy(mock_smtp_server.cert_pem()).to_string()),
|
||||
}),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -60,6 +74,7 @@ impl TestServer {
|
|||
server_task_handle,
|
||||
addr,
|
||||
db,
|
||||
mock_smtp_server,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,6 +83,13 @@ impl TestServer {
|
|||
format!("http://{}{path}", self.addr)
|
||||
}
|
||||
|
||||
/// Request a graceful shutdown and return imideately.
|
||||
pub async fn start_shutdown(&self) -> Result<()> {
|
||||
self.server_task_handle.abort();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Request a graceful shutdown and then wait for shutdown to complete
|
||||
pub async fn shutdown(self) -> Result<()> {
|
||||
self.server_task_handle.abort();
|
||||
|
|
|
@ -2,6 +2,8 @@ pub mod fixture;
|
|||
use fixture::TestServer;
|
||||
|
||||
use anyhow::Result;
|
||||
use maik::MailAssertion;
|
||||
use regex::bytes::Regex;
|
||||
use test_log::test as traced;
|
||||
|
||||
#[traced(tokio::test)]
|
||||
|
@ -19,6 +21,13 @@ async fn subscribe_succeeds_with_valid_input() -> Result<()> {
|
|||
|
||||
assert_eq!(resp.status(), 200, "subscribe succeeds");
|
||||
|
||||
assert!(
|
||||
server
|
||||
.mock_smtp_server
|
||||
.assert(MailAssertion::new().body_matches(Regex::new(r"example\.com")?)),
|
||||
"email sent has link"
|
||||
);
|
||||
|
||||
server.shutdown().await
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue