Author: Manos Pitsidianakis [manos@pitsidianak.is]
Hash: 1af45795194b8c0df139c1396ba02c7d68b0096e
Timestamp: Sat, 13 May 2023 20:42:55 +0000 (1 year ago)

+364 -55 +/-6 browse
rest-http: add unit tests
1diff --git a/Cargo.lock b/Cargo.lock
2index 8ceffe9..cf366b1 100644
3--- a/Cargo.lock
4+++ b/Cargo.lock
5 @@ -1975,19 +1975,24 @@ dependencies = [
6 "assert-json-diff",
7 "async-trait",
8 "axum",
9+ "axum-extra",
10 "bcrypt",
11 "config",
12 "http",
13+ "hyper",
14 "lazy_static",
15 "log",
16 "mailpot",
17+ "mailpot-tests",
18 "mailpot-web",
19 "reqwest",
20 "serde",
21 "serde_json",
22 "stderrlog",
23+ "tempfile",
24 "thiserror",
25 "tokio",
26+ "tower",
27 "tower-http 0.4.0",
28 ]
29
30 diff --git a/mailpot-tests/for_testing.db b/mailpot-tests/for_testing.db
31new file mode 100644
32index 0000000..70ccedb
33 Binary files /dev/null and b/mailpot-tests/for_testing.db differ
34 diff --git a/rest-http/Cargo.toml b/rest-http/Cargo.toml
35index b237918..e79f615 100644
36--- a/rest-http/Cargo.toml
37+++ b/rest-http/Cargo.toml
38 @@ -18,6 +18,7 @@ path = "src/main.rs"
39 [dependencies]
40 async-trait = "0.1"
41 axum = { version = "0.6", features = ["headers"] }
42+ axum-extra = { version = "^0.7", features = ["typed-routing"] }
43 #jsonwebtoken = "8.3"
44 bcrypt = "0.14"
45 config = "0.13"
46 @@ -41,4 +42,8 @@ tower-http = { version = "0.4", features = [
47
48 [dev-dependencies]
49 assert-json-diff = "2"
50+ hyper = { version = "0.14" }
51+ mailpot-tests = { version = "^0.1", path = "../mailpot-tests" }
52 reqwest = { version = "0.11", features = ["json"] }
53+ tempfile = "3.3"
54+ tower = { version = "^0.4" }
55 diff --git a/rest-http/src/lib.rs b/rest-http/src/lib.rs
56index 6bc9fc6..3dd161a 100644
57--- a/rest-http/src/lib.rs
58+++ b/rest-http/src/lib.rs
59 @@ -26,3 +26,26 @@ pub use mailpot::{models::*, Configuration, Connection};
60 pub mod errors;
61 pub mod routes;
62 pub mod settings;
63+
64+ use tower_http::{
65+ compression::CompressionLayer, cors::CorsLayer, propagate_header::PropagateHeaderLayer,
66+ sensitive_headers::SetSensitiveHeadersLayer,
67+ };
68+
69+ pub fn create_app(conf: Arc<Configuration>) -> Router {
70+ Router::new()
71+ .with_state(conf.clone())
72+ .merge(Router::new().nest("/v1", Router::new().merge(routes::list::create_route(conf))))
73+ .layer(SetSensitiveHeadersLayer::new(std::iter::once(
74+ header::AUTHORIZATION,
75+ )))
76+ // Compress responses
77+ .layer(CompressionLayer::new())
78+ // Propagate `X-Request-Id`s from requests to responses
79+ .layer(PropagateHeaderLayer::new(header::HeaderName::from_static(
80+ "x-request-id",
81+ )))
82+ // CORS configuration. This should probably be more restrictive in
83+ // production.
84+ .layer(CorsLayer::permissive())
85+ }
86 diff --git a/rest-http/src/main.rs b/rest-http/src/main.rs
87index c86a5ef..180de46 100644
88--- a/rest-http/src/main.rs
89+++ b/rest-http/src/main.rs
90 @@ -1,25 +1,9 @@
91 use mailpot_http::{settings::SETTINGS, *};
92- use tower_http::{
93- compression::CompressionLayer, cors::CorsLayer, propagate_header::PropagateHeaderLayer,
94- sensitive_headers::SetSensitiveHeadersLayer,
95- };
96
97- use crate::routes;
98+ use crate::create_app;
99
100 #[tokio::main]
101 async fn main() {
102- let app = create_app().await;
103-
104- let port = SETTINGS.server.port;
105- let address = SocketAddr::from(([127, 0, 0, 1], port));
106-
107- info!("Server listening on {}", &address);
108- axum::Server::bind(&address)
109- .serve(app.into_make_service())
110- .await
111- .expect("Failed to start server");
112- }
113- pub async fn create_app() -> Router {
114 let config_path = std::env::args()
115 .nth(1)
116 .expect("Expected configuration file path as first argument.");
117 @@ -31,20 +15,14 @@ pub async fn create_app() -> Router {
118 .init()
119 .unwrap();
120 let conf = Arc::new(Configuration::from_file(config_path).unwrap());
121+ let app = create_app(conf);
122
123- Router::new()
124- .with_state(conf.clone())
125- .merge(Router::new().nest("/v1", Router::new().merge(routes::list::create_route(conf))))
126- .layer(SetSensitiveHeadersLayer::new(std::iter::once(
127- header::AUTHORIZATION,
128- )))
129- // Compress responses
130- .layer(CompressionLayer::new())
131- // Propagate `X-Request-Id`s from requests to responses
132- .layer(PropagateHeaderLayer::new(header::HeaderName::from_static(
133- "x-request-id",
134- )))
135- // CORS configuration. This should probably be more restrictive in
136- // production.
137- .layer(CorsLayer::permissive())
138+ let port = SETTINGS.server.port;
139+ let address = SocketAddr::from(([127, 0, 0, 1], port));
140+
141+ info!("Server listening on {}", &address);
142+ axum::Server::bind(&address)
143+ .serve(app.into_make_service())
144+ .await
145+ .expect("Failed to start server");
146 }
147 diff --git a/rest-http/src/routes/list.rs b/rest-http/src/routes/list.rs
148index 6aa7f7d..c9947f0 100644
149--- a/rest-http/src/routes/list.rs
150+++ b/rest-http/src/routes/list.rs
151 @@ -1,28 +1,48 @@
152 use std::sync::Arc;
153
154 pub use axum::extract::{Path, Query, State};
155- use axum::{
156- http::StatusCode,
157- routing::{get, post},
158- Json, Router,
159- };
160- use mailpot_web::{typed_paths::*, ResponseError, RouterExt};
161+ use axum::{http::StatusCode, Json, Router};
162+ use mailpot_web::{typed_paths::*, ResponseError, RouterExt, TypedPath};
163 use serde::{Deserialize, Serialize};
164
165 use crate::*;
166
167+ #[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
168+ #[typed_path("/list/")]
169+ pub struct ListsPath;
170+
171+ #[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
172+ #[typed_path("/list/:id/owner/")]
173+ pub struct ListOwnerPath(pub ListPathIdentifier);
174+
175+ #[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
176+ #[typed_path("/list/:id/subscription/")]
177+ pub struct ListSubscriptionPath(pub ListPathIdentifier);
178+
179 pub fn create_route(conf: Arc<Configuration>) -> Router {
180 Router::new()
181- .route("/list/", get(all_lists))
182- .route("/list/", post(post_list))
183+ .typed_get(all_lists)
184+ .typed_post(new_list)
185 .typed_get(get_list)
186+ .typed_post({
187+ move |_: ListPath| async move {
188+ Err::<(), ResponseError>(mailpot_web::ResponseError::new(
189+ "Invalid method".to_string(),
190+ StatusCode::BAD_REQUEST,
191+ ))
192+ }
193+ })
194+ .typed_get(get_list_owner)
195+ .typed_post(new_list_owner)
196+ .typed_get(get_list_subs)
197+ .typed_post(new_list_sub)
198 .with_state(conf)
199 }
200
201 async fn get_list(
202 ListPath(id): ListPath,
203 State(state): State<Arc<Configuration>>,
204- ) -> Result<Json<DbVal<MailingList>>, ResponseError> {
205+ ) -> Result<Json<MailingList>, ResponseError> {
206 let db = Connection::open_db(Configuration::clone(&state))?;
207 let Some(list) = (match id {
208 ListPathIdentifier::Pk(id) => db.list(id)?,
209 @@ -33,10 +53,11 @@ async fn get_list(
210 StatusCode::NOT_FOUND,
211 ));
212 };
213- Ok(Json(list))
214+ Ok(Json(list.into_inner()))
215 }
216
217 async fn all_lists(
218+ _: ListsPath,
219 Query(GetRequest {
220 filter: _,
221 count,
222 @@ -61,7 +82,12 @@ async fn all_lists(
223 }));
224 };
225 let offset = page * count;
226- let res: Vec<_> = lists_values.into_iter().skip(offset).take(count).collect();
227+ let res: Vec<_> = lists_values
228+ .into_iter()
229+ .skip(offset)
230+ .take(count)
231+ .map(DbVal::into_inner)
232+ .collect();
233
234 Ok(Json(GetResponse {
235 total: res.len(),
236 @@ -70,18 +96,16 @@ async fn all_lists(
237 }))
238 }
239
240- async fn post_list(
241- State(state): State<Arc<Configuration>>,
242- Json(_body): Json<GetRequest>,
243+ async fn new_list(
244+ _: ListsPath,
245+ State(_state): State<Arc<Configuration>>,
246+ //Json(_body): Json<GetRequest>,
247 ) -> Result<Json<()>, ResponseError> {
248- let _db = Connection::open_db(Configuration::clone(&state))?;
249- // let password_hash = list::hash_password(body.password).await?;
250- // let list = list::new(body.name, body.email, password_hash);
251- // let list = list::create(list).await?;
252- // let res = Publiclist::from(list);
253- //
254-
255- Ok(Json(()))
256+ // TODO create new list
257+ Err(mailpot_web::ResponseError::new(
258+ "Not allowed".to_string(),
259+ StatusCode::UNAUTHORIZED,
260+ ))
261 }
262
263 #[derive(Debug, Serialize, Deserialize)]
264 @@ -101,7 +125,281 @@ struct GetRequest {
265
266 #[derive(Debug, Serialize, Deserialize)]
267 struct GetResponse {
268- entries: Vec<DbVal<MailingList>>,
269+ entries: Vec<MailingList>,
270 total: usize,
271 start: usize,
272 }
273+
274+ async fn get_list_owner(
275+ ListOwnerPath(id): ListOwnerPath,
276+ State(state): State<Arc<Configuration>>,
277+ ) -> Result<Json<Vec<ListOwner>>, ResponseError> {
278+ let db = Connection::open_db(Configuration::clone(&state))?;
279+ let owners = match id {
280+ ListPathIdentifier::Pk(id) => db.list_owners(id)?,
281+ ListPathIdentifier::Id(id) => {
282+ if let Some(owners) = db.list_by_id(id)?.map(|l| db.list_owners(l.pk())) {
283+ owners?
284+ } else {
285+ return Err(mailpot_web::ResponseError::new(
286+ "Not found".to_string(),
287+ StatusCode::NOT_FOUND,
288+ ));
289+ }
290+ }
291+ };
292+ Ok(Json(owners.into_iter().map(DbVal::into_inner).collect()))
293+ }
294+
295+ async fn new_list_owner(
296+ ListOwnerPath(_id): ListOwnerPath,
297+ State(_state): State<Arc<Configuration>>,
298+ //Json(_body): Json<GetRequest>,
299+ ) -> Result<Json<Vec<ListOwner>>, ResponseError> {
300+ Err(mailpot_web::ResponseError::new(
301+ "Not allowed".to_string(),
302+ StatusCode::UNAUTHORIZED,
303+ ))
304+ }
305+
306+ async fn get_list_subs(
307+ ListSubscriptionPath(id): ListSubscriptionPath,
308+ State(state): State<Arc<Configuration>>,
309+ ) -> Result<Json<Vec<ListSubscription>>, ResponseError> {
310+ let db = Connection::open_db(Configuration::clone(&state))?;
311+ let subs = match id {
312+ ListPathIdentifier::Pk(id) => db.list_subscriptions(id)?,
313+ ListPathIdentifier::Id(id) => {
314+ if let Some(v) = db.list_by_id(id)?.map(|l| db.list_subscriptions(l.pk())) {
315+ v?
316+ } else {
317+ return Err(mailpot_web::ResponseError::new(
318+ "Not found".to_string(),
319+ StatusCode::NOT_FOUND,
320+ ));
321+ }
322+ }
323+ };
324+ Ok(Json(subs.into_iter().map(DbVal::into_inner).collect()))
325+ }
326+
327+ async fn new_list_sub(
328+ ListSubscriptionPath(_id): ListSubscriptionPath,
329+ State(_state): State<Arc<Configuration>>,
330+ //Json(_body): Json<GetRequest>,
331+ ) -> Result<Json<ListSubscription>, ResponseError> {
332+ Err(mailpot_web::ResponseError::new(
333+ "Not allowed".to_string(),
334+ StatusCode::UNAUTHORIZED,
335+ ))
336+ }
337+
338+ #[cfg(test)]
339+ mod tests {
340+
341+ use axum::{
342+ body::Body,
343+ http::{method::Method, Request, StatusCode},
344+ };
345+ use mailpot::{models::*, Configuration, Connection, SendMail};
346+ use mailpot_tests::init_stderr_logging;
347+ use serde_json::json;
348+ use tempfile::TempDir;
349+ use tower::ServiceExt; // for `oneshot` and `ready`
350+
351+ use super::*;
352+
353+ #[tokio::test]
354+ async fn test_list_router() {
355+ init_stderr_logging();
356+
357+ let tmp_dir = TempDir::new().unwrap();
358+
359+ let db_path = tmp_dir.path().join("mpot.db");
360+ std::fs::copy("../mailpot-tests/for_testing.db", &db_path).unwrap();
361+ let config = Configuration {
362+ send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
363+ db_path,
364+ data_path: tmp_dir.path().to_path_buf(),
365+ administrators: vec![],
366+ };
367+
368+ let db = Connection::open_db(config.clone()).unwrap().trusted();
369+ assert!(!db.lists().unwrap().is_empty());
370+ let foo_chat = MailingList {
371+ pk: 1,
372+ name: "foobar chat".into(),
373+ id: "foo-chat".into(),
374+ address: "foo-chat@example.com".into(),
375+ description: None,
376+ archive_url: None,
377+ };
378+ drop(db);
379+
380+ let config = Arc::new(config);
381+
382+ // ------------------------------------------------------------
383+ // all_lists() get total
384+
385+ let response = crate::create_app(config.clone())
386+ .oneshot(
387+ Request::builder()
388+ .uri("/v1/list/")
389+ .body(Body::empty())
390+ .unwrap(),
391+ )
392+ .await
393+ .unwrap();
394+
395+ assert_eq!(response.status(), StatusCode::OK);
396+
397+ let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
398+ let r: GetResponse = serde_json::from_slice(&body).unwrap();
399+
400+ assert_eq!(&r.entries, &[]);
401+ assert_eq!(r.total, 1);
402+ assert_eq!(r.start, 0);
403+
404+ // ------------------------------------------------------------
405+ // all_lists() with count
406+
407+ let response = crate::create_app(config.clone())
408+ .oneshot(
409+ Request::builder()
410+ .uri("/v1/list/?count=20")
411+ .body(Body::empty())
412+ .unwrap(),
413+ )
414+ .await
415+ .unwrap();
416+ assert_eq!(response.status(), StatusCode::OK);
417+
418+ let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
419+ let r: GetResponse = serde_json::from_slice(&body).unwrap();
420+
421+ assert_eq!(&r.entries, &[foo_chat.clone()]);
422+ assert_eq!(r.total, 1);
423+ assert_eq!(r.start, 0);
424+
425+ // ------------------------------------------------------------
426+ // new_list()
427+
428+ let response = crate::create_app(config.clone())
429+ .oneshot(
430+ Request::builder()
431+ .uri("/v1/list/")
432+ .header("Content-Type", "application/json")
433+ .method(Method::POST)
434+ .body(Body::from(serde_json::to_vec(&json! {{}}).unwrap()))
435+ .unwrap(),
436+ )
437+ .await
438+ .unwrap();
439+ assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
440+
441+ // ------------------------------------------------------------
442+ // get_list()
443+
444+ let response = crate::create_app(config.clone())
445+ .oneshot(
446+ Request::builder()
447+ .uri("/v1/list/1/")
448+ .header("Content-Type", "application/json")
449+ .method(Method::GET)
450+ .body(Body::empty())
451+ .unwrap(),
452+ )
453+ .await
454+ .unwrap();
455+ assert_eq!(response.status(), StatusCode::OK);
456+ let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
457+ let r: MailingList = serde_json::from_slice(&body).unwrap();
458+ assert_eq!(&r, &foo_chat);
459+
460+ // ------------------------------------------------------------
461+ // get_list_subs()
462+
463+ let response = crate::create_app(config.clone())
464+ .oneshot(
465+ Request::builder()
466+ .uri("/v1/list/1/subscription/")
467+ .header("Content-Type", "application/json")
468+ .method(Method::GET)
469+ .body(Body::empty())
470+ .unwrap(),
471+ )
472+ .await
473+ .unwrap();
474+ assert_eq!(response.status(), StatusCode::OK);
475+ let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
476+ let r: Vec<ListSubscription> = serde_json::from_slice(&body).unwrap();
477+ assert_eq!(
478+ &r,
479+ &[ListSubscription {
480+ pk: 1,
481+ list: 1,
482+ address: "user@example.com".to_string(),
483+ name: Some("Name".to_string()),
484+ account: None,
485+ enabled: true,
486+ verified: false,
487+ digest: false,
488+ hide_address: false,
489+ receive_duplicates: true,
490+ receive_own_posts: false,
491+ receive_confirmation: true
492+ }]
493+ );
494+
495+ // ------------------------------------------------------------
496+ // new_list_sub()
497+
498+ let response = crate::create_app(config.clone())
499+ .oneshot(
500+ Request::builder()
501+ .uri("/v1/list/1/subscription/")
502+ .header("Content-Type", "application/json")
503+ .method(Method::POST)
504+ .body(Body::from(serde_json::to_vec(&json! {{}}).unwrap()))
505+ .unwrap(),
506+ )
507+ .await
508+ .unwrap();
509+ assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
510+
511+ // ------------------------------------------------------------
512+ // get_list_owner()
513+
514+ let response = crate::create_app(config.clone())
515+ .oneshot(
516+ Request::builder()
517+ .uri("/v1/list/1/owner/")
518+ .header("Content-Type", "application/json")
519+ .method(Method::GET)
520+ .body(Body::empty())
521+ .unwrap(),
522+ )
523+ .await
524+ .unwrap();
525+ assert_eq!(response.status(), StatusCode::OK);
526+ let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
527+ let r: Vec<ListOwner> = serde_json::from_slice(&body).unwrap();
528+ assert_eq!(&r, &[]);
529+
530+ // ------------------------------------------------------------
531+ // new_list_owner()
532+
533+ let response = crate::create_app(config.clone())
534+ .oneshot(
535+ Request::builder()
536+ .uri("/v1/list/1/owner/")
537+ .header("Content-Type", "application/json")
538+ .method(Method::POST)
539+ .body(Body::from(serde_json::to_vec(&json! {{}}).unwrap()))
540+ .unwrap(),
541+ )
542+ .await
543+ .unwrap();
544+ assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
545+ }
546+ }