Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ frontend/src/assets/logo.svg
target
.env
.vscode
.idea

# ergo irc server
ergo/ergo-conf.yaml
Expand Down
2 changes: 2 additions & 0 deletions backend/api/src/api_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ use arcadia_storage::models::user_application::UserApplicationHierarchy;
crate::handlers::forum::edit_forum_thread::exec,
crate::handlers::forum::pin_forum_thread::exec,
crate::handlers::forum::create_forum_post::exec,
crate::handlers::forum::set_forum_post_reaction::exec,
crate::handlers::forum::delete_forum_post_reaction::exec,
crate::handlers::forum::edit_forum_post::exec,
crate::handlers::forum::delete_forum_category::exec,
crate::handlers::forum::delete_forum_sub_category::exec,
Expand Down
43 changes: 43 additions & 0 deletions backend/api/src/handlers/forum/delete_forum_post_reaction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use crate::{middlewares::auth_middleware::Authdata, Arcadia};
use actix_web::{
web::{Data, Path},
HttpRequest, HttpResponse,
};
use arcadia_common::error::Result;
use arcadia_storage::{
models::{forum::ForumPostHierarchy, user::UserPermission},
redis::RedisPoolInterface,
};

#[utoipa::path(
delete,
operation_id = "Delete forum post reaction",
tag = "Forum",
path = "/api/forum/post/{id}/reaction",
security(
("http" = ["Bearer"])
),
params(
("id" = i64, Path, description = "Forum post id")
),
responses(
(status = 200, description = "Successfully delete the forum post reaction", body = ForumPostHierarchy),
)
)]
pub async fn exec<R: RedisPoolInterface + 'static>(
post_id: Path<i64>,
arc: Data<Arcadia<R>>,
user: Authdata,
req: HttpRequest,
) -> Result<HttpResponse> {
arc.pool
.require_permission(user.sub, &UserPermission::CreateForumPost, req.path())
.await?;

let forum_post = arc
.pool
.delete_forum_post_reaction_and_get_post(*post_id, user.sub)
.await?;

Ok(HttpResponse::Ok().json(forum_post))
}
7 changes: 7 additions & 0 deletions backend/api/src/handlers/forum/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod create_forum_sub_category;
pub mod create_forum_thread;
pub mod delete_forum_category;
pub mod delete_forum_post;
pub mod delete_forum_post_reaction;
pub mod delete_forum_sub_category;
pub mod delete_forum_thread;
pub mod edit_forum_category;
Expand All @@ -20,6 +21,7 @@ pub mod pin_forum_thread;
pub mod remove_forum_sub_category_allowed_poster;
pub mod reorder_forum_category;
pub mod reorder_forum_sub_category;
pub mod set_forum_post_reaction;

use actix_web::web::{delete, get, post, put, resource, ServiceConfig};
use arcadia_storage::redis::RedisPoolInterface;
Expand Down Expand Up @@ -50,6 +52,11 @@ pub fn config<R: RedisPoolInterface + 'static>(cfg: &mut ServiceConfig) {
.route(put().to(self::edit_forum_post::exec::<R>))
.route(delete().to(self::delete_forum_post::exec::<R>)),
);
cfg.service(
resource("/post/{id}/reaction")
.route(put().to(self::set_forum_post_reaction::exec::<R>))
.route(delete().to(self::delete_forum_post_reaction::exec::<R>)),
);
cfg.service(
resource("/sub-category")
.route(get().to(self::get_forum_sub_category_threads::exec::<R>))
Expand Down
48 changes: 48 additions & 0 deletions backend/api/src/handlers/forum/set_forum_post_reaction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use crate::{middlewares::auth_middleware::Authdata, Arcadia};
use actix_web::{
web::{Data, Json, Path},
HttpRequest, HttpResponse,
};
use arcadia_common::error::Result;
use arcadia_storage::{
models::{
forum::{ForumPostHierarchy, UserCreatedForumPostReaction},
user::UserPermission,
},
redis::RedisPoolInterface,
};

#[utoipa::path(
put,
operation_id = "Set forum post reaction",
tag = "Forum",
path = "/api/forum/post/{id}/reaction",
security(
("http" = ["Bearer"])
),
params(
("id" = i64, Path, description = "Forum post id")
),
request_body = UserCreatedForumPostReaction,
responses(
(status = 200, description = "Successfully set the forum post reaction", body = ForumPostHierarchy),
)
)]
pub async fn exec<R: RedisPoolInterface + 'static>(
post_id: Path<i64>,
reaction: Json<UserCreatedForumPostReaction>,
arc: Data<Arcadia<R>>,
user: Authdata,
req: HttpRequest,
) -> Result<HttpResponse> {
arc.pool
.require_permission(user.sub, &UserPermission::CreateForumPost, req.path())
.await?;

let forum_post = arc
.pool
.set_forum_post_reaction_and_get_post(*post_id, reaction.into_inner(), user.sub)
.await?;

Ok(HttpResponse::Ok().json(forum_post))
}
3 changes: 2 additions & 1 deletion backend/api/tests/fixtures/with_test_forum_post.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ VALUES
(100, 100, 'This is the first post in the test thread', '2025-01-01 10:00:00+00', 100),
(101, 101, 'This is the first post in the locked thread', '2025-01-01 11:00:00+00', 100),
(102, 102, 'This is the first post in the pinned thread', '2025-01-01 12:00:00+00', 100),
(103, 103, 'This is the first post in the thread in different sub category', '2025-01-01 13:00:00+00', 100);
(103, 103, 'This is the first post in the thread in different sub category', '2025-01-01 13:00:00+00', 100),
(104, 100, 'This is the second post in the test thread', '2025-01-01 13:00:00+00', 100);
4 changes: 4 additions & 0 deletions backend/api/tests/fixtures/with_test_forum_reaction.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
INSERT INTO
forum_post_reactions (id, forum_post_id, user_id, emoji)
VALUES
(100, 100, 100, '🥰');
243 changes: 243 additions & 0 deletions backend/api/tests/test_forum_post.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
pub mod common;
pub mod mocks;

use actix_web::http::StatusCode;
use actix_web::test;

use arcadia_storage::models::forum::{ForumPostHierarchy, UserCreatedForumPostReaction};
use arcadia_storage::{connection_pool::ConnectionPool, models::common::PaginatedResults};
use common::{auth_header, create_test_app_and_login, TestUser};
use mocks::mock_redis::MockRedisPool;
use sqlx::PgPool;
use std::sync::Arc;

// ============================================================================
// GET FORUM POST REACTIONS TESTS
// ============================================================================
#[sqlx::test(
fixtures(
"with_test_users",
"with_test_forum_category",
"with_test_forum_sub_category",
"with_test_forum_thread",
"with_test_forum_post",
"with_test_forum_reaction",
),
migrations = "../storage/migrations"
)]
async fn test_get_forum_post_with_one_reaction(pool: PgPool) {
let pool = Arc::new(ConnectionPool::with_pg_pool(pool));
let (service, user) =
create_test_app_and_login(pool, MockRedisPool::default(), TestUser::Standard).await;

// We retrieve the thread
let req = test::TestRequest::get()
.uri("/api/forum/thread/posts?thread_id=100&page_size=10")
.insert_header(auth_header(&user.token))
.to_request();

let posts: PaginatedResults<ForumPostHierarchy> =
common::call_and_read_body_json_with_status(&service, req, StatusCode::OK).await;

// We got 2 posts for this thread from this user
// The first post has a reaction, the second none
assert_eq!(posts.total_items, 2);
assert_eq!(posts.results[0].created_by.id, 100);
assert!(posts.results[0].reaction.is_some());
assert_eq!(posts.results[0].reaction.as_ref().unwrap().emoji, "🥰");
assert_eq!(
posts.results[0].reaction.as_ref().unwrap().forum_post_id,
100
);
assert!(posts.results[1].reaction.is_none());
}

// ============================================================================
// POST FORUM POST REACTIONS TESTS
// ============================================================================
#[sqlx::test(
fixtures(
"with_test_users",
"with_test_forum_category",
"with_test_forum_sub_category",
"with_test_forum_thread",
"with_test_forum_post",
"with_test_forum_reaction"
),
migrations = "../storage/migrations"
)]
async fn test_post_forum_post_reaction(pool: PgPool) {
let pool = Arc::new(ConnectionPool::with_pg_pool(pool));
let (service, user) =
create_test_app_and_login(pool, MockRedisPool::default(), TestUser::Standard).await;

// We get the post and check if not reaction is bound to it
let req_get = test::TestRequest::get()
.uri("/api/forum/thread/posts?thread_id=103&page_size=10")
.insert_header(auth_header(&user.token))
.to_request();

let posts: PaginatedResults<ForumPostHierarchy> =
common::call_and_read_body_json_with_status(&service, req_get, StatusCode::OK).await;

assert_eq!(posts.total_items, 1);
assert_eq!(posts.results[0].id, 103);
assert!(posts.results[0].reaction.is_none());

// We create the reaction and test that it was created
let create_body = UserCreatedForumPostReaction {
emoji: "😺".to_string(),
};

let req = test::TestRequest::put()
.uri("/api/forum/post/103/reaction")
.insert_header(auth_header(&user.token))
.set_json(&create_body)
.to_request();

let post: ForumPostHierarchy =
common::call_and_read_body_json_with_status(&service, req, StatusCode::OK).await;

assert_eq!(post.id, 103);
assert_eq!(post.reaction.as_ref().unwrap().forum_post_id, 103);
assert_eq!(post.reaction.as_ref().unwrap().emoji, "😺");
}

#[sqlx::test(
fixtures(
"with_test_users",
"with_test_forum_category",
"with_test_forum_sub_category",
"with_test_forum_thread",
"with_test_forum_post",
"with_test_forum_reaction"
),
migrations = "../storage/migrations"
)]
async fn test_post_forum_post_reaction_should_return_error(pool: PgPool) {
let pool = Arc::new(ConnectionPool::with_pg_pool(pool));
let (service, user) =
create_test_app_and_login(pool, MockRedisPool::default(), TestUser::Standard).await;

// We get the post and check if not reaction is bound to it
let req_get = test::TestRequest::get()
.uri("/api/forum/thread/posts?thread_id=103&page_size=10")
.insert_header(auth_header(&user.token))
.to_request();

let posts: PaginatedResults<ForumPostHierarchy> =
common::call_and_read_body_json_with_status(&service, req_get, StatusCode::OK).await;

assert_eq!(posts.total_items, 1);
assert_eq!(posts.results[0].id, 103);
assert!(posts.results[0].reaction.is_none());

// We create the reaction with 2 emojis, it will not work since the column is a VARCHAR(1)
let create_body = UserCreatedForumPostReaction {
emoji: "😺😺".to_string(),
};

let req = test::TestRequest::put()
.uri("/api/forum/post/103/reaction")
.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::BAD_REQUEST);

let body = test::read_body(resp).await;
let body = String::from_utf8(body.to_vec()).unwrap();

assert!(body.contains("could not update forum post reaction"));
}

// ============================================================================
// PUT FORUM POST REACTIONS TESTS
// ============================================================================
#[sqlx::test(
fixtures(
"with_test_users",
"with_test_forum_category",
"with_test_forum_sub_category",
"with_test_forum_thread",
"with_test_forum_post",
"with_test_forum_reaction",
),
migrations = "../storage/migrations"
)]
async fn test_put_forum_post_reaction(pool: PgPool) {
let pool = Arc::new(ConnectionPool::with_pg_pool(pool));
let (service, user) =
create_test_app_and_login(pool, MockRedisPool::default(), TestUser::Standard).await;

// We retrieve the old reaction to check it's already created and has the emoji 🥰
let req_get = test::TestRequest::get()
.uri("/api/forum/thread/posts?thread_id=100&page_size=10")
.insert_header(auth_header(&user.token))
.to_request();

let posts: PaginatedResults<ForumPostHierarchy> =
common::call_and_read_body_json_with_status(&service, req_get, StatusCode::OK).await;

assert!(posts.results[0].reaction.is_some());
assert_eq!(posts.results[0].id, 100);
assert_eq!(posts.results[0].reaction.as_ref().unwrap().emoji, "🥰");

// We modify the reaction (the emoji)
let create_body = UserCreatedForumPostReaction {
emoji: "😺".to_string(),
};

let req = test::TestRequest::put()
.uri("/api/forum/post/100/reaction")
.insert_header(auth_header(&user.token))
.set_json(&create_body)
.to_request();

let post: ForumPostHierarchy =
common::call_and_read_body_json_with_status(&service, req, StatusCode::OK).await;

assert_eq!(post.reaction.as_ref().unwrap().id, 100);
assert_eq!(post.reaction.as_ref().unwrap().emoji, "😺");
}

// ============================================================================
// DELETE FORUM POST REACTIONS TESTS
// ============================================================================
#[sqlx::test(
fixtures(
"with_test_users",
"with_test_forum_category",
"with_test_forum_sub_category",
"with_test_forum_thread",
"with_test_forum_post",
"with_test_forum_reaction",
),
migrations = "../storage/migrations"
)]
async fn test_delete_forum_post_reaction(pool: PgPool) {
let pool = Arc::new(ConnectionPool::with_pg_pool(pool));
let (service, user) =
create_test_app_and_login(pool, MockRedisPool::default(), TestUser::Standard).await;

let req = test::TestRequest::delete()
.uri("/api/forum/post/100/reaction")
.insert_header(auth_header(&user.token))
.to_request();

let res = test::call_service(&service, req).await;
assert_eq!(res.status(), StatusCode::OK);

// Verify delete by getting the post and checking that reaction is null
let req_get = test::TestRequest::get()
.uri("/api/forum/thread/posts?thread_id=100&page_size=10&post_id=100")
.insert_header(auth_header(&user.token))
.to_request();

let posts: PaginatedResults<ForumPostHierarchy> =
common::call_and_read_body_json_with_status(&service, req_get, StatusCode::OK).await;

assert!(posts.results[0].reaction.is_none());
}
Loading
Loading