diff --git a/packages/api-server/src/domains/posts/dto_tests.inc b/packages/api-server/src/domains/posts/dto_tests.inc index 1f1028ee..8bcfaf26 100644 --- a/packages/api-server/src/domains/posts/dto_tests.inc +++ b/packages/api-server/src/domains/posts/dto_tests.inc @@ -49,6 +49,8 @@ fn sample_user() -> UsersModel { rank: "gold".into(), total_points: 0, is_admin: false, + ink_credits: 0, + style_dna: None, created_at: t, updated_at: t, } diff --git a/packages/api-server/src/domains/posts/tests.rs b/packages/api-server/src/domains/posts/tests.rs index 486ee7d7..00062805 100644 --- a/packages/api-server/src/domains/posts/tests.rs +++ b/packages/api-server/src/domains/posts/tests.rs @@ -574,6 +574,8 @@ mod tests { fn image_upload_response_serialization() { let r = ImageUploadResponse { image_url: "https://cdn.test.com/posts/uuid/img.webp".to_string(), + image_width: None, + image_height: None, }; let v = serde_json::to_value(&r).unwrap(); assert_eq!(v["image_url"], "https://cdn.test.com/posts/uuid/img.webp"); diff --git a/packages/api-server/src/domains/users/dto.rs b/packages/api-server/src/domains/users/dto.rs index 7822446f..7a0081fb 100644 --- a/packages/api-server/src/domains/users/dto.rs +++ b/packages/api-server/src/domains/users/dto.rs @@ -284,6 +284,8 @@ mod tests { rank: "Member".to_string(), total_points: 100, is_admin: false, + ink_credits: 0, + style_dna: None, created_at: now, updated_at: now, }; @@ -312,6 +314,8 @@ mod tests { rank: "Gold".to_string(), total_points: 42, is_admin: true, + ink_credits: 0, + style_dna: None, followers_count: 0, following_count: 0, }; @@ -343,6 +347,8 @@ mod tests { rank: "Gold".to_string(), total_points: 0, is_admin: false, + ink_credits: 0, + style_dna: None, followers_count: 42, following_count: 7, }; @@ -368,6 +374,8 @@ mod tests { rank: "Member".to_string(), total_points: 0, is_admin: false, + ink_credits: 0, + style_dna: None, created_at: now, updated_at: now, }; diff --git a/packages/api-server/src/domains/users/service.rs b/packages/api-server/src/domains/users/service.rs index df1b3d5d..38f989c5 100644 --- a/packages/api-server/src/domains/users/service.rs +++ b/packages/api-server/src/domains/users/service.rs @@ -119,42 +119,6 @@ pub async fn get_user_stats( let total_comments = count_user_comments(db, user_id).await?; let total_likes_received = count_user_likes_received(db, user_id).await?; - let count_query = |table: &str| { - format!( - "SELECT COUNT(*)::BIGINT AS cnt FROM public.{} WHERE user_id = $1", - table - ) - }; - - let query_count = |sql: String| async move { - db.query_one(Statement::from_sql_and_values( - DbBackend::Postgres, - &sql, - [user_id.into()], - )) - .await - .map_err(AppError::DatabaseError) - .map(|r| { - r.map(|row| row.try_get::("", "cnt").unwrap_or(0)) - .unwrap_or(0) - }) - }; - - let total_posts = query_count(count_query("posts")).await?; - let total_comments = query_count(count_query("comments")).await?; - - let likes_sql = "SELECT COUNT(*)::BIGINT AS cnt FROM public.post_likes pl JOIN public.posts p ON pl.post_id = p.id WHERE p.user_id = $1"; - let total_likes_received = db - .query_one(Statement::from_sql_and_values( - DbBackend::Postgres, - likes_sql, - [user_id.into()], - )) - .await - .map_err(AppError::DatabaseError)? - .map(|row| row.try_get::("", "cnt").unwrap_or(0)) - .unwrap_or(0); - Ok(UserStatsResponse { user_id, total_posts, diff --git a/packages/api-server/src/domains/users/tests.rs b/packages/api-server/src/domains/users/tests.rs index d2c6c541..03ad886b 100644 --- a/packages/api-server/src/domains/users/tests.rs +++ b/packages/api-server/src/domains/users/tests.rs @@ -66,8 +66,17 @@ mod mock_db_tests { #[tokio::test] async fn get_user_stats_success() { + let mk_cnt = |n: i64| { + let mut m = std::collections::BTreeMap::new(); + m.insert("cnt".to_string(), sea_orm::Value::BigInt(Some(n))); + m + }; + // user lookup + 3 count queries (posts, comments, likes_received) let db = MockDatabase::new(DatabaseBackend::Postgres) .append_query_results([[fixtures::user_model()]]) + .append_query_results([[mk_cnt(0)]]) + .append_query_results([[mk_cnt(0)]]) + .append_query_results([[mk_cnt(0)]]) .into_connection(); let result = service::get_user_stats(&db, fixtures::test_uuid(10)).await; assert!(result.is_ok(), "unexpected err: {:?}", result.err()); @@ -90,13 +99,23 @@ mod mock_db_tests { } #[tokio::test] - async fn list_user_activities_returns_empty_stub() { - // Service currently returns an empty stub — no DB call. - let db = crate::tests::helpers::empty_mock_db(); + async fn list_user_activities_returns_empty() { + // dev 버전은 실제 UNION 쿼리 사용: 1st query count, 2nd query rows + let mk_cnt = |n: i64| { + let mut m = std::collections::BTreeMap::new(); + m.insert("cnt".to_string(), sea_orm::Value::BigInt(Some(n))); + m + }; + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([[mk_cnt(0)]]) + .append_query_results([ + Vec::>::new(), + ]) + .into_connection(); let pagination = crate::utils::pagination::Pagination::new(1, 20); let result = service::list_user_activities(&db, fixtures::test_uuid(10), None, pagination).await; - assert!(result.is_ok()); + assert!(result.is_ok(), "unexpected err: {:?}", result.err()); let resp = result.unwrap(); assert!(resp.data.is_empty()); assert_eq!(resp.pagination.total_items, 0); @@ -162,6 +181,224 @@ mod mock_db_tests { let result = service::get_user_with_follow_counts(&db, fixtures::test_uuid(99)).await; assert!(matches!(result, Err(crate::AppError::NotFound(_)))); } + + // ── follow / unfollow ── + + #[tokio::test] + async fn follow_user_self_returns_bad_request() { + let db = crate::tests::helpers::empty_mock_db(); + let result = + service::follow_user(&db, fixtures::test_uuid(10), fixtures::test_uuid(10)).await; + assert!(matches!(result, Err(crate::AppError::BadRequest(_)))); + } + + #[tokio::test] + async fn follow_user_target_not_found() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([Vec::::new()]) + .into_connection(); + let result = + service::follow_user(&db, fixtures::test_uuid(10), fixtures::test_uuid(99)).await; + assert!(matches!(result, Err(crate::AppError::NotFound(_)))); + } + + #[tokio::test] + async fn follow_user_success() { + use sea_orm::MockExecResult; + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([[fixtures::user_model()]]) + .append_exec_results([MockExecResult { + last_insert_id: 0, + rows_affected: 1, + }]) + .into_connection(); + let result = + service::follow_user(&db, fixtures::test_uuid(11), fixtures::test_uuid(10)).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn unfollow_user_success() { + use sea_orm::MockExecResult; + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results([MockExecResult { + last_insert_id: 0, + rows_affected: 1, + }]) + .into_connection(); + let result = + service::unfollow_user(&db, fixtures::test_uuid(11), fixtures::test_uuid(10)).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn check_is_following_returns_true() { + let mut row = std::collections::BTreeMap::new(); + row.insert("is_following".to_string(), sea_orm::Value::Bool(Some(true))); + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([[row]]) + .into_connection(); + let result = + service::check_is_following(&db, fixtures::test_uuid(11), fixtures::test_uuid(10)) + .await; + assert!(result.unwrap()); + } + + #[tokio::test] + async fn check_is_following_returns_false_when_empty() { + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([ + Vec::>::new(), + ]) + .into_connection(); + let result = + service::check_is_following(&db, fixtures::test_uuid(11), fixtures::test_uuid(10)) + .await; + assert!(!result.unwrap()); + } + + // ── follow handlers ── + + #[tokio::test] + async fn follow_user_handler_target_not_found() { + use crate::domains::users::handlers::follow_user_handler; + use crate::tests::helpers::{mock_user, test_app_state}; + use axum::extract::{Path, State}; + use axum::Extension; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([Vec::::new()]) + .into_connection(); + let state = test_app_state(db); + let user = mock_user(); + let result = + follow_user_handler(State(state), Extension(user), Path(fixtures::test_uuid(99))).await; + assert!(matches!(result, Err(crate::AppError::NotFound(_)))); + } + + #[tokio::test] + async fn unfollow_user_handler_success() { + use crate::domains::users::handlers::unfollow_user_handler; + use crate::tests::helpers::{mock_user, test_app_state}; + use axum::extract::{Path, State}; + use axum::Extension; + use sea_orm::MockExecResult; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_exec_results([MockExecResult { + last_insert_id: 0, + rows_affected: 1, + }]) + .into_connection(); + let state = test_app_state(db); + let user = mock_user(); + let result = + unfollow_user_handler(State(state), Extension(user), Path(fixtures::test_uuid(11))) + .await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn get_follow_status_handler_returns_false() { + use crate::domains::users::handlers::get_follow_status; + use crate::tests::helpers::{mock_user, test_app_state}; + use axum::extract::{Path, State}; + use axum::Extension; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([ + Vec::>::new(), + ]) + .into_connection(); + let state = test_app_state(db); + let user = mock_user(); + let result = + get_follow_status(State(state), Extension(user), Path(fixtures::test_uuid(11))).await; + assert!(result.is_ok()); + let body = result.unwrap(); + assert!(!body.is_following); + } + + #[tokio::test] + async fn get_user_profile_handler_not_found() { + use crate::domains::users::handlers::get_user_profile; + use crate::tests::helpers::test_app_state; + use axum::extract::{Path, State}; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([Vec::::new()]) + .into_connection(); + let state = test_app_state(db); + let result = get_user_profile(State(state), Path(fixtures::test_uuid(99))).await; + assert!(matches!(result, Err(crate::AppError::NotFound(_)))); + } + + #[tokio::test] + async fn get_my_profile_handler_not_found() { + use crate::domains::users::handlers::get_my_profile; + use crate::tests::helpers::{mock_user, test_app_state}; + use axum::extract::State; + use axum::Extension; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([Vec::::new()]) + .into_connection(); + let state = test_app_state(db); + let user = mock_user(); + let result = get_my_profile(State(state), Extension(user)).await; + assert!(matches!(result, Err(crate::AppError::NotFound(_)))); + } + + #[tokio::test] + async fn get_my_stats_handler_not_found() { + use crate::domains::users::handlers::get_my_stats; + use crate::tests::helpers::{mock_user, test_app_state}; + use axum::extract::State; + use axum::Extension; + + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([Vec::::new()]) + .into_connection(); + let state = test_app_state(db); + let user = mock_user(); + let result = get_my_stats(State(state), Extension(user)).await; + assert!(matches!(result, Err(crate::AppError::NotFound(_)))); + } + + #[tokio::test] + async fn list_user_spots_empty() { + let mk_cnt = |n: i64| { + let mut m = std::collections::BTreeMap::new(); + m.insert("num_items".to_string(), sea_orm::Value::BigInt(Some(n))); + m + }; + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([[mk_cnt(0)]]) + .append_query_results([Vec::::new()]) + .append_query_results([Vec::::new()]) + .into_connection(); + let pagination = crate::utils::pagination::Pagination::new(1, 20); + let result = service::list_user_spots(&db, fixtures::test_uuid(10), pagination).await; + assert!(result.is_ok(), "unexpected err: {:?}", result.err()); + assert!(result.unwrap().data.is_empty()); + } + + #[tokio::test] + async fn list_user_solutions_empty() { + let mk_cnt = |n: i64| { + let mut m = std::collections::BTreeMap::new(); + m.insert("num_items".to_string(), sea_orm::Value::BigInt(Some(n))); + m + }; + let db = MockDatabase::new(DatabaseBackend::Postgres) + .append_query_results([[mk_cnt(0)]]) + .append_query_results([Vec::::new()]) + .into_connection(); + let pagination = crate::utils::pagination::Pagination::new(1, 20); + let result = service::list_user_solutions(&db, fixtures::test_uuid(10), pagination).await; + assert!(result.is_ok()); + assert!(result.unwrap().data.is_empty()); + } } #[cfg(test)] diff --git a/packages/api-server/src/openapi.rs b/packages/api-server/src/openapi.rs index 15b9b47b..2a9e8aa1 100644 --- a/packages/api-server/src/openapi.rs +++ b/packages/api-server/src/openapi.rs @@ -24,6 +24,9 @@ use utoipa::OpenApi; crate::domains::users::handlers::get_my_spots, crate::domains::users::handlers::get_my_solutions, crate::domains::users::handlers::get_my_social_accounts, + crate::domains::users::handlers::follow_user_handler, + crate::domains::users::handlers::unfollow_user_handler, + crate::domains::users::handlers::get_follow_status, crate::domains::categories::handlers::get_categories, crate::domains::subcategories::handlers::get_all_subcategories, crate::domains::subcategories::handlers::get_subcategories_by_category, diff --git a/packages/api-server/src/tests/fixtures.rs b/packages/api-server/src/tests/fixtures.rs index cf1a507e..ff3b2370 100644 --- a/packages/api-server/src/tests/fixtures.rs +++ b/packages/api-server/src/tests/fixtures.rs @@ -56,6 +56,8 @@ pub fn user_model() -> crate::entities::users::Model { rank: "user".to_string(), total_points: 100, is_admin: false, + ink_credits: 0, + style_dna: None, created_at: test_timestamp(), updated_at: test_timestamp(), } diff --git a/packages/web/lib/supabase/queries/debug/posts.ts b/packages/web/lib/supabase/queries/debug/posts.ts deleted file mode 100644 index 59fd4584..00000000 --- a/packages/web/lib/supabase/queries/debug/posts.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * DEBUG ONLY: Client-side query functions for posts table - */ - -import { supabaseBrowserClient } from "../../client"; -import type { PostRow } from "../../types"; - -export async function fetchLatestPosts(limit = 10): Promise { - const { data, error } = await supabaseBrowserClient - .from("posts") - .select("*") - .eq("status", "active") - .order("created_at", { ascending: false }) - .limit(limit); - - if (error) throw error; - return (data ?? []) as PostRow[]; -}