rest-http: add unit tests
parent
19860d2d87
commit
1af4579519
|
@ -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.
|
@ -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" }
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue