use crate::{middleware::AuthenticatedAdminMiddleware, model::Article, AppState}; use actix_web::{ delete, get, post, put, web::{Data, Form, Json, Path}, HttpRequest, HttpResponse, Responder, }; use chrono::Utc; use futures::stream::StreamExt; use serde::{Deserialize, Serialize}; use wither::{ bson::{doc, oid::ObjectId, DateTime}, mongodb::Collection, prelude::Model, }; #[derive(Deserialize, Serialize)] pub struct ArticleTitleFormData { pub title: String, } fn get_collection(app_state: &AppState) -> Collection<Article> { app_state.db.collection_with_type::<Article>("articles") } #[post("/post-article")] pub async fn post_article( app_state: Data<AppState>, article_data: Json<Article>, middleware: Data<AuthenticatedAdminMiddleware<'_>>, req: HttpRequest, ) -> impl Responder { if middleware.exec(&app_state, &req, None).await.is_err() { return HttpResponse::Unauthorized().finish(); } let mut article_data = article_data.into_inner(); article_data.date = Some(DateTime(Utc::now())); match get_collection(&app_state) .insert_one(article_data, None) .await { Ok(res) => HttpResponse::Created().json(res), Err(e) => { HttpResponse::InternalServerError().body(format!("Error inserting new article {:?}", e)) } } } #[put("/update-article/{article_id}")] pub async fn update_article( app_state: Data<AppState>, article_data: Json<Article>, middleware: Data<AuthenticatedAdminMiddleware<'_>>, article_id: Path<String>, req: HttpRequest, ) -> impl Responder { if middleware.exec(&app_state, &req, None).await.is_err() { return HttpResponse::Unauthorized().finish(); } let article_id = match ObjectId::with_string(&article_id.into_inner()) { Ok(id) => id, Err(_) => { return HttpResponse::BadRequest() .body("Failed to convert article_id to ObjectId. String may be malformed") } }; let mut article_data = article_data.into_inner(); article_data.date = Some(DateTime(Utc::now())); match get_collection(&app_state) .find_one_and_replace(doc! {"_id": &article_id}, article_data, None) .await { Ok(res) => HttpResponse::Ok().json(res.unwrap()), Err(_) => HttpResponse::InternalServerError().finish(), } } #[delete("/delete-article/{article_id}")] pub async fn delete_article( app_state: Data<AppState>, middleware: Data<AuthenticatedAdminMiddleware<'_>>, article_id: Path<String>, req: HttpRequest, ) -> impl Responder { if middleware.exec(&app_state, &req, None).await.is_err() { return HttpResponse::Unauthorized().finish(); } let article_id = match ObjectId::with_string(&article_id.into_inner()) { Ok(id) => id, Err(_) => { return HttpResponse::BadRequest() .body("Failed to convert article_id to ObjectId. String may be malformed") } }; match get_collection(&app_state) .find_one_and_delete(doc! {"_id": &article_id}, None) .await { Ok(_) => HttpResponse::Accepted().body("Article was deleted"), Err(e) => HttpResponse::InternalServerError().body(&format!("{:?}", e)), } } #[get("/articles/{category}")] pub async fn get_articles_by_category( app_state: Data<AppState>, category: Path<String>, ) -> impl Responder { match get_collection(&app_state) .find(doc! {"category": category.into_inner()}, None) .await { Ok(mut cursor) => { let mut results: Vec<Article> = Vec::new(); while let Some(result) = cursor.next().await { match result { Ok(article) => { results.push(article); } Err(_) => { return HttpResponse::InternalServerError().finish(); } } } HttpResponse::Ok().json(results) } Err(_) => HttpResponse::InternalServerError().finish(), } } #[get("/article/{article_id}")] pub async fn get_article(app_state: Data<AppState>, article_id: Path<String>) -> impl Responder { let article_id = match ObjectId::with_string(&article_id.into_inner()) { Ok(id) => id, Err(_) => { return HttpResponse::BadRequest() .body("Failed to convert article_id to ObjectId. String may be malformed") } }; match Article::find_one(&app_state.db, doc! {"_id":&article_id}, None).await { Ok(art) => { if art.is_none() { return HttpResponse::NotFound().body("Article was not found"); } HttpResponse::Ok().json(art) } Err(e) => HttpResponse::InternalServerError().body(format!("Database error: {:#?}", e)), } } #[post("/article-by-title")] pub async fn get_article_by_title( app_state: Data<AppState>, form_data: Form<ArticleTitleFormData>, ) -> impl Responder { let title = form_data.into_inner().title; match Article::find_one(&app_state.db, doc! {"title":title}, None).await { Ok(art) => { if art.is_none() { return HttpResponse::NotFound().body("Article was not found"); } HttpResponse::Ok().json(art) } Err(e) => HttpResponse::InternalServerError().body(format!("Database error: {:#?}", e)), } } #[get("/articles")] pub async fn get_all_articles(app_state: Data<AppState>) -> impl Responder { match get_collection(&app_state).find(None, None).await { Ok(mut cursor) => { let mut results: Vec<Article> = Vec::new(); while let Some(result) = cursor.next().await { match result { Ok(article) => { results.push(article); } Err(_) => { return HttpResponse::InternalServerError().finish(); } } } HttpResponse::Ok().json(results) } Err(_) => HttpResponse::InternalServerError().finish(), } } /*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ * _______ ______ ______ _______ *@@ * |__ __@ | ____@ / ____@ |__ __@ *@@ * | @ | @__ \_ @_ | @ *@@ * | @ | __@ \ @_ | @ *@@ * | @ | @___ ____\ @ | @ *@@ * |__@ |______@ \______@ |__@ *@@ * *@@ *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@@ *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*@*/ #[cfg(test)] mod test_articles { use super::*; use crate::middleware::get_auth_cookie; use crate::model::{AdminAuthCredentials, Administrator}; use actix_web::{ http::{Method, StatusCode}, test, web::Bytes, App, }; use wither::bson::Bson; async fn insert_test_article( app_state: &AppState, test_article: Article, ) -> Result<(ObjectId, String), String> { let title = test_article.title.to_owned(); match get_collection(&app_state) .insert_one(test_article, None) .await { Ok(inserted) => match inserted.inserted_id { Bson::ObjectId(id) => Ok((id, title)), _ => Err(String::from("Failed to parse inserted_id")), }, Err(e) => Err(format!("{:?}", e)), } } async fn delete_test_article( app_state: &AppState, article_id: &ObjectId, ) -> Result<i64, String> { match get_collection(&app_state) .delete_one(doc! {"_id": article_id}, None) .await { Ok(delete_result) => Ok(delete_result.deleted_count), Err(e) => Err(format!("{:?}", e)), } } async fn get_authenticated_admin(app_state: &AppState) -> Administrator { Administrator::authenticated( app_state, AdminAuthCredentials { username: app_state.env.default_admin_username.to_owned(), password: app_state.env.default_admin_password.to_owned(), }, ) .await .unwrap() } #[tokio::test] async fn test_post_article() { dotenv::dotenv().ok(); let app_state = AppState::for_test().await; let mut app = test::init_service( App::new() .app_data(Data::new(app_state.clone())) .app_data(Data::new(AuthenticatedAdminMiddleware::new( "kuadrado-admin-auth", ))) .service(post_article), ) .await; let article = Article::test_article(); let admin_user = get_authenticated_admin(&app_state).await; let req = test::TestRequest::with_uri("/post-article") .method(Method::POST) .header("Content-Type", "application/json") .header("Accept", "text/html") .cookie(get_auth_cookie( "kuadrado-admin-auth", app_state .encryption .decrypt(&admin_user.auth_token.unwrap()) .to_owned(), )) .set_payload(Bytes::from(serde_json::to_string(&article).unwrap())) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::CREATED); let find_inserted = Article::find_one(&app_state.db, doc! {"title": &article.title}, None) .await .unwrap(); assert!(find_inserted.is_some()); assert_eq!(find_inserted.unwrap().title, article.title); get_collection(&app_state) .delete_one(doc! {"title": article.title}, None) .await .unwrap(); } #[tokio::test] async fn test_post_article_unauthorized() { dotenv::dotenv().ok(); let app_state = AppState::for_test().await; let mut app = test::init_service( App::new() .app_data(Data::new(app_state.clone())) .app_data(Data::new(AuthenticatedAdminMiddleware::new( "kuadrado-admin-auth", ))) .service(post_article), ) .await; let article = Article::test_article(); let req = test::TestRequest::with_uri("/post-article") .method(Method::POST) .header("Content-Type", "application/json") .header("Accept", "text/html") .cookie(get_auth_cookie( "wrong-cookie", app_state.encryption.random_ascii_lc_string(32), )) .set_payload(Bytes::from(serde_json::to_string(&article).unwrap())) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_update_article() { dotenv::dotenv().ok(); let app_state = AppState::for_test().await; let mut app = test::init_service( App::new() .app_data(Data::new(app_state.clone())) .app_data(Data::new(AuthenticatedAdminMiddleware::new( "kuadrado-admin-auth", ))) .service(update_article), ) .await; let mut article = Article::test_article(); let (article_id, _) = insert_test_article(&app_state, article.clone()) .await .unwrap(); article.title = "changed title".to_string(); let admin_user = get_authenticated_admin(&app_state).await; let req = test::TestRequest::with_uri( format!("/update-article/{}", article_id.to_hex()).as_str(), ) .method(Method::PUT) .header("Content-Type", "application/json") .header("Accept", "text/html") .cookie(get_auth_cookie( "kuadrado-admin-auth", app_state .encryption .decrypt(&admin_user.auth_token.unwrap()) .to_owned(), )) .set_payload(Bytes::from(serde_json::to_string(&article).unwrap())) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::OK); let find_inserted = Article::find_one(&app_state.db, doc! {"_id": &article_id}, None) .await .unwrap(); assert!(find_inserted.is_some()); assert_eq!(find_inserted.unwrap().title, "changed title"); let del_count = delete_test_article(&app_state, &article_id).await.unwrap(); assert_eq!(del_count, 1); } #[tokio::test] async fn test_update_article_unauthorized() { dotenv::dotenv().ok(); let app_state = AppState::for_test().await; let mut app = test::init_service( App::new() .app_data(Data::new(app_state.clone())) .app_data(Data::new(AuthenticatedAdminMiddleware::new( "kuadrado-admin-auth", ))) .service(update_article), ) .await; let article = Article::test_article(); let req = test::TestRequest::with_uri( format!("/update-article/{}", ObjectId::new().to_hex()).as_str(), ) .method(Method::PUT) .header("Content-Type", "application/json") .header("Accept", "text/html") .cookie(get_auth_cookie( "wrong-cookie", app_state.encryption.random_ascii_lc_string(32), )) .set_payload(Bytes::from(serde_json::to_string(&article).unwrap())) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_delete_article() { dotenv::dotenv().ok(); let app_state = AppState::for_test().await; let mut app = test::init_service( App::new() .app_data(Data::new(app_state.clone())) .app_data(Data::new(AuthenticatedAdminMiddleware::new( "kuadrado-admin-auth", ))) .service(delete_article), ) .await; let article = Article::test_article(); let (article_id, _) = insert_test_article(&app_state, article.clone()) .await .unwrap(); let admin_user = get_authenticated_admin(&app_state).await; let req = test::TestRequest::with_uri( format!("/delete-article/{}", article_id.to_hex()).as_str(), ) .method(Method::DELETE) .cookie(get_auth_cookie( "kuadrado-admin-auth", app_state .encryption .decrypt(&admin_user.auth_token.unwrap()) .to_owned(), )) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::ACCEPTED); let find_inserted = Article::find_one(&app_state.db, doc! {"_id": &article_id}, None) .await .unwrap(); assert!(find_inserted.is_none()); } #[tokio::test] async fn test_delete_article_unauthorized() { dotenv::dotenv().ok(); let app_state = AppState::for_test().await; let mut app = test::init_service( App::new() .app_data(Data::new(app_state.clone())) .app_data(Data::new(AuthenticatedAdminMiddleware::new( "kuadrado-admin-auth", ))) .service(delete_article), ) .await; let article = Article::test_article(); let req = test::TestRequest::with_uri( format!("/delete-article/{}", ObjectId::new().to_hex()).as_str(), ) .method(Method::DELETE) .cookie(get_auth_cookie( "wrong-cookie", app_state.encryption.random_ascii_lc_string(32), )) .set_payload(Bytes::from(serde_json::to_string(&article).unwrap())) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_get_article() { dotenv::dotenv().ok(); let app_state = AppState::for_test().await; let mut app = test::init_service( App::new() .app_data(Data::new(app_state.clone())) .service(get_article), ) .await; let article = Article::test_article(); let (article_id, article_title) = insert_test_article(&app_state, article.clone()) .await .unwrap(); let req = test::TestRequest::with_uri(format!("/article/{}", article_id.to_hex()).as_str()) .header("Accept", "application/json") .method(Method::GET) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::OK); let result: Article = test::read_body_json(resp).await; assert_eq!(result.title, article_title); let del_count = delete_test_article(&app_state, &article_id).await.unwrap(); assert_eq!(del_count, 1); } #[tokio::test] async fn test_get_article_by_title() { dotenv::dotenv().ok(); let app_state = AppState::for_test().await; let mut app = test::init_service( App::new() .app_data(Data::new(app_state.clone())) .service(get_article_by_title), ) .await; let article = Article::test_article(); let (article_id, article_title) = insert_test_article(&app_state, article.clone()) .await .unwrap(); let req = test::TestRequest::with_uri("/article-by-title") .header("Accept", "application/json") .method(Method::POST) .set_form(&ArticleTitleFormData { title: article_title.to_owned(), }) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::OK); let result: Article = test::read_body_json(resp).await; assert_eq!(result.title, article_title); let del_count = delete_test_article(&app_state, &article_id).await.unwrap(); assert_eq!(del_count, 1); } #[tokio::test] async fn test_get_articles_by_category() { dotenv::dotenv().ok(); let app_state = AppState::for_test().await; let mut app = test::init_service( App::new() .app_data(Data::new(app_state.clone())) .service(get_articles_by_category), ) .await; let article = Article::test_article(); let (article_id, article_title) = insert_test_article(&app_state, article.clone()) .await .unwrap(); let req = test::TestRequest::with_uri("/articles/testing") .header("Accept", "application/json") .method(Method::GET) .to_request(); let resp = test::call_service(&mut app, req).await; assert_eq!(resp.status(), StatusCode::OK); let results: Vec<Article> = test::read_body_json(resp).await; let find_inserted = results.iter().find(|&art| art.title == article_title); assert!(find_inserted.is_some()); let del_count = delete_test_article(&app_state, &article_id).await.unwrap(); assert_eq!(del_count, 1); } }