use crate::{errors::ApiError, extractors::AuthUser, state::AppState}; use api_types::requests::NotificationUpdateRequest; use application::use_cases::notifications::{ list_notifications as uc_list_notifications, mark_all_notifications_read, mark_notification_read as uc_mark_notification_read, }; use axum::{ extract::{Path, State}, http::StatusCode, Json, }; use domain::{models::feed::PageParams, value_objects::NotificationId}; use uuid::Uuid; #[utoipa::path(get, path = "/notifications", responses((status = 200, description = "Notification summary")), security(("bearer_auth" = [])))] pub async fn list_notifications( State(s): State, AuthUser(uid): AuthUser, ) -> Result, ApiError> { let page = PageParams { page: 1, per_page: 20, }; let result = uc_list_notifications(&*s.notifications, &uid, page).await?; Ok(Json(serde_json::json!({ "total": result.total, "unread": result.items.iter().filter(|n| !n.read).count() }))) } #[utoipa::path(patch, path = "/notifications/{id}", params(("id" = uuid::Uuid, Path, description = "Notification ID")), request_body = NotificationUpdateRequest, responses((status = 204, description = "Marked read")), security(("bearer_auth" = [])))] pub async fn mark_notification_read( State(s): State, AuthUser(uid): AuthUser, Path(id): Path, Json(body): Json, ) -> Result { if body.read { uc_mark_notification_read(&*s.notifications, &NotificationId::from_uuid(id), &uid).await?; } Ok(StatusCode::NO_CONTENT) } #[utoipa::path(patch, path = "/notifications", request_body = NotificationUpdateRequest, responses((status = 204, description = "All marked read")), security(("bearer_auth" = [])))] pub async fn mark_all_read( State(s): State, AuthUser(uid): AuthUser, Json(body): Json, ) -> Result { if body.read { mark_all_notifications_read(&*s.notifications, &uid).await?; } Ok(StatusCode::NO_CONTENT) } #[cfg(test)] mod tests { use super::*; use async_trait::async_trait; use axum::{ body::Body, http::{header, Request}, routing::{get, patch}, 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("/notifications", patch(mark_all_read)) .route("/notifications/:id", patch(mark_notification_read)) .with_state(make_state()) } #[tokio::test] async fn patch_notification_without_auth_returns_401() { let resp = app() .oneshot( Request::builder() .method("PATCH") .uri("/notifications/00000000-0000-0000-0000-000000000001") .header(header::CONTENT_TYPE, "application/json") .body(Body::from(r#"{"read":true}"#)) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), 401); } #[tokio::test] async fn patch_all_without_auth_returns_401() { let resp = app() .oneshot( Request::builder() .method("PATCH") .uri("/notifications") .header(header::CONTENT_TYPE, "application/json") .body(Body::from(r#"{"read":true}"#)) .unwrap(), ) .await .unwrap(); assert_eq!(resp.status(), 401); } }