···2525 pub published: bool,
2626}
27272828-// TODO: add tests
2928pub async fn create_deck(
3029 State(state): State<SharedState>, ctx: Option<axum::Extension<UserContext>>, Json(payload): Json<CreateDeckRequest>,
3130) -> impl IntoResponse {
···80798180 match state.deck_repo.get(&id).await {
8281 Ok(deck) => {
8383- // Access control check
8482 let is_owner = user_did.as_ref() == Some(&deck.owner_did);
8583 let has_access = match &deck.visibility {
8684 Visibility::Public | Visibility::Unlisted => true,
···247245 }
248246 }
249247}
248248+249249+#[cfg(test)]
250250+mod tests {
251251+ use super::*;
252252+ use crate::middleware::auth::UserContext;
253253+ use crate::state::AppState;
254254+ use axum::extract::{Extension, State};
255255+ use axum::http::StatusCode;
256256+ use axum::response::IntoResponse;
257257+ use malfestio_core::model::Visibility;
258258+ use std::sync::Arc;
259259+260260+ #[tokio::test]
261261+ async fn test_create_deck_success() {
262262+ let pool = crate::db::create_pool("postgres://postgres:postgres@localhost:5432/malfestio").unwrap();
263263+264264+ let state = AppState::new_with_repos(
265265+ pool,
266266+ Arc::new(crate::repository::card::mock::MockCardRepository::new()),
267267+ Arc::new(crate::repository::note::mock::MockNoteRepository::new()),
268268+ Arc::new(crate::repository::oauth::mock::MockOAuthRepository::new()),
269269+ );
270270+271271+ let user = UserContext { did: "did:plc:alice".to_string(), handle: "alice.bsky.social".to_string() };
272272+273273+ let payload = CreateDeckRequest {
274274+ title: "My New Deck".to_string(),
275275+ description: "A test deck".to_string(),
276276+ tags: vec!["rust".to_string()],
277277+ visibility: Visibility::Public,
278278+ };
279279+280280+ let response = create_deck(State(state), Some(Extension(user)), Json(payload))
281281+ .await
282282+ .into_response();
283283+284284+ assert_eq!(response.status(), StatusCode::CREATED);
285285+286286+ let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
287287+ let body_json: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
288288+289289+ assert_eq!(body_json["title"], "My New Deck");
290290+ assert_eq!(body_json["owner_did"], "did:plc:alice");
291291+ assert_eq!(body_json["visibility"]["type"], "Public");
292292+ }
293293+294294+ #[tokio::test]
295295+ async fn test_create_deck_unauthorized() {
296296+ let pool = crate::db::create_pool("postgres://postgres:postgres@localhost:5432/malfestio").unwrap();
297297+ let state = AppState::new_with_repos(
298298+ pool,
299299+ Arc::new(crate::repository::card::mock::MockCardRepository::new()),
300300+ Arc::new(crate::repository::note::mock::MockNoteRepository::new()),
301301+ Arc::new(crate::repository::oauth::mock::MockOAuthRepository::new()),
302302+ );
303303+304304+ let payload = CreateDeckRequest {
305305+ title: "My New Deck".to_string(),
306306+ description: "A test deck".to_string(),
307307+ tags: vec![],
308308+ visibility: Visibility::Public,
309309+ };
310310+311311+ let response = create_deck(State(state), None, Json(payload)).await.into_response();
312312+313313+ assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
314314+ }
315315+}
+32-1
crates/server/src/api/importer.rs
···88 url: String,
99}
10101111-// TODO: add tests
1211pub async fn import_article(Json(payload): Json<ImportRequest>) -> impl IntoResponse {
1312 if payload.url.trim().is_empty() {
1413 return (StatusCode::BAD_REQUEST, Json(json!({"error": "URL is required"}))).into_response();
···3938 .into_response(),
4039 }
4140}
4141+4242+#[cfg(test)]
4343+mod tests {
4444+ use super::*;
4545+ use axum::http::StatusCode;
4646+ use axum::response::IntoResponse;
4747+4848+ #[tokio::test]
4949+ async fn test_import_article_wikipedia() {
5050+ let payload = ImportRequest { url: "https://www.rust-lang.org".to_string() };
5151+ let response = import_article(Json(payload)).await.into_response();
5252+ let status = response.status();
5353+ if status != StatusCode::OK {
5454+ let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
5555+ let body_str = String::from_utf8(body_bytes.to_vec()).unwrap();
5656+ panic!("Test failed with status {}. Body: {}", status, body_str);
5757+ }
5858+5959+ let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
6060+ let body_json: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
6161+ let title = body_json["title"].as_str().unwrap();
6262+ assert!(title.contains("Rust"));
6363+ assert!(body_json["text"].as_str().unwrap().len() > 100);
6464+ }
6565+6666+ #[tokio::test]
6767+ async fn test_import_article_empty_url() {
6868+ let payload = ImportRequest { url: " ".to_string() };
6969+ let response = import_article(Json(payload)).await.into_response();
7070+ assert_eq!(response.status(), StatusCode::BAD_REQUEST);
7171+ }
7272+}
+7-1
crates/server/src/api/social.rs
···252252 .into_response();
253253254254 assert_eq!(response.status(), StatusCode::OK);
255255- // TODO: parse body to verify content
255255+256256+ let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap();
257257+ let followers: Vec<String> = serde_json::from_slice(&body_bytes).unwrap();
258258+259259+ assert_eq!(followers.len(), 2);
260260+ assert!(followers.contains(&"did:plc:1".to_string()));
261261+ assert!(followers.contains(&"did:plc:2".to_string()));
256262 }
257263258264 #[tokio::test]
+11-5
crates/server/src/middleware/auth.rs
···11use crate::state::SharedState;
22+23use axum::{
34 extract::{Request, State},
45 http::{self},
···1415 pub handle: String,
1516}
16171717-/// Cache expiry time
1818-const CACHE_TTL: Duration = Duration::from_secs(300); // 5 minutes
1818+/// Cache expiry time (5 minutes)
1919+const CACHE_TTL: Duration = Duration::from_secs(300);
19202020-/// TODO: Cache or use signature verification for performance
2121+/// Delegated Authentication Strategy:
2222+///
2323+/// We verify the token by calling the PDS `getSession` endpoint.
2424+/// To improve performance, we cache the result for a short duration (TTL).
2525+/// This avoids validating the JWT signature locally, which simplifies key management
2626+/// (no need to fetch/rotate PDS public keys) while maintaining security via the PDS.
2727+///
2828+/// NOTE: This assumes the PDS is trusted.
2129pub async fn auth_middleware(State(state): State<SharedState>, mut req: Request, next: Next) -> Response {
2230 let auth_header = req.headers().get(http::header::AUTHORIZATION);
2331···5664 let body: serde_json::Value = response.json().await.unwrap_or_default();
5765 let did = body["did"].as_str().unwrap_or("").to_string();
5866 let handle = body["handle"].as_str().unwrap_or("").to_string();
5959-6067 let user_ctx = UserContext { did, handle };
61686262- // Update cache
6369 {
6470 let mut cache = state.auth_cache.write().await;
6571 cache.insert(token.to_string(), (user_ctx.clone(), Instant::now()));