rest-http: add unit tests

axum-login-upgrade
Manos Pitsidianakis 2023-05-13 23:42:55 +03:00
parent 19860d2d87
commit 1af4579519
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
6 changed files with 364 additions and 55 deletions

5
Cargo.lock generated
View File

@ -1975,19 +1975,24 @@ dependencies = [
"assert-json-diff",
"async-trait",
"axum",
"axum-extra",
"bcrypt",
"config",
"http",
"hyper",
"lazy_static",
"log",
"mailpot",
"mailpot-tests",
"mailpot-web",
"reqwest",
"serde",
"serde_json",
"stderrlog",
"tempfile",
"thiserror",
"tokio",
"tower",
"tower-http 0.4.0",
]

Binary file not shown.

View File

@ -18,6 +18,7 @@ path = "src/main.rs"
[dependencies]
async-trait = "0.1"
axum = { version = "0.6", features = ["headers"] }
axum-extra = { version = "^0.7", features = ["typed-routing"] }
#jsonwebtoken = "8.3"
bcrypt = "0.14"
config = "0.13"
@ -41,4 +42,8 @@ tower-http = { version = "0.4", features = [
[dev-dependencies]
assert-json-diff = "2"
hyper = { version = "0.14" }
mailpot-tests = { version = "^0.1", path = "../mailpot-tests" }
reqwest = { version = "0.11", features = ["json"] }
tempfile = "3.3"
tower = { version = "^0.4" }

View File

@ -26,3 +26,26 @@ pub use mailpot::{models::*, Configuration, Connection};
pub mod errors;
pub mod routes;
pub mod settings;
use tower_http::{
compression::CompressionLayer, cors::CorsLayer, propagate_header::PropagateHeaderLayer,
sensitive_headers::SetSensitiveHeadersLayer,
};
pub fn create_app(conf: Arc<Configuration>) -> Router {
Router::new()
.with_state(conf.clone())
.merge(Router::new().nest("/v1", Router::new().merge(routes::list::create_route(conf))))
.layer(SetSensitiveHeadersLayer::new(std::iter::once(
header::AUTHORIZATION,
)))
// Compress responses
.layer(CompressionLayer::new())
// Propagate `X-Request-Id`s from requests to responses
.layer(PropagateHeaderLayer::new(header::HeaderName::from_static(
"x-request-id",
)))
// CORS configuration. This should probably be more restrictive in
// production.
.layer(CorsLayer::permissive())
}

View File

@ -1,25 +1,9 @@
use mailpot_http::{settings::SETTINGS, *};
use tower_http::{
compression::CompressionLayer, cors::CorsLayer, propagate_header::PropagateHeaderLayer,
sensitive_headers::SetSensitiveHeadersLayer,
};
use crate::routes;
use crate::create_app;
#[tokio::main]
async fn main() {
let app = create_app().await;
let port = SETTINGS.server.port;
let address = SocketAddr::from(([127, 0, 0, 1], port));
info!("Server listening on {}", &address);
axum::Server::bind(&address)
.serve(app.into_make_service())
.await
.expect("Failed to start server");
}
pub async fn create_app() -> Router {
let config_path = std::env::args()
.nth(1)
.expect("Expected configuration file path as first argument.");
@ -31,20 +15,14 @@ pub async fn create_app() -> Router {
.init()
.unwrap();
let conf = Arc::new(Configuration::from_file(config_path).unwrap());
let app = create_app(conf);
Router::new()
.with_state(conf.clone())
.merge(Router::new().nest("/v1", Router::new().merge(routes::list::create_route(conf))))
.layer(SetSensitiveHeadersLayer::new(std::iter::once(
header::AUTHORIZATION,
)))
// Compress responses
.layer(CompressionLayer::new())
// Propagate `X-Request-Id`s from requests to responses
.layer(PropagateHeaderLayer::new(header::HeaderName::from_static(
"x-request-id",
)))
// CORS configuration. This should probably be more restrictive in
// production.
.layer(CorsLayer::permissive())
let port = SETTINGS.server.port;
let address = SocketAddr::from(([127, 0, 0, 1], port));
info!("Server listening on {}", &address);
axum::Server::bind(&address)
.serve(app.into_make_service())
.await
.expect("Failed to start server");
}

View File

@ -1,28 +1,48 @@
use std::sync::Arc;
pub use axum::extract::{Path, Query, State};
use axum::{
http::StatusCode,
routing::{get, post},
Json, Router,
};
use mailpot_web::{typed_paths::*, ResponseError, RouterExt};
use axum::{http::StatusCode, Json, Router};
use mailpot_web::{typed_paths::*, ResponseError, RouterExt, TypedPath};
use serde::{Deserialize, Serialize};
use crate::*;
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
#[typed_path("/list/")]
pub struct ListsPath;
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
#[typed_path("/list/:id/owner/")]
pub struct ListOwnerPath(pub ListPathIdentifier);
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
#[typed_path("/list/:id/subscription/")]
pub struct ListSubscriptionPath(pub ListPathIdentifier);
pub fn create_route(conf: Arc<Configuration>) -> Router {
Router::new()
.route("/list/", get(all_lists))
.route("/list/", post(post_list))
.typed_get(all_lists)
.typed_post(new_list)
.typed_get(get_list)
.typed_post({
move |_: ListPath| async move {
Err::<(), ResponseError>(mailpot_web::ResponseError::new(
"Invalid method".to_string(),
StatusCode::BAD_REQUEST,
))
}
})
.typed_get(get_list_owner)
.typed_post(new_list_owner)
.typed_get(get_list_subs)
.typed_post(new_list_sub)
.with_state(conf)
}
async fn get_list(
ListPath(id): ListPath,
State(state): State<Arc<Configuration>>,
) -> Result<Json<DbVal<MailingList>>, ResponseError> {
) -> Result<Json<MailingList>, ResponseError> {
let db = Connection::open_db(Configuration::clone(&state))?;
let Some(list) = (match id {
ListPathIdentifier::Pk(id) => db.list(id)?,
@ -33,10 +53,11 @@ async fn get_list(
StatusCode::NOT_FOUND,
));
};
Ok(Json(list))
Ok(Json(list.into_inner()))
}
async fn all_lists(
_: ListsPath,
Query(GetRequest {
filter: _,
count,
@ -61,7 +82,12 @@ async fn all_lists(
}));
};
let offset = page * count;
let res: Vec<_> = lists_values.into_iter().skip(offset).take(count).collect();
let res: Vec<_> = lists_values
.into_iter()
.skip(offset)
.take(count)
.map(DbVal::into_inner)
.collect();
Ok(Json(GetResponse {
total: res.len(),
@ -70,18 +96,16 @@ async fn all_lists(
}))
}
async fn post_list(
State(state): State<Arc<Configuration>>,
Json(_body): Json<GetRequest>,
async fn new_list(
_: ListsPath,
State(_state): State<Arc<Configuration>>,
//Json(_body): Json<GetRequest>,
) -> Result<Json<()>, ResponseError> {
let _db = Connection::open_db(Configuration::clone(&state))?;
// let password_hash = list::hash_password(body.password).await?;
// let list = list::new(body.name, body.email, password_hash);
// let list = list::create(list).await?;
// let res = Publiclist::from(list);
//
Ok(Json(()))
// TODO create new list
Err(mailpot_web::ResponseError::new(
"Not allowed".to_string(),
StatusCode::UNAUTHORIZED,
))
}
#[derive(Debug, Serialize, Deserialize)]
@ -101,7 +125,281 @@ struct GetRequest {
#[derive(Debug, Serialize, Deserialize)]
struct GetResponse {
entries: Vec<DbVal<MailingList>>,
entries: Vec<MailingList>,
total: usize,
start: usize,
}
async fn get_list_owner(
ListOwnerPath(id): ListOwnerPath,
State(state): State<Arc<Configuration>>,
) -> Result<Json<Vec<ListOwner>>, ResponseError> {
let db = Connection::open_db(Configuration::clone(&state))?;
let owners = match id {
ListPathIdentifier::Pk(id) => db.list_owners(id)?,
ListPathIdentifier::Id(id) => {
if let Some(owners) = db.list_by_id(id)?.map(|l| db.list_owners(l.pk())) {
owners?
} else {
return Err(mailpot_web::ResponseError::new(
"Not found".to_string(),
StatusCode::NOT_FOUND,
));
}
}
};
Ok(Json(owners.into_iter().map(DbVal::into_inner).collect()))
}
async fn new_list_owner(
ListOwnerPath(_id): ListOwnerPath,
State(_state): State<Arc<Configuration>>,
//Json(_body): Json<GetRequest>,
) -> Result<Json<Vec<ListOwner>>, ResponseError> {
Err(mailpot_web::ResponseError::new(
"Not allowed".to_string(),
StatusCode::UNAUTHORIZED,
))
}
async fn get_list_subs(
ListSubscriptionPath(id): ListSubscriptionPath,
State(state): State<Arc<Configuration>>,
) -> Result<Json<Vec<ListSubscription>>, ResponseError> {
let db = Connection::open_db(Configuration::clone(&state))?;
let subs = match id {
ListPathIdentifier::Pk(id) => db.list_subscriptions(id)?,
ListPathIdentifier::Id(id) => {
if let Some(v) = db.list_by_id(id)?.map(|l| db.list_subscriptions(l.pk())) {
v?
} else {
return Err(mailpot_web::ResponseError::new(
"Not found".to_string(),
StatusCode::NOT_FOUND,
));
}
}
};
Ok(Json(subs.into_iter().map(DbVal::into_inner).collect()))
}
async fn new_list_sub(
ListSubscriptionPath(_id): ListSubscriptionPath,
State(_state): State<Arc<Configuration>>,
//Json(_body): Json<GetRequest>,
) -> Result<Json<ListSubscription>, ResponseError> {
Err(mailpot_web::ResponseError::new(
"Not allowed".to_string(),
StatusCode::UNAUTHORIZED,
))
}
#[cfg(test)]
mod tests {
use axum::{
body::Body,
http::{method::Method, Request, StatusCode},
};
use mailpot::{models::*, Configuration, Connection, SendMail};
use mailpot_tests::init_stderr_logging;
use serde_json::json;
use tempfile::TempDir;
use tower::ServiceExt; // for `oneshot` and `ready`
use super::*;
#[tokio::test]
async fn test_list_router() {
init_stderr_logging();
let tmp_dir = TempDir::new().unwrap();
let db_path = tmp_dir.path().join("mpot.db");
std::fs::copy("../mailpot-tests/for_testing.db", &db_path).unwrap();
let config = Configuration {
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
db_path,
data_path: tmp_dir.path().to_path_buf(),
administrators: vec![],
};
let db = Connection::open_db(config.clone()).unwrap().trusted();
assert!(!db.lists().unwrap().is_empty());
let foo_chat = MailingList {
pk: 1,
name: "foobar chat".into(),
id: "foo-chat".into(),
address: "foo-chat@example.com".into(),
description: None,
archive_url: None,
};
drop(db);
let config = Arc::new(config);
// ------------------------------------------------------------
// all_lists() get total
let response = crate::create_app(config.clone())
.oneshot(
Request::builder()
.uri("/v1/list/")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
let r: GetResponse = serde_json::from_slice(&body).unwrap();
assert_eq!(&r.entries, &[]);
assert_eq!(r.total, 1);
assert_eq!(r.start, 0);
// ------------------------------------------------------------
// all_lists() with count
let response = crate::create_app(config.clone())
.oneshot(
Request::builder()
.uri("/v1/list/?count=20")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
let r: GetResponse = serde_json::from_slice(&body).unwrap();
assert_eq!(&r.entries, &[foo_chat.clone()]);
assert_eq!(r.total, 1);
assert_eq!(r.start, 0);
// ------------------------------------------------------------
// new_list()
let response = crate::create_app(config.clone())
.oneshot(
Request::builder()
.uri("/v1/list/")
.header("Content-Type", "application/json")
.method(Method::POST)
.body(Body::from(serde_json::to_vec(&json! {{}}).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
// ------------------------------------------------------------
// get_list()
let response = crate::create_app(config.clone())
.oneshot(
Request::builder()
.uri("/v1/list/1/")
.header("Content-Type", "application/json")
.method(Method::GET)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
let r: MailingList = serde_json::from_slice(&body).unwrap();
assert_eq!(&r, &foo_chat);
// ------------------------------------------------------------
// get_list_subs()
let response = crate::create_app(config.clone())
.oneshot(
Request::builder()
.uri("/v1/list/1/subscription/")
.header("Content-Type", "application/json")
.method(Method::GET)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
let r: Vec<ListSubscription> = serde_json::from_slice(&body).unwrap();
assert_eq!(
&r,
&[ListSubscription {
pk: 1,
list: 1,
address: "user@example.com".to_string(),
name: Some("Name".to_string()),
account: None,
enabled: true,
verified: false,
digest: false,
hide_address: false,
receive_duplicates: true,
receive_own_posts: false,
receive_confirmation: true
}]
);
// ------------------------------------------------------------
// new_list_sub()
let response = crate::create_app(config.clone())
.oneshot(
Request::builder()
.uri("/v1/list/1/subscription/")
.header("Content-Type", "application/json")
.method(Method::POST)
.body(Body::from(serde_json::to_vec(&json! {{}}).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
// ------------------------------------------------------------
// get_list_owner()
let response = crate::create_app(config.clone())
.oneshot(
Request::builder()
.uri("/v1/list/1/owner/")
.header("Content-Type", "application/json")
.method(Method::GET)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
let r: Vec<ListOwner> = serde_json::from_slice(&body).unwrap();
assert_eq!(&r, &[]);
// ------------------------------------------------------------
// new_list_owner()
let response = crate::create_app(config.clone())
.oneshot(
Request::builder()
.uri("/v1/list/1/owner/")
.header("Content-Type", "application/json")
.method(Method::POST)
.body(Body::from(serde_json::to_vec(&json! {{}}).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
}