From 400648c3d12ed066fa9881f073a9babaa438f165 Mon Sep 17 00:00:00 2001 From: mace Date: Tue, 9 Jun 2026 21:55:07 +0200 Subject: [PATCH] added admin area to delete feeds --- src/json_serialization/feed_info.rs | 24 +++++ src/reader/delete_feed.rs | 124 ++++++++++++++++++++++++++ src/reader/list_feeds.rs | 111 +++++++++++++++++++++++ vue/src/components/AdminFeeds.vue | 133 ++++++++++++++++++++++++++++ vue/src/views/AdminView.vue | 9 ++ 5 files changed, 401 insertions(+) create mode 100644 src/json_serialization/feed_info.rs create mode 100644 src/reader/delete_feed.rs create mode 100644 src/reader/list_feeds.rs create mode 100644 vue/src/components/AdminFeeds.vue create mode 100644 vue/src/views/AdminView.vue diff --git a/src/json_serialization/feed_info.rs b/src/json_serialization/feed_info.rs new file mode 100644 index 0000000..8880e86 --- /dev/null +++ b/src/json_serialization/feed_info.rs @@ -0,0 +1,24 @@ +use actix_web::http::StatusCode; +use actix_web::{HttpResponse, Responder}; +use serde::Serialize; + +#[derive(Serialize)] +pub struct FeedInfo { + pub id: i32, + pub title: String, + pub url: String, +} + +#[derive(Serialize)] +pub struct FeedInfoList { + pub feeds: Vec, +} + +impl Responder for FeedInfoList { + type Body = String; + + fn respond_to(self, _req: &actix_web::HttpRequest) -> actix_web::HttpResponse { + let body = serde_json::to_string(&self).unwrap(); + HttpResponse::with_body(StatusCode::OK, body) + } +} diff --git a/src/reader/delete_feed.rs b/src/reader/delete_feed.rs new file mode 100644 index 0000000..1005d83 --- /dev/null +++ b/src/reader/delete_feed.rs @@ -0,0 +1,124 @@ +use actix_web::{web, HttpResponse}; +use diesel::prelude::*; + +use crate::{ + database::establish_connection, + schema::{feed, feed_item}, +}; + +pub async fn delete_feed(path: web::Path) -> HttpResponse { + let feed_id = path.into_inner(); + let mut connection = establish_connection(); + + let exists = feed::table + .find(feed_id) + .count() + .get_result::(&mut connection) + .unwrap_or(0); + + if exists == 0 { + return HttpResponse::NotFound().finish(); + } + + diesel::delete(feed_item::table.filter(feed_item::feed_id.eq(feed_id))) + .execute(&mut connection) + .ok(); + + diesel::delete(feed::table.filter(feed::id.eq(feed_id))) + .execute(&mut connection) + .ok(); + + HttpResponse::NoContent().finish() +} + +#[cfg(test)] +mod tests { + use actix_web::http::StatusCode; + use actix_web::{test, web, App}; + use diesel::prelude::*; + + use super::delete_feed; + use crate::database::establish_connection; + use crate::schema::{feed, feed_item}; + use crate::test_helpers::{ + delete_feed as cleanup_feed, delete_user, insert_feed, insert_feed_item, insert_user, + }; + + #[actix_web::test] + async fn delete_feed_removes_feed_and_items() { + let mut connection = establish_connection(); + let user = insert_user(&mut connection, "secret"); + let f = insert_feed(&mut connection, user.id); + let item = insert_feed_item(&mut connection, f.id, false); + + let app = test::init_service( + App::new().route("/feed/{feed_id}", web::delete().to(delete_feed)), + ) + .await; + let req = test::TestRequest::delete() + .uri(&format!("/feed/{}", f.id)) + .to_request(); + let resp = test::call_service(&app, req).await; + + assert_eq!(StatusCode::NO_CONTENT, resp.status()); + + let feed_exists: i64 = feed::table + .filter(feed::id.eq(f.id)) + .count() + .get_result(&mut connection) + .unwrap(); + assert_eq!(0, feed_exists); + + let item_exists: i64 = feed_item::table + .filter(feed_item::id.eq(item.id)) + .count() + .get_result(&mut connection) + .unwrap(); + assert_eq!(0, item_exists); + + delete_user(&mut connection, user.id); + } + + #[actix_web::test] + async fn delete_feed_returns_404_for_nonexistent_feed() { + let app = test::init_service( + App::new().route("/feed/{feed_id}", web::delete().to(delete_feed)), + ) + .await; + let req = test::TestRequest::delete() + .uri("/feed/999999999") + .to_request(); + let resp = test::call_service(&app, req).await; + + assert_eq!(StatusCode::NOT_FOUND, resp.status()); + } + + #[actix_web::test] + async fn delete_feed_does_not_affect_other_feeds() { + let mut connection = establish_connection(); + let user = insert_user(&mut connection, "secret"); + let feed_a = insert_feed(&mut connection, user.id); + let feed_b = insert_feed(&mut connection, user.id); + + let app = test::init_service( + App::new().route("/feed/{feed_id}", web::delete().to(delete_feed)), + ) + .await; + let req = test::TestRequest::delete() + .uri(&format!("/feed/{}", feed_a.id)) + .to_request(); + let resp = test::call_service(&app, req).await; + + assert_eq!(StatusCode::NO_CONTENT, resp.status()); + + let feed_b_exists: i64 = feed::table + .filter(feed::id.eq(feed_b.id)) + .count() + .get_result(&mut connection) + .unwrap(); + assert_eq!(1, feed_b_exists); + + cleanup_feed(&mut connection, feed_b.id); + delete_user(&mut connection, user.id); + } +} diff --git a/src/reader/list_feeds.rs b/src/reader/list_feeds.rs new file mode 100644 index 0000000..42a5b4d --- /dev/null +++ b/src/reader/list_feeds.rs @@ -0,0 +1,111 @@ +use actix_web::{web, HttpRequest, Responder}; +use diesel::prelude::*; + +use crate::{ + database::establish_connection, + json_serialization::feed_info::{FeedInfo, FeedInfoList}, + json_serialization::user::JsonUser, + schema::feed::{self, user_id}, +}; + +pub async fn list_feeds(path: web::Path, req: HttpRequest) -> impl Responder { + let request = req.clone(); + let req_user_id = path.user_id; + + let mut connection = establish_connection(); + let feeds = feed::table + .filter(user_id.eq(req_user_id)) + .select((feed::id, feed::title, feed::url)) + .load::<(i32, String, String)>(&mut connection) + .unwrap(); + + let feed_list: Vec = feeds + .into_iter() + .map(|(id, title, url)| FeedInfo { id, title, url }) + .collect(); + + FeedInfoList { feeds: feed_list }.respond_to(&request) +} + +#[cfg(test)] +mod tests { + use actix_web::http::StatusCode; + use actix_web::{test, web, App}; + + use super::list_feeds; + use crate::database::establish_connection; + use crate::test_helpers::{delete_feed, delete_user, insert_feed, insert_user}; + + #[actix_web::test] + async fn list_feeds_returns_feeds_for_user() { + let mut connection = establish_connection(); + let user = insert_user(&mut connection, "secret"); + let feed = insert_feed(&mut connection, user.id); + + let app = test::init_service( + App::new().route("/feeds/{user_id}", web::get().to(list_feeds)), + ) + .await; + let req = test::TestRequest::get() + .uri(&format!("/feeds/{}", user.id)) + .to_request(); + let resp = test::call_service(&app, req).await; + + assert_eq!(StatusCode::OK, resp.status()); + let body = test::read_body(resp).await; + let body_str = String::from_utf8(body.to_vec()).unwrap(); + assert!(body_str.contains(&feed.title)); + assert!(body_str.contains(&feed.url)); + + delete_feed(&mut connection, feed.id); + delete_user(&mut connection, user.id); + } + + #[actix_web::test] + async fn list_feeds_returns_empty_list_for_user_with_no_feeds() { + let mut connection = establish_connection(); + let user = insert_user(&mut connection, "secret"); + + let app = test::init_service( + App::new().route("/feeds/{user_id}", web::get().to(list_feeds)), + ) + .await; + let req = test::TestRequest::get() + .uri(&format!("/feeds/{}", user.id)) + .to_request(); + let resp = test::call_service(&app, req).await; + + assert_eq!(StatusCode::OK, resp.status()); + let body = test::read_body(resp).await; + let body_str = String::from_utf8(body.to_vec()).unwrap(); + assert!(body_str.contains("\"feeds\":[]")); + + delete_user(&mut connection, user.id); + } + + #[actix_web::test] + async fn list_feeds_does_not_return_other_users_feeds() { + 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 app = test::init_service( + App::new().route("/feeds/{user_id}", web::get().to(list_feeds)), + ) + .await; + let req = test::TestRequest::get() + .uri(&format!("/feeds/{}", user_a.id)) + .to_request(); + let resp = test::call_service(&app, req).await; + + assert_eq!(StatusCode::OK, resp.status()); + let body = test::read_body(resp).await; + let body_str = String::from_utf8(body.to_vec()).unwrap(); + assert!(!body_str.contains(&feed_b.title)); + + delete_feed(&mut connection, feed_b.id); + delete_user(&mut connection, user_a.id); + delete_user(&mut connection, user_b.id); + } +} diff --git a/vue/src/components/AdminFeeds.vue b/vue/src/components/AdminFeeds.vue new file mode 100644 index 0000000..3bc698b --- /dev/null +++ b/vue/src/components/AdminFeeds.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/vue/src/views/AdminView.vue b/vue/src/views/AdminView.vue new file mode 100644 index 0000000..60c7a38 --- /dev/null +++ b/vue/src/views/AdminView.vue @@ -0,0 +1,9 @@ + + +