use crate::auth::extractor::AuthUser; use crate::error::AppError; use crate::schema::feed_item::{id, read}; use crate::{ database::establish_connection, json_serialization::read_feed_item::ReadItem, models::feed_item::rss_feed_item::FeedItem, schema::{feed, feed_item}, }; use actix_web::{web, HttpRequest, HttpResponse, Responder}; use diesel::RunQueryDsl; use diesel::{ExpressionMethods, OptionalExtension, QueryDsl}; pub async fn mark_read( _req: HttpRequest, path: web::Path, auth_user: AuthUser, ) -> Result { let mut connection = establish_connection(); log::info!("Id: {}", path.id); // Join through to `feed` so we can confirm the item belongs to the caller // before mutating it. "Doesn't exist" and "not yours" both return 404. let owned_item: Option<(FeedItem, i32)> = feed_item::table .inner_join(feed::table) .filter(id.eq(path.id)) .select((feed_item::all_columns, feed::user_id)) .first(&mut connection) .optional()?; let feed_item = match owned_item { Some((feed_item, owner_id)) if owner_id == auth_user.0 => feed_item, _ => return Ok(HttpResponse::NotFound().finish()), }; let result = diesel::update(&feed_item) .set(read.eq(true)) .execute(&mut connection)?; log::info!("Mark as read: {:?}", result); Ok(HttpResponse::Ok().finish()) } #[cfg(test)] mod tests { use actix_service::Service; use actix_web::http::StatusCode; use actix_web::{test, web, App, HttpMessage}; use diesel::prelude::*; use super::mark_read; use crate::auth::extractor::AuthUser; use crate::database::establish_connection; use crate::models::feed_item::rss_feed_item::FeedItem; use crate::schema::feed_item; use crate::test_helpers::{ delete_feed, delete_feed_item, delete_user, insert_feed, insert_feed_item, insert_user, }; #[actix_web::test] async fn mark_read_flips_the_read_flag() { let mut connection = establish_connection(); let user = insert_user(&mut connection, "secret"); let feed = insert_feed(&mut connection, user.id); let item = insert_feed_item(&mut connection, feed.id, false); let user_id = user.id; let app = test::init_service( App::new() .wrap_fn(move |req, srv| { req.extensions_mut().insert(AuthUser(user_id)); srv.call(req) }) .route("/read/{id}", web::put().to(mark_read)), ) .await; let req = test::TestRequest::put() .uri(&format!("/read/{}", item.id)) .to_request(); let resp = test::call_service(&app, req).await; assert_eq!(StatusCode::OK, resp.status()); let updated: FeedItem = feed_item::table .find(item.id) .first(&mut connection) .unwrap(); assert!(updated.read); delete_feed_item(&mut connection, item.id); delete_feed(&mut connection, feed.id); delete_user(&mut connection, user.id); } #[actix_web::test] async fn mark_read_returns_not_found_for_unknown_id() { let app = test::init_service( App::new() .wrap_fn(move |req, srv| { req.extensions_mut().insert(AuthUser(1)); srv.call(req) }) .route("/read/{id}", web::put().to(mark_read)), ) .await; let req = test::TestRequest::put().uri("/read/999999999").to_request(); let resp = test::call_service(&app, req).await; assert_eq!(StatusCode::NOT_FOUND, resp.status()); } #[actix_web::test] async fn mark_read_rejects_other_users_item() { let mut connection = establish_connection(); let user_a = insert_user(&mut connection, "secret"); let user_b = insert_user(&mut connection, "secret"); let feed_b = insert_feed(&mut connection, user_b.id); let item_b = insert_feed_item(&mut connection, feed_b.id, false); let user_a_id = user_a.id; let app = test::init_service( App::new() .wrap_fn(move |req, srv| { req.extensions_mut().insert(AuthUser(user_a_id)); srv.call(req) }) .route("/read/{id}", web::put().to(mark_read)), ) .await; let req = test::TestRequest::put() .uri(&format!("/read/{}", item_b.id)) .to_request(); let resp = test::call_service(&app, req).await; assert_eq!(StatusCode::NOT_FOUND, resp.status()); let updated: FeedItem = feed_item::table .find(item_b.id) .first(&mut connection) .unwrap(); assert!(!updated.read); delete_feed_item(&mut connection, item_b.id); delete_feed(&mut connection, feed_b.id); delete_user(&mut connection, user_a.id); delete_user(&mut connection, user_b.id); } }