mailpot/rest-http/src/routes/list.rs

420 lines
13 KiB
Rust

use std::sync::Arc;
pub use axum::extract::{Path, Query, State};
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()
.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<MailingList>, ResponseError> {
let db = Connection::open_db(Configuration::clone(&state))?;
let Some(list) = (match id {
ListPathIdentifier::Pk(id) => db.list(id)?,
ListPathIdentifier::Id(id) => db.list_by_id(id)?,
}) else {
return Err(mailpot_web::ResponseError::new(
"Not found".to_string(),
StatusCode::NOT_FOUND,
));
};
Ok(Json(list.into_inner()))
}
async fn all_lists(
_: ListsPath,
Query(GetRequest {
filter: _,
count,
page,
}): Query<GetRequest>,
State(state): State<Arc<Configuration>>,
) -> Result<Json<GetResponse>, ResponseError> {
let db = Connection::open_db(Configuration::clone(&state))?;
let lists_values = db.lists()?;
let page = page.unwrap_or(0);
let Some(count) = count else {
let mut stmt = db
.connection
.prepare("SELECT count(*) FROM list;")?;
return Ok(Json(GetResponse {
entries: vec![],
total: stmt.query_row([], |row| {
let count: usize = row.get(0)?;
Ok(count)
})?,
start: 0,
}));
};
let offset = page * count;
let res: Vec<_> = lists_values
.into_iter()
.skip(offset)
.take(count)
.map(DbVal::into_inner)
.collect();
Ok(Json(GetResponse {
total: res.len(),
start: offset,
entries: res,
}))
}
async fn new_list(
_: ListsPath,
State(_state): State<Arc<Configuration>>,
//Json(_body): Json<GetRequest>,
) -> Result<Json<()>, ResponseError> {
// TODO create new list
Err(mailpot_web::ResponseError::new(
"Not allowed".to_string(),
StatusCode::UNAUTHORIZED,
))
}
#[derive(Debug, Serialize, Deserialize)]
enum GetFilter {
Pk(i64),
Address(String),
Id(String),
Name(String),
}
#[derive(Debug, Serialize, Deserialize)]
struct GetRequest {
filter: Option<GetFilter>,
count: Option<usize>,
page: Option<usize>,
}
#[derive(Debug, Serialize, Deserialize)]
struct GetResponse {
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 mut perms = std::fs::metadata(&db_path).unwrap().permissions();
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
std::fs::set_permissions(&db_path, perms).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(),
topics: vec![],
description: None,
archive_url: None,
};
assert_eq!(&db.lists().unwrap().remove(0).into_inner(), &foo_chat);
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: Some(1),
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,
&[ListOwner {
pk: 1,
list: 1,
address: "user@example.com".into(),
name: None
}]
);
// ------------------------------------------------------------
// 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);
}
}