feat(ayarlar): hane ve üyeler, davet bağlantısı, hesap ve şifre, bütçe varsayılanları
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
//! 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<String>) {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user