use axum::{ extract::{Query, State}, http::StatusCode, Json, }; use serde::Deserialize; use api_types::{requests::FollowRemoteRequest, responses::RemoteActorResponse}; use crate::{errors::ApiError, extractors::AuthUser, state::AppState}; #[derive(Deserialize)] pub struct LookupQuery { pub handle: String, } pub async fn lookup_handler( State(s): State, Query(q): Query, ) -> Result, ApiError> { let actor = s.federation.lookup_actor(&q.handle).await?; Ok(Json(RemoteActorResponse { handle: actor.handle, display_name: actor.display_name, avatar_url: actor.avatar_url, url: actor.url, })) } pub async fn follow_remote_handler( State(s): State, AuthUser(uid): AuthUser, Json(body): Json, ) -> Result { s.federation.follow_remote(&uid, &body.handle).await?; Ok(StatusCode::NO_CONTENT) } #[cfg(test)] mod tests { use super::*; use async_trait::async_trait; use axum::{ body::Body, http::{Request, StatusCode}, routing::{get, post}, Router, }; use domain::{ errors::DomainError, ports::{AuthService, GeneratedToken, PasswordHasher}, testing::TestStore, value_objects::{PasswordHash, UserId}, }; use std::sync::Arc; use tower::ServiceExt; struct NoOpAuth; impl AuthService for NoOpAuth { fn generate_token(&self, _uid: &UserId) -> Result { Err(DomainError::Internal("noop".into())) } fn validate_token(&self, _token: &str) -> Result { Err(DomainError::Unauthorized) } } struct NoOpHasher; #[async_trait] impl PasswordHasher for NoOpHasher { async fn hash(&self, _plain: &str) -> Result { Err(DomainError::Internal("noop".into())) } async fn verify(&self, _plain: &str, _hash: &PasswordHash) -> Result { Ok(false) } } fn make_state() -> crate::state::AppState { let store = Arc::new(TestStore::default()); crate::state::AppState { users: store.clone(), thoughts: store.clone(), likes: store.clone(), boosts: store.clone(), follows: store.clone(), blocks: store.clone(), tags: store.clone(), api_keys: store.clone(), top_friends: store.clone(), notifications: store.clone(), remote_actors: store.clone(), feed: store.clone(), search: store.clone(), auth: Arc::new(NoOpAuth), hasher: Arc::new(NoOpHasher), events: store.clone(), federation: store.clone(), } } fn app() -> Router { Router::new() .route("/federation/lookup", get(lookup_handler)) .route("/federation/follow", post(follow_remote_handler)) .with_state(make_state()) } #[tokio::test] async fn lookup_unknown_handle_returns_404() { let req = Request::builder() .uri("/federation/lookup?handle=%40alice%40example.com") .body(Body::empty()) .unwrap(); let resp = app().oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn follow_remote_without_auth_returns_401() { let req = Request::builder() .method("POST") .uri("/federation/follow") .header("content-type", "application/json") .body(Body::from(r#"{"handle":"@alice@example.com"}"#)) .unwrap(); let resp = app().oneshot(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } }