Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/api-server/src/domains/posts/dto_tests.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
2 changes: 2 additions & 0 deletions packages/api-server/src/domains/posts/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
8 changes: 8 additions & 0 deletions packages/api-server/src/domains/users/dto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
};
Expand All @@ -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,
};
Expand Down
36 changes: 0 additions & 36 deletions packages/api-server/src/domains/users/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<i64>("", "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::<i64>("", "cnt").unwrap_or(0))
.unwrap_or(0);

Ok(UserStatsResponse {
user_id,
total_posts,
Expand Down
245 changes: 241 additions & 4 deletions packages/api-server/src/domains/users/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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::<std::collections::BTreeMap<String, sea_orm::Value>>::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);
Expand Down Expand Up @@ -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::<crate::entities::users::Model>::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::<std::collections::BTreeMap<String, sea_orm::Value>>::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::<crate::entities::users::Model>::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::<std::collections::BTreeMap<String, sea_orm::Value>>::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::<crate::entities::users::Model>::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::<crate::entities::users::Model>::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::<crate::entities::users::Model>::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::<crate::entities::spots::Model>::new()])
.append_query_results([Vec::<crate::entities::posts::Model>::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::<crate::entities::solutions::Model>::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)]
Expand Down
3 changes: 3 additions & 0 deletions packages/api-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/api-server/src/tests/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand Down
Loading
Loading