From 73bbb7bde76287b9613a58c38d012c9c1aa2a501 Mon Sep 17 00:00:00 2001 From: FrenchGithubUser Date: Sat, 13 Dec 2025 16:23:52 +0100 Subject: [PATCH] feat: create/edit forum categories in the backend --- backend/api/src/api_doc.rs | 2 + .../handlers/forum/create_forum_category.rs | 42 +++ .../src/handlers/forum/edit_forum_category.rs | 39 ++ backend/api/src/handlers/forum/mod.rs | 7 + backend/api/tests/test_forum_category.rs | 335 ++++++++++++++++++ backend/common/src/error/mod.rs | 18 +- ...1076b4d105b7c6abdb63159c8cf5241ed6903.json | 41 +++ ...9e0f6a7d9c8757371b80af6f813e9bef8eeee.json | 41 +++ backend/storage/src/models/forum.rs | 11 + .../src/repositories/forum_repository.rs | 65 +++- 10 files changed, 594 insertions(+), 7 deletions(-) create mode 100644 backend/api/src/handlers/forum/create_forum_category.rs create mode 100644 backend/api/src/handlers/forum/edit_forum_category.rs create mode 100644 backend/api/tests/test_forum_category.rs create mode 100644 backend/storage/.sqlx/query-2888cad14797fcf3bf4f198b2f01076b4d105b7c6abdb63159c8cf5241ed6903.json create mode 100644 backend/storage/.sqlx/query-4646106c53b4d1dbb1946e0bc789e0f6a7d9c8757371b80af6f813e9bef8eeee.json diff --git a/backend/api/src/api_doc.rs b/backend/api/src/api_doc.rs index 985eb227..f7574641 100644 --- a/backend/api/src/api_doc.rs +++ b/backend/api/src/api_doc.rs @@ -90,6 +90,8 @@ use crate::handlers::user_applications::get_user_applications::GetUserApplicatio crate::handlers::torrent_requests::create_torrent_request_comment::exec, crate::handlers::gifts::create_gift::exec, crate::handlers::forum::get_forum::exec, + crate::handlers::forum::create_forum_category::exec, + crate::handlers::forum::edit_forum_category::exec, crate::handlers::forum::get_forum_sub_category_threads::exec, crate::handlers::forum::get_forum_thread::exec, crate::handlers::forum::get_forum_thread_posts::exec, diff --git a/backend/api/src/handlers/forum/create_forum_category.rs b/backend/api/src/handlers/forum/create_forum_category.rs new file mode 100644 index 00000000..5284f435 --- /dev/null +++ b/backend/api/src/handlers/forum/create_forum_category.rs @@ -0,0 +1,42 @@ +use crate::{middlewares::auth_middleware::Authdata, Arcadia}; +use actix_web::{ + web::{Data, Json}, + HttpResponse, +}; +use arcadia_common::error::{Error, Result}; +use arcadia_storage::{ + models::{ + forum::{ForumCategory, UserCreatedForumCategory}, + user::UserClass, + }, + redis::RedisPoolInterface, +}; + +#[utoipa::path( + post, + operation_id = "Create forum category", + tag = "Forum", + path = "/api/forum/category", + security( + ("http" = ["Bearer"]) + ), + responses( + (status = 201, description = "Successfully created the forum category", body=ForumCategory), + ) +)] +pub async fn exec( + forum_category: Json, + arc: Data>, + user: Authdata, +) -> Result { + if user.class != UserClass::Staff { + return Err(Error::InsufficientPrivileges); + } + + let created_category = arc + .pool + .create_forum_category(&forum_category, user.sub) + .await?; + + Ok(HttpResponse::Created().json(created_category)) +} diff --git a/backend/api/src/handlers/forum/edit_forum_category.rs b/backend/api/src/handlers/forum/edit_forum_category.rs new file mode 100644 index 00000000..f61e615a --- /dev/null +++ b/backend/api/src/handlers/forum/edit_forum_category.rs @@ -0,0 +1,39 @@ +use crate::{middlewares::auth_middleware::Authdata, Arcadia}; +use actix_web::{ + web::{Data, Json}, + HttpResponse, +}; +use arcadia_common::error::{Error, Result}; +use arcadia_storage::{ + models::{ + forum::{EditedForumCategory, ForumCategory}, + user::UserClass, + }, + redis::RedisPoolInterface, +}; + +#[utoipa::path( + put, + operation_id = "Edit forum category", + tag = "Forum", + path = "/api/forum/category", + security( + ("http" = ["Bearer"]) + ), + responses( + (status = 200, description = "Successfully edited the forum category", body=ForumCategory), + ) +)] +pub async fn exec( + edited_category: Json, + arc: Data>, + user: Authdata, +) -> Result { + if user.class != UserClass::Staff { + return Err(Error::InsufficientPrivileges); + } + + let updated_category = arc.pool.update_forum_category(&edited_category).await?; + + Ok(HttpResponse::Ok().json(updated_category)) +} diff --git a/backend/api/src/handlers/forum/mod.rs b/backend/api/src/handlers/forum/mod.rs index caf33f29..4491bc20 100644 --- a/backend/api/src/handlers/forum/mod.rs +++ b/backend/api/src/handlers/forum/mod.rs @@ -1,5 +1,7 @@ +pub mod create_forum_category; pub mod create_forum_post; pub mod create_forum_thread; +pub mod edit_forum_category; pub mod edit_forum_post; pub mod edit_forum_thread; pub mod get_forum; @@ -12,6 +14,11 @@ use arcadia_storage::redis::RedisPoolInterface; pub fn config(cfg: &mut ServiceConfig) { cfg.service(resource("").route(get().to(self::get_forum::exec::))); + cfg.service( + resource("/category") + .route(post().to(self::create_forum_category::exec::)) + .route(put().to(self::edit_forum_category::exec::)), + ); cfg.service( resource("/thread") .route(get().to(self::get_forum_thread::exec::)) diff --git a/backend/api/tests/test_forum_category.rs b/backend/api/tests/test_forum_category.rs new file mode 100644 index 00000000..fbd96a89 --- /dev/null +++ b/backend/api/tests/test_forum_category.rs @@ -0,0 +1,335 @@ +pub mod common; +pub mod mocks; + +use actix_web::http::StatusCode; +use actix_web::test; +use arcadia_storage::connection_pool::ConnectionPool; +use arcadia_storage::models::forum::{ + EditedForumCategory, ForumCategory, UserCreatedForumCategory, +}; +use common::{auth_header, create_test_app_and_login, TestUser}; +use mocks::mock_redis::MockRedisPool; +use sqlx::PgPool; +use std::sync::Arc; + +// ============================================================================ +// CREATE CATEGORY TESTS +// ============================================================================ + +#[sqlx::test(fixtures("with_test_user2"), migrations = "../storage/migrations")] +async fn test_staff_can_create_category(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let (service, staff) = + create_test_app_and_login(pool, MockRedisPool::default(), 101, 101, TestUser::Staff).await; + + let create_body = UserCreatedForumCategory { + name: "New Category".into(), + }; + + let req = test::TestRequest::post() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&staff.token)) + .set_json(&create_body) + .to_request(); + + let category: ForumCategory = + common::call_and_read_body_json_with_status(&service, req, StatusCode::CREATED).await; + + assert_eq!(category.name, "New Category"); + assert_eq!(category.created_by_id, 101); +} + +#[sqlx::test(fixtures("with_test_user"), migrations = "../storage/migrations")] +async fn test_non_staff_cannot_create_category(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let (service, user) = + create_test_app_and_login(pool, MockRedisPool::default(), 100, 100, TestUser::Standard) + .await; + + let create_body = UserCreatedForumCategory { + name: "New Category".into(), + }; + + let req = test::TestRequest::post() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&user.token)) + .set_json(&create_body) + .to_request(); + + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::FORBIDDEN); +} + +#[sqlx::test(fixtures("with_test_user"), migrations = "../storage/migrations")] +async fn test_create_category_without_auth(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let service = common::create_test_app( + pool, + MockRedisPool::default(), + arcadia_api::OpenSignups::Disabled, + 100, + 100, + ) + .await; + + let create_body = UserCreatedForumCategory { + name: "New Category".into(), + }; + + let req = test::TestRequest::post() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .set_json(&create_body) + .to_request(); + + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); +} + +#[sqlx::test(fixtures("with_test_user2"), migrations = "../storage/migrations")] +async fn test_create_category_with_empty_name(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let (service, staff) = + create_test_app_and_login(pool, MockRedisPool::default(), 101, 101, TestUser::Staff).await; + + let create_body = UserCreatedForumCategory { name: "".into() }; + + let req = test::TestRequest::post() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&staff.token)) + .set_json(&create_body) + .to_request(); + + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); +} + +#[sqlx::test(fixtures("with_test_user2"), migrations = "../storage/migrations")] +async fn test_create_category_with_whitespace_only_name(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let (service, staff) = + create_test_app_and_login(pool, MockRedisPool::default(), 101, 101, TestUser::Staff).await; + + let create_body = UserCreatedForumCategory { name: " ".into() }; + + let req = test::TestRequest::post() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&staff.token)) + .set_json(&create_body) + .to_request(); + + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); +} + +// ============================================================================ +// EDIT CATEGORY TESTS +// ============================================================================ + +#[sqlx::test( + fixtures("with_test_user", "with_test_user2", "with_test_forum_category"), + migrations = "../storage/migrations" +)] +async fn test_staff_can_edit_category(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let (service, staff) = + create_test_app_and_login(pool, MockRedisPool::default(), 101, 101, TestUser::Staff).await; + + let edit_body = EditedForumCategory { + id: 100, + name: "Updated Category Name".into(), + }; + + let req = test::TestRequest::put() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&staff.token)) + .set_json(&edit_body) + .to_request(); + + let category: ForumCategory = + common::call_and_read_body_json_with_status(&service, req, StatusCode::OK).await; + + assert_eq!(category.id, 100); + assert_eq!(category.name, "Updated Category Name"); +} + +#[sqlx::test( + fixtures("with_test_user", "with_test_forum_category"), + migrations = "../storage/migrations" +)] +async fn test_non_staff_cannot_edit_category(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let (service, user) = + create_test_app_and_login(pool, MockRedisPool::default(), 100, 100, TestUser::Standard) + .await; + + let edit_body = EditedForumCategory { + id: 100, + name: "Updated Category Name".into(), + }; + + let req = test::TestRequest::put() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&user.token)) + .set_json(&edit_body) + .to_request(); + + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::FORBIDDEN); +} + +#[sqlx::test( + fixtures("with_test_user", "with_test_forum_category"), + migrations = "../storage/migrations" +)] +async fn test_edit_category_without_auth(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let service = common::create_test_app( + pool, + MockRedisPool::default(), + arcadia_api::OpenSignups::Disabled, + 100, + 100, + ) + .await; + + let edit_body = EditedForumCategory { + id: 100, + name: "Updated Category Name".into(), + }; + + let req = test::TestRequest::put() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .set_json(&edit_body) + .to_request(); + + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); +} + +#[sqlx::test(fixtures("with_test_user2"), migrations = "../storage/migrations")] +async fn test_edit_nonexistent_category(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let (service, staff) = + create_test_app_and_login(pool, MockRedisPool::default(), 101, 101, TestUser::Staff).await; + + let edit_body = EditedForumCategory { + id: 999, + name: "Updated Category Name".into(), + }; + + let req = test::TestRequest::put() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&staff.token)) + .set_json(&edit_body) + .to_request(); + + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); +} + +#[sqlx::test( + fixtures("with_test_user", "with_test_user2", "with_test_forum_category"), + migrations = "../storage/migrations" +)] +async fn test_edit_category_with_empty_name(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let (service, staff) = + create_test_app_and_login(pool, MockRedisPool::default(), 101, 101, TestUser::Staff).await; + + let edit_body = EditedForumCategory { + id: 100, + name: "".into(), + }; + + let req = test::TestRequest::put() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&staff.token)) + .set_json(&edit_body) + .to_request(); + + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); +} + +#[sqlx::test( + fixtures("with_test_user", "with_test_user2", "with_test_forum_category"), + migrations = "../storage/migrations" +)] +async fn test_edit_category_with_whitespace_only_name(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let (service, staff) = + create_test_app_and_login(pool, MockRedisPool::default(), 101, 101, TestUser::Staff).await; + + let edit_body = EditedForumCategory { + id: 100, + name: " ".into(), + }; + + let req = test::TestRequest::put() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&staff.token)) + .set_json(&edit_body) + .to_request(); + + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); +} + +// ============================================================================ +// INTEGRATION TESTS +// ============================================================================ + +#[sqlx::test(fixtures("with_test_user2"), migrations = "../storage/migrations")] +async fn test_create_and_edit_category_flow(pool: PgPool) { + let pool = Arc::new(ConnectionPool::with_pg_pool(pool)); + let (service, staff) = + create_test_app_and_login(pool, MockRedisPool::default(), 101, 101, TestUser::Staff).await; + + // Create a category + let create_body = UserCreatedForumCategory { + name: "Initial Category".into(), + }; + + let req = test::TestRequest::post() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&staff.token)) + .set_json(&create_body) + .to_request(); + + let category: ForumCategory = + common::call_and_read_body_json_with_status(&service, req, StatusCode::CREATED).await; + let category_id = category.id; + + assert_eq!(category.name, "Initial Category"); + + // Edit the category + let edit_body = EditedForumCategory { + id: category_id, + name: "Edited Category".into(), + }; + + let req = test::TestRequest::put() + .uri("/api/forum/category") + .insert_header(("X-Forwarded-For", "10.10.4.88")) + .insert_header(auth_header(&staff.token)) + .set_json(&edit_body) + .to_request(); + + let edited_category: ForumCategory = + common::call_and_read_body_json_with_status(&service, req, StatusCode::OK).await; + + assert_eq!(edited_category.id, category_id); + assert_eq!(edited_category.name, "Edited Category"); +} diff --git a/backend/common/src/error/mod.rs b/backend/common/src/error/mod.rs index e2be56df..f0b2834c 100644 --- a/backend/common/src/error/mod.rs +++ b/backend/common/src/error/mod.rs @@ -258,6 +258,18 @@ pub enum Error { #[error("could not search forum threads")] CouldNotSearchForumThreads(#[source] sqlx::Error), + #[error("could not create forum category")] + CouldNotCreateForumCategory(#[source] sqlx::Error), + + #[error("could not update forum category")] + CouldNotUpdateForumCategory(#[source] sqlx::Error), + + #[error("forum category not found")] + ForumCategoryNotFound, + + #[error("forum category name cannot be empty")] + ForumCategoryNameEmpty, + #[error("insufficient privileges")] InsufficientPrivileges, @@ -358,7 +370,8 @@ impl actix_web::ResponseError for Error { | Error::TorrentFileInvalid | Error::InvalidUserIdOrTorrentId | Error::ForumThreadNameEmpty - | Error::ForumPostEmpty => StatusCode::BAD_REQUEST, + | Error::ForumPostEmpty + | Error::ForumCategoryNameEmpty => StatusCode::BAD_REQUEST, // 401 Unauthorized Error::InvalidOrExpiredRefreshToken | Error::InvalidatedToken => { @@ -380,7 +393,8 @@ impl actix_web::ResponseError for Error { | Error::CouldNotFindTitleGroupComment(_) | Error::CouldNotFindForumThread(_) | Error::CouldNotFindForumSubCategory(_) - | Error::CssSheetNotFound(_) => StatusCode::NOT_FOUND, + | Error::CssSheetNotFound(_) + | Error::ForumCategoryNotFound => StatusCode::NOT_FOUND, // 409 Conflict Error::NoInvitationsAvailable diff --git a/backend/storage/.sqlx/query-2888cad14797fcf3bf4f198b2f01076b4d105b7c6abdb63159c8cf5241ed6903.json b/backend/storage/.sqlx/query-2888cad14797fcf3bf4f198b2f01076b4d105b7c6abdb63159c8cf5241ed6903.json new file mode 100644 index 00000000..90b69abd --- /dev/null +++ b/backend/storage/.sqlx/query-2888cad14797fcf3bf4f198b2f01076b4d105b7c6abdb63159c8cf5241ed6903.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE forum_categories\n SET name = $1\n WHERE id = $2\n RETURNING *\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "created_by_id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "2888cad14797fcf3bf4f198b2f01076b4d105b7c6abdb63159c8cf5241ed6903" +} diff --git a/backend/storage/.sqlx/query-4646106c53b4d1dbb1946e0bc789e0f6a7d9c8757371b80af6f813e9bef8eeee.json b/backend/storage/.sqlx/query-4646106c53b4d1dbb1946e0bc789e0f6a7d9c8757371b80af6f813e9bef8eeee.json new file mode 100644 index 00000000..3a28562e --- /dev/null +++ b/backend/storage/.sqlx/query-4646106c53b4d1dbb1946e0bc789e0f6a7d9c8757371b80af6f813e9bef8eeee.json @@ -0,0 +1,41 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO forum_categories (name, created_by_id)\n VALUES ($1, $2)\n RETURNING *\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "created_by_id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "4646106c53b4d1dbb1946e0bc789e0f6a7d9c8757371b80af6f813e9bef8eeee" +} diff --git a/backend/storage/src/models/forum.rs b/backend/storage/src/models/forum.rs index d27ca023..de8841e3 100644 --- a/backend/storage/src/models/forum.rs +++ b/backend/storage/src/models/forum.rs @@ -14,6 +14,17 @@ pub struct ForumCategory { pub created_by_id: i32, } +#[derive(Debug, Deserialize, Serialize, ToSchema)] +pub struct UserCreatedForumCategory { + pub name: String, +} + +#[derive(Debug, Deserialize, Serialize, ToSchema)] +pub struct EditedForumCategory { + pub id: i32, + pub name: String, +} + #[derive(Debug, Deserialize, Serialize, FromRow, ToSchema)] pub struct ForumSubCategory { pub id: i32, diff --git a/backend/storage/src/repositories/forum_repository.rs b/backend/storage/src/repositories/forum_repository.rs index 7e204cde..0dab8650 100644 --- a/backend/storage/src/repositories/forum_repository.rs +++ b/backend/storage/src/repositories/forum_repository.rs @@ -3,11 +3,11 @@ use crate::{ models::{ common::PaginatedResults, forum::{ - EditedForumPost, EditedForumThread, ForumCategoryHierarchy, ForumCategoryLite, - ForumPost, ForumPostAndThreadName, ForumPostHierarchy, ForumSearchQuery, - ForumSearchResult, ForumSubCategoryHierarchy, ForumThread, ForumThreadEnriched, - ForumThreadPostLite, GetForumThreadPostsQuery, UserCreatedForumPost, - UserCreatedForumThread, + EditedForumCategory, EditedForumPost, EditedForumThread, ForumCategory, + ForumCategoryHierarchy, ForumCategoryLite, ForumPost, ForumPostAndThreadName, + ForumPostHierarchy, ForumSearchQuery, ForumSearchResult, ForumSubCategoryHierarchy, + ForumThread, ForumThreadEnriched, ForumThreadPostLite, GetForumThreadPostsQuery, + UserCreatedForumCategory, UserCreatedForumPost, UserCreatedForumThread, }, user::{UserLite, UserLiteAvatar}, }, @@ -734,4 +734,59 @@ impl ConnectionPool { page_size: form.page_size, }) } + + pub async fn create_forum_category( + &self, + forum_category: &UserCreatedForumCategory, + current_user_id: i32, + ) -> Result { + if forum_category.name.trim().is_empty() { + return Err(Error::ForumCategoryNameEmpty); + } + + let created_category = sqlx::query_as!( + ForumCategory, + r#" + INSERT INTO forum_categories (name, created_by_id) + VALUES ($1, $2) + RETURNING * + "#, + forum_category.name, + current_user_id + ) + .fetch_one(self.borrow()) + .await + .map_err(Error::CouldNotCreateForumCategory)?; + + Ok(created_category) + } + + pub async fn update_forum_category( + &self, + edited_category: &EditedForumCategory, + ) -> Result { + if edited_category.name.trim().is_empty() { + return Err(Error::ForumCategoryNameEmpty); + } + + let updated_category = sqlx::query_as!( + ForumCategory, + r#" + UPDATE forum_categories + SET name = $1 + WHERE id = $2 + RETURNING * + "#, + edited_category.name, + edited_category.id + ) + .fetch_one(self.borrow()) + .await + .map_err(|e| match e { + sqlx::Error::RowNotFound => Error::ForumCategoryNotFound, + _ => Error::CouldNotUpdateForumCategory(e), + })?; + + Ok(updated_category) + } }