//! Hesap ve hane ayarları: profil adı, şifre değiştirme (diğer oturumlar kapanır), //! hane adı + bütçe varsayılanları, üye çıkarma, yetkiler. mod common; use std::time::Duration; use axum::http::{Method, StatusCode}; use common::{Session, TestApp}; use serde_json::{Value, json}; async fn login(app: &TestApp, email: &str, password: &str) -> (StatusCode, Option) { let (st, _, cookie) = app .send( Method::POST, "/auth/login", None, Some(json!({ "email": email, "password": password, "remember_me": true })), ) .await; (st, cookie) } async fn me(app: &TestApp, s: &Session) -> (StatusCode, Value) { app.get("/auth/me", s).await } fn with_cookie(s: &Session, cookie: String) -> Session { Session { cookie, household_id: s.household_id.clone(), } } #[tokio::test] async fn profile_name_and_password_change_closes_other_sessions() { let app = TestApp::new().await; let a = app.register("Ayse", None).await; let (_, m) = me(&app, &a).await; let email = m["user"]["email"].as_str().unwrap().to_string(); // Ad let (st, m) = app .patch("/auth/me", &a, json!({ "name": " Ayşe Ç. " })) .await; assert_eq!( (st, m["user"]["name"].as_str()), (StatusCode::OK, Some("Ayşe Ç.")) ); let (st, _) = app.patch("/auth/me", &a, json!({ "name": " " })).await; assert_eq!(st, StatusCode::BAD_REQUEST); // İkinci cihaz let (st, other) = login(&app, &email, "test-sifre-123").await; assert_eq!(st, StatusCode::OK); let other = with_cookie(&a, other.unwrap()); tokio::time::sleep(Duration::from_millis(1100)).await; // iat saniye hassasiyetinde // Yanlış mevcut şifre / kısa / aynı şifre let pw = |cur: &str, new: &str| json!({ "current_password": cur, "new_password": new }); let (st, b) = app .post( "/auth/me/password", &a, pw("yanlis-sifre", "yeni-sifre-456"), ) .await; assert_eq!(st, StatusCode::BAD_REQUEST); assert_eq!(b["error"]["message"], "Mevcut şifre hatalı"); let (st, _) = app .post("/auth/me/password", &a, pw("test-sifre-123", "kisa")) .await; assert_eq!(st, StatusCode::BAD_REQUEST); let (st, b) = app .post( "/auth/me/password", &a, pw("test-sifre-123", "test-sifre-123"), ) .await; assert_eq!(st, StatusCode::BAD_REQUEST, "{b}"); // Doğru → bu cihaz yeni çerezle devam, diğer cihaz düşer let (st, _, cookie) = app .send( Method::POST, "/auth/me/password", Some(&a.cookie), Some(pw("test-sifre-123", "yeni-sifre-456")), ) .await; assert_eq!(st, StatusCode::NO_CONTENT); let fresh = with_cookie(&a, cookie.expect("yeni oturum çerezi")); assert_eq!(me(&app, &fresh).await.0, StatusCode::OK); assert_eq!( me(&app, &other).await.0, StatusCode::UNAUTHORIZED, "diğer cihaz" ); assert_eq!(me(&app, &a).await.0, StatusCode::UNAUTHORIZED, "eski çerez"); // Giriş: eski şifre geçmez, yeni geçer assert_eq!( login(&app, &email, "test-sifre-123").await.0, StatusCode::UNAUTHORIZED ); assert_eq!( login(&app, &email, "yeni-sifre-456").await.0, StatusCode::OK ); app.cleanup().await; } #[tokio::test] async fn wrong_current_password_is_rate_limited() { let app = TestApp::new().await; let a = app.register("Ayse", None).await; let body = json!({ "current_password": "yanlis", "new_password": "yeni-sifre-456" }); for _ in 0..5 { let (st, _) = app.post("/auth/me/password", &a, body.clone()).await; assert_eq!(st, StatusCode::BAD_REQUEST); } let (st, _) = app .post( "/auth/me/password", &a, json!({ "current_password": "test-sifre-123", "new_password": "yeni-sifre-456" }), ) .await; assert_eq!(st, StatusCode::TOO_MANY_REQUESTS, "doğru şifre de bekler"); app.cleanup().await; } #[tokio::test] async fn household_settings_and_budget_default_split() { let app = TestApp::new().await; let owner = app.register("Sahip", None).await; let (_, inv) = app.post("/household/invites", &owner, json!({})).await; let member = app .register("Uye", Some(inv["code"].as_str().unwrap())) .await; let (_, h) = app.get("/household", &owner).await; assert_eq!( h["settings"], json!({ "budget_split_pct": { "needs": 50, "wants": 30, "culture": 10, "unexpected": 10 }, "budget_suggestion": "default" }) ); let update = json!({ "name": "Çakır Ailesi", "budget_split_pct": { "needs": 60, "wants": 20, "culture": 10, "unexpected": 10 }, "budget_suggestion": "avg3", }); let (st, _) = app.patch("/household", &member, update.clone()).await; assert_eq!(st, StatusCode::FORBIDDEN, "üye değiştiremez"); let (st, h) = app.patch("/household", &owner, update).await; assert_eq!(st, StatusCode::OK, "{h}"); assert_eq!(h["name"], "Çakır Ailesi"); assert_eq!(h["settings"]["budget_split_pct"]["needs"], 60); assert_eq!(h["settings"]["budget_suggestion"], "avg3"); let (_, m) = me(&app, &member).await; assert_eq!( m["household"]["name"], "Çakır Ailesi", "üye de yeni adı görür" ); // Toplam 100 değil / geçersiz yöntem for bad in [ json!({ "budget_split_pct": { "needs": 60, "wants": 30, "culture": 10, "unexpected": 10 } }), json!({ "budget_suggestion": "hepsi" }), json!({ "name": "" }), ] { let (st, b) = app.patch("/household", &owner, bad).await; assert_eq!(st, StatusCode::BAD_REQUEST, "{b}"); } // Varsayılan öneri yeni yüzdeleri kullanır: harcanabilir 100.000 → 60/20/10/10 let (_, p) = app .post( "/periods", &owner, json!({ "start_date": "2026-11-01", "end_date": "2026-12-01" }), ) .await; let pid = p["id"].as_str().unwrap(); app.post( &format!("/periods/{pid}/plan-items"), &owner, json!({ "kind": "income", "name": "Maaş", "amount": 100_000 }), ) .await; let (_, sug) = app .get( &format!("/periods/{pid}/budget/suggestion?method=default"), &owner, ) .await; assert_eq!( sug["amounts"], json!({ "needs": 60_000, "wants": 20_000, "culture": 10_000, "unexpected": 10_000 }) ); app.cleanup().await; } #[tokio::test] async fn owner_removes_member_who_can_be_invited_again() { let app = TestApp::new().await; let owner = app.register("Sahip", None).await; let (_, inv) = app.post("/household/invites", &owner, json!({})).await; let member = app .register("Uye", Some(inv["code"].as_str().unwrap())) .await; let (_, mm) = me(&app, &member).await; let member_id = mm["user"]["id"].as_str().unwrap().to_string(); let member_email = mm["user"]["email"].as_str().unwrap().to_string(); let (_, om) = me(&app, &owner).await; let owner_id = om["user"]["id"].as_str().unwrap().to_string(); // Üye kimseyi çıkaramaz; sahip kendini çıkaramaz; başka hane bulunamaz let (st, _) = app .delete(&format!("/household/members/{owner_id}"), &member) .await; assert_eq!(st, StatusCode::FORBIDDEN); let (st, _) = app .delete(&format!("/household/members/{owner_id}"), &owner) .await; assert_eq!(st, StatusCode::BAD_REQUEST); let stranger = app.register("Yabanci", None).await; let (st, _) = app .delete(&format!("/household/members/{member_id}"), &stranger) .await; assert_eq!(st, StatusCode::NOT_FOUND); // Çıkar → oturumu anında düşer, listede yok, giriş yapamaz let (st, _) = app .delete(&format!("/household/members/{member_id}"), &owner) .await; assert_eq!(st, StatusCode::NO_CONTENT); assert_eq!(me(&app, &member).await.0, StatusCode::UNAUTHORIZED); let (_, h) = app.get("/household", &owner).await; assert_eq!(h["members"].as_array().unwrap().len(), 1); assert_eq!( login(&app, &member_email, "test-sifre-123").await.0, StatusCode::UNAUTHORIZED ); // Aynı e-posta yeni davetle yeniden katılabilir let (_, inv) = app.post("/household/invites", &owner, json!({})).await; let (st, b, _) = app .send( Method::POST, "/auth/register", None, Some(json!({ "name": "Uye", "email": member_email, "password": "test-sifre-123", "invite_code": inv["code"], })), ) .await; assert_eq!(st, StatusCode::CREATED, "{b}"); app.cleanup().await; }