learn and share notes on atproto (wip) 馃 malfestio.stormlightlabs.org/
readability solid axum atproto srs
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 219 lines 8.1 kB view raw
1use crate::middleware::auth::UserContext; 2use crate::repository::preferences::{PreferencesRepoError, UpdatePreferences}; 3use crate::state::SharedState; 4 5use axum::{ 6 Json, 7 extract::{Extension, State}, 8 http::StatusCode, 9 response::IntoResponse, 10}; 11use serde::Deserialize; 12use serde_json::json; 13 14#[derive(Deserialize)] 15pub struct UpdatePreferencesRequest { 16 pub persona: Option<String>, 17 pub complete_onboarding: Option<bool>, 18 pub tutorial_deck_completed: Option<bool>, 19 pub density_mode: Option<String>, 20} 21 22/// GET /api/preferences - Get current user preferences 23pub async fn get_preferences( 24 State(state): State<SharedState>, ctx: Option<Extension<UserContext>>, 25) -> impl IntoResponse { 26 let user = match ctx { 27 Some(Extension(user)) => user, 28 None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "Unauthorized"}))).into_response(), 29 }; 30 31 let result = state.prefs_repo.get_or_create(&user.did).await; 32 33 match result { 34 Ok(prefs) => Json(prefs).into_response(), 35 Err(e) => { 36 tracing::error!("Failed to get preferences: {:?}", e); 37 ( 38 StatusCode::INTERNAL_SERVER_ERROR, 39 Json(json!({"error": "Failed to get preferences"})), 40 ) 41 .into_response() 42 } 43 } 44} 45 46/// PUT /api/preferences - Update user preferences 47pub async fn update_preferences( 48 State(state): State<SharedState>, ctx: Option<Extension<UserContext>>, 49 Json(payload): Json<UpdatePreferencesRequest>, 50) -> impl IntoResponse { 51 let user = match ctx { 52 Some(Extension(user)) => user, 53 None => return (StatusCode::UNAUTHORIZED, Json(json!({"error": "Unauthorized"}))).into_response(), 54 }; 55 56 let persona = if let Some(ref p) = payload.persona { 57 match p.parse() { 58 Ok(persona) => Some(persona), 59 Err(_) => { 60 return ( 61 StatusCode::BAD_REQUEST, 62 Json(json!({"error": "Invalid persona. Must be 'learner', 'creator', or 'curator'"})), 63 ) 64 .into_response(); 65 } 66 } 67 } else { 68 None 69 }; 70 71 let updates = UpdatePreferences { 72 persona, 73 complete_onboarding: payload.complete_onboarding, 74 tutorial_deck_completed: payload.tutorial_deck_completed, 75 density_mode: payload.density_mode, 76 }; 77 78 let result = state.prefs_repo.update(&user.did, updates).await; 79 80 match result { 81 Ok(prefs) => Json(prefs).into_response(), 82 Err(PreferencesRepoError::NotFound(msg)) => { 83 (StatusCode::NOT_FOUND, Json(json!({"error": msg}))).into_response() 84 } 85 Err(e) => { 86 tracing::error!("Failed to update preferences: {:?}", e); 87 ( 88 StatusCode::INTERNAL_SERVER_ERROR, 89 Json(json!({"error": "Failed to update preferences"})), 90 ) 91 .into_response() 92 } 93 } 94} 95 96#[cfg(test)] 97mod tests { 98 use super::*; 99 use crate::repository::preferences::PreferencesRepository; 100 use crate::repository::preferences::mock::MockPreferencesRepository; 101 use crate::state::{AppConfig, AppState, Repositories}; 102 use std::sync::Arc; 103 104 fn create_test_state_with_prefs(prefs_repo: Arc<dyn PreferencesRepository>) -> SharedState { 105 let pool = crate::db::create_mock_pool(); 106 let card_repo = Arc::new(crate::repository::card::mock::MockCardRepository::new()) 107 as Arc<dyn crate::repository::card::CardRepository>; 108 let note_repo = Arc::new(crate::repository::note::mock::MockNoteRepository::new()) 109 as Arc<dyn crate::repository::note::NoteRepository>; 110 let oauth_repo = Arc::new(crate::repository::oauth::mock::MockOAuthRepository::new()) 111 as Arc<dyn crate::repository::oauth::OAuthRepository>; 112 let social_repo = Arc::new(crate::repository::social::mock::MockSocialRepository::new()) 113 as Arc<dyn crate::repository::social::SocialRepository>; 114 let deck_repo = Arc::new(crate::repository::deck::mock::MockDeckRepository::new()) 115 as Arc<dyn crate::repository::deck::DeckRepository>; 116 let search_repo = Arc::new(crate::repository::search::mock::MockSearchRepository::new()) 117 as Arc<dyn crate::repository::search::SearchRepository>; 118 let review_repo = Arc::new(crate::repository::review::mock::MockReviewRepository::new()) 119 as Arc<dyn crate::repository::review::ReviewRepository>; 120 let sync_repo = Arc::new(crate::repository::sync::mock::MockSyncRepository::new()) 121 as Arc<dyn crate::repository::sync::SyncRepository>; 122 123 let config = AppConfig { pds_url: "https://bsky.social".to_string() }; 124 125 let repos = Repositories { 126 card: card_repo, 127 note: note_repo, 128 oauth: oauth_repo, 129 review: review_repo, 130 social: social_repo, 131 deck: deck_repo, 132 search: search_repo, 133 prefs: prefs_repo, 134 sync: sync_repo, 135 }; 136 137 AppState::new(pool, repos, config) 138 } 139 140 #[tokio::test] 141 async fn test_get_preferences_unauthorized() { 142 let prefs_repo = Arc::new(MockPreferencesRepository::new()) as Arc<dyn PreferencesRepository>; 143 let state = create_test_state_with_prefs(prefs_repo); 144 145 let response = get_preferences(State(state), None).await.into_response(); 146 assert_eq!(response.status(), StatusCode::UNAUTHORIZED); 147 } 148 149 #[tokio::test] 150 async fn test_get_preferences_success() { 151 let prefs_repo = Arc::new(MockPreferencesRepository::new()) as Arc<dyn PreferencesRepository>; 152 let state = create_test_state_with_prefs(prefs_repo); 153 154 let user = UserContext { 155 did: "did:plc:test".to_string(), 156 handle: "test.handle".to_string(), 157 access_token: "test_token".to_string(), 158 pds_url: "https://bsky.social".to_string(), 159 has_dpop: false, 160 }; 161 let response = get_preferences(State(state), Some(Extension(user))) 162 .await 163 .into_response(); 164 165 assert_eq!(response.status(), StatusCode::OK); 166 } 167 168 #[tokio::test] 169 async fn test_update_preferences_set_persona() { 170 let prefs_repo = Arc::new(MockPreferencesRepository::new()) as Arc<dyn PreferencesRepository>; 171 let state = create_test_state_with_prefs(prefs_repo); 172 173 let user = UserContext { 174 did: "did:plc:test".to_string(), 175 handle: "test.handle".to_string(), 176 access_token: "test_token".to_string(), 177 pds_url: "https://bsky.social".to_string(), 178 has_dpop: false, 179 }; 180 let payload = UpdatePreferencesRequest { 181 persona: Some("creator".to_string()), 182 complete_onboarding: Some(true), 183 tutorial_deck_completed: None, 184 density_mode: None, 185 }; 186 187 let response = update_preferences(State(state), Some(Extension(user)), Json(payload)) 188 .await 189 .into_response(); 190 191 assert_eq!(response.status(), StatusCode::OK); 192 } 193 194 #[tokio::test] 195 async fn test_update_preferences_invalid_persona() { 196 let prefs_repo = Arc::new(MockPreferencesRepository::new()) as Arc<dyn PreferencesRepository>; 197 let state = create_test_state_with_prefs(prefs_repo); 198 199 let user = UserContext { 200 did: "did:plc:test".to_string(), 201 handle: "test.handle".to_string(), 202 access_token: "test_token".to_string(), 203 pds_url: "https://bsky.social".to_string(), 204 has_dpop: false, 205 }; 206 let payload = UpdatePreferencesRequest { 207 persona: Some("invalid".to_string()), 208 complete_onboarding: None, 209 tutorial_deck_completed: None, 210 density_mode: None, 211 }; 212 213 let response = update_preferences(State(state), Some(Extension(user)), Json(payload)) 214 .await 215 .into_response(); 216 217 assert_eq!(response.status(), StatusCode::BAD_REQUEST); 218 } 219}