From 0e2b72b77a5e6d977f02385181e497556348dcf8 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 28 May 2026 03:46:52 +0200 Subject: [PATCH] feat(presentation): add GET /users/me/friends handler and route --- .../presentation/src/handlers/social/mod.rs | 38 +++++++++++++++++-- .../presentation/src/handlers/social/tests.rs | 21 +++++++++- crates/presentation/src/openapi/social.rs | 1 + crates/presentation/src/routes.rs | 1 + 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/crates/presentation/src/handlers/social/mod.rs b/crates/presentation/src/handlers/social/mod.rs index b642c8f..79a6ff2 100644 --- a/crates/presentation/src/handlers/social/mod.rs +++ b/crates/presentation/src/handlers/social/mod.rs @@ -4,11 +4,15 @@ use crate::{ errors::ApiError, extractors::{AuthUser, Deps}, }; -use api_types::requests::SetTopFriendsRequest; -use api_types::responses::TopFriendsResponse; +use api_types::requests::{PaginationQuery, SetTopFriendsRequest}; +use api_types::responses::{PagedResponse, TopFriendsResponse, UserResponse}; use application::use_cases::profile::{get_top_friends, get_user_by_username, set_top_friends}; use application::use_cases::social::*; -use axum::{extract::Path, http::StatusCode, Json}; +use axum::{ + extract::{Path, Query}, + http::StatusCode, + Json, +}; use domain::{ ports::{ BlockRepository, BoostRepository, EventPublisher, FederationActionPort, FollowRepository, @@ -150,5 +154,33 @@ pub async fn get_top_friends_handler( Ok(Json(TopFriendsResponse { top_friends })) } +#[utoipa::path( + get, path = "/users/me/friends", + params(PaginationQuery), + responses( + (status = 200, description = "Local mutual follows (paginated)", body = inline(PagedResponse)), + (status = 401, description = "Unauthorized"), + ), + security(("bearer_auth" = [])) +)] +pub async fn get_friends_handler( + Deps(d): Deps, + AuthUser(uid): AuthUser, + Query(q): Query, +) -> Result>, ApiError> { + use domain::models::feed::PageParams; + let page = PageParams { + page: q.page(), + per_page: q.per_page(), + }; + let result = get_local_friends(&*d.follows, &uid, &page).await?; + Ok(Json(PagedResponse { + items: result.items.iter().map(to_user_response).collect(), + total: result.total, + page: result.page, + per_page: result.per_page, + })) +} + #[cfg(test)] mod tests; diff --git a/crates/presentation/src/handlers/social/tests.rs b/crates/presentation/src/handlers/social/tests.rs index ef61d51..3d6dcb1 100644 --- a/crates/presentation/src/handlers/social/tests.rs +++ b/crates/presentation/src/handlers/social/tests.rs @@ -1,9 +1,10 @@ +use super::get_friends_handler; use super::*; use crate::testing::make_state; use axum::{ body::Body, http::Request, - routing::{delete, post}, + routing::{delete, get, post}, Router, }; use tower::ServiceExt; @@ -32,6 +33,24 @@ async fn follow_without_auth_returns_401() { assert_eq!(resp.status(), 401); } +#[tokio::test] +async fn get_friends_without_auth_returns_401() { + let app = Router::new() + .route("/users/me/friends", get(get_friends_handler)) + .with_state(make_state()); + let resp = app + .oneshot( + Request::builder() + .method("GET") + .uri("/users/me/friends") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(resp.status(), 401); +} + #[tokio::test] async fn unfollow_remote_without_auth_returns_401() { let resp = app() diff --git a/crates/presentation/src/openapi/social.rs b/crates/presentation/src/openapi/social.rs index ab90680..86f66cf 100644 --- a/crates/presentation/src/openapi/social.rs +++ b/crates/presentation/src/openapi/social.rs @@ -14,6 +14,7 @@ use utoipa::OpenApi; crate::handlers::social::delete_block, crate::handlers::social::put_top_friends, crate::handlers::social::get_top_friends_handler, + crate::handlers::social::get_friends_handler, ), components(schemas(SetTopFriendsRequest)) )] diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs index ead9440..7ae5721 100644 --- a/crates/presentation/src/routes.rs +++ b/crates/presentation/src/routes.rs @@ -26,6 +26,7 @@ pub fn router() -> Router { put(users::upload_banner).layer(DefaultBodyLimit::max(10 * 1024 * 1024)), ) .route("/users/me/following", get(users::get_me_following)) + .route("/users/me/friends", get(social::get_friends_handler)) .route("/users/me/top-friends", put(social::put_top_friends)) .route("/users/{username}", get(users::get_user)) .route(