Files
kakeibo/backend/tests/plan_notes.rs
T
suphicakir 7f3fde074c feat(plan): sabit gelir/gider kalemleri ve dönem notları (backend)
- /api/periods/{id}/plan-items: listele, ekle; /api/plan-items/{id}: düzenle, sil
- Önceki dönemden kopyala: göreli gün korunur, dönem sonuna kırpılır, aynı kalem tekrar eklenmez
- /api/periods/{id}/notes (planning | review), /api/notes/{id}: Markdown notlar
- Kapalı dönemde değişiklik yok; silme geri alınabilir şekilde işaretlenir
- Entegrasyon testleri (5): kurallar, kopyalama, salt okunur dönem, hane izolasyonu

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 03:34:35 +03:00

376 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Plan kalemleri ve notlar.
mod common;
use axum::http::StatusCode;
use common::{Session, TestApp};
use mongodb::bson::{Document, doc};
use serde_json::{Value, json};
async fn open_period(app: &TestApp, s: &Session, start: &str, end: &str) -> String {
let (st, p) = app
.post(
"/periods",
s,
json!({ "start_date": start, "end_date": end }),
)
.await;
assert_eq!(st, StatusCode::CREATED, "{p}");
p["id"].as_str().unwrap().to_string()
}
async fn close_period(app: &TestApp, id: &str) {
let uuid = mongodb::bson::Uuid::parse_str(id).unwrap();
app.db
.collection::<Document>("periods")
.update_one(
doc! { "_id": uuid },
doc! { "$set": { "status": "closed" } },
)
.await
.unwrap();
}
fn names(list: &Value) -> Vec<String> {
list.as_array()
.unwrap()
.iter()
.map(|i| {
format!(
"{}:{}",
i["kind"].as_str().unwrap(),
i["name"].as_str().unwrap()
)
})
.collect()
}
#[tokio::test]
async fn plan_items_crud_and_validation() {
let app = TestApp::new().await;
let s = app.register("Test", None).await;
let pid = open_period(&app, &s, "2026-12-01", "2027-01-01").await;
let items = format!("/periods/{pid}/plan-items");
// Gider önce eklense de liste gelirle başlar
let (st, rent) = app
.post(
&items,
&s,
json!({ "kind": "expense", "name": "Kira", "amount": 1_500_000, "date": "2026-12-05" }),
)
.await;
assert_eq!(st, StatusCode::CREATED, "{rent}");
app.post(
&items,
&s,
json!({ "kind": "income", "name": "Maaş", "amount": 4_500_000 }),
)
.await;
app.post(
&items,
&s,
json!({ "kind": "expense", "name": "İnternet", "amount": 45_000 }),
)
.await;
let (_, list) = app.get(&items, &s).await;
assert_eq!(
names(&list),
vec!["income:Maaş", "expense:Kira", "expense:İnternet"]
);
assert_eq!(list[0]["date"], Value::Null);
// Doğrulamalar
for bad in [
json!({ "kind": "expense", "name": "X", "amount": 0 }),
json!({ "kind": "expense", "name": " ", "amount": 100 }),
json!({ "kind": "expense", "name": "X", "amount": 100, "date": "2027-01-01" }), // dönem dışı (bitiş hariç)
json!({ "kind": "expense", "name": "X", "amount": 100, "date": "05.12.2026" }),
json!({ "kind": "gift", "name": "X", "amount": 100 }),
] {
let (st, _) = app.post(&items, &s, bad.clone()).await;
assert_eq!(st, StatusCode::BAD_REQUEST, "{bad}");
}
// Düzenle: tutar + tarihi kaldır; eski sürüm → 409
let rent_path = format!("/plan-items/{}", rent["id"].as_str().unwrap());
let (st, upd) = app
.patch(
&rent_path,
&s,
json!({ "amount": 1_600_000, "date": "", "version": 1 }),
)
.await;
assert_eq!(st, StatusCode::OK, "{upd}");
assert_eq!(
(upd["amount"].as_i64(), &upd["date"]),
(Some(1_600_000), &Value::Null)
);
let (st, _) = app
.patch(&rent_path, &s, json!({ "name": "Y", "version": 1 }))
.await;
assert_eq!(st, StatusCode::CONFLICT);
// Sil → listeden çıkar
assert_eq!(app.delete(&rent_path, &s).await.0, StatusCode::NO_CONTENT);
let (_, list) = app.get(&items, &s).await;
assert_eq!(names(&list), vec!["income:Maaş", "expense:İnternet"]);
app.cleanup().await;
}
#[tokio::test]
async fn copy_previous_shifts_dates_and_skips_duplicates() {
let app = TestApp::new().await;
let s = app.register("Test", None).await;
let dec = open_period(&app, &s, "2026-12-01", "2027-01-01").await;
let dec_items = format!("/periods/{dec}/plan-items");
app.post(
&dec_items,
&s,
json!({ "kind": "income", "name": "Maaş", "amount": 4_500_000, "date": "2026-12-15" }),
)
.await;
app.post(
&dec_items,
&s,
json!({ "kind": "expense", "name": "Kira", "amount": 1_500_000, "date": "2026-12-05" }),
)
.await;
app.post(
&dec_items,
&s,
json!({ "kind": "expense", "name": "Aidat", "amount": 80_000 }),
)
.await;
app.post(
&dec_items,
&s,
json!({ "kind": "expense", "name": "Fatura", "amount": 90_000, "date": "2026-12-31" }),
)
.await;
close_period(&app, &dec).await;
let jan = open_period(&app, &s, "2027-01-01", "2027-02-01").await;
let jan_items = format!("/periods/{jan}/plan-items");
// Kullanıcı kirayı elle zaten eklemiş (farklı tutarla) → kopyada atlanmalı
app.post(
&jan_items,
&s,
json!({ "kind": "expense", "name": "kira", "amount": 1_700_000 }),
)
.await;
let (st, res) = app
.post(&format!("{jan_items}/copy-previous"), &s, json!({}))
.await;
assert_eq!(st, StatusCode::OK, "{res}");
assert_eq!(res["added"], 3);
let (_, list) = app.get(&jan_items, &s).await;
let by_name = |n: &str| {
list.as_array()
.unwrap()
.iter()
.find(|i| i["name"] == n)
.unwrap()
.clone()
};
assert_eq!(by_name("Maaş")["date"], "2027-01-15", "göreli gün korunur");
assert_eq!(by_name("Aidat")["date"], Value::Null);
assert_eq!(by_name("kira")["amount"], 1_700_000, "elle eklenen korunur");
assert_eq!(by_name("Fatura")["date"], "2027-01-31");
// Ocak → Şubat (28 gün): 31 Ocak'taki fatura dönem dışına taşar → son güne (28 Şubat) çekilir
close_period(&app, &jan).await;
let feb = open_period(&app, &s, "2027-02-01", "2027-03-01").await;
let feb_items = format!("/periods/{feb}/plan-items");
app.post(&format!("{feb_items}/copy-previous"), &s, json!({}))
.await;
let (_, feb_list) = app.get(&feb_items, &s).await;
let fatura = feb_list
.as_array()
.unwrap()
.iter()
.find(|i| i["name"] == "Fatura")
.unwrap();
assert_eq!(fatura["date"], "2027-02-28");
// İkinci kez → hiçbir şey eklenmez (Ocak kapalı olduğu için Şubat'ta deniyoruz)
let (_, res) = app
.post(&format!("{feb_items}/copy-previous"), &s, json!({}))
.await;
assert_eq!(res["added"], 0);
// İlk dönemde önceki yok → 400
let (st, _) = app
.post(&format!("{dec_items}/copy-previous"), &s, json!({}))
.await;
assert!(
st == StatusCode::BAD_REQUEST || st == StatusCode::CONFLICT,
"{st}"
);
app.cleanup().await;
}
#[tokio::test]
async fn closed_period_is_read_only() {
let app = TestApp::new().await;
let s = app.register("Test", None).await;
let pid = open_period(&app, &s, "2026-12-01", "2027-01-01").await;
let (_, item) = app
.post(
&format!("/periods/{pid}/plan-items"),
&s,
json!({ "kind": "income", "name": "Maaş", "amount": 100 }),
)
.await;
let (_, note) = app
.post(
&format!("/periods/{pid}/notes"),
&s,
json!({ "context": "planning", "title": "Hedef" }),
)
.await;
close_period(&app, &pid).await;
let item_path = format!("/plan-items/{}", item["id"].as_str().unwrap());
let note_path = format!("/notes/{}", note["id"].as_str().unwrap());
let checks = [
app.post(
&format!("/periods/{pid}/plan-items"),
&s,
json!({ "kind": "income", "name": "X", "amount": 1 }),
)
.await
.0,
app.patch(&item_path, &s, json!({ "amount": 5 })).await.0,
app.delete(&item_path, &s).await.0,
app.post(
&format!("/periods/{pid}/notes"),
&s,
json!({ "context": "review", "title": "X" }),
)
.await
.0,
app.patch(&note_path, &s, json!({ "title": "Y" })).await.0,
app.delete(&note_path, &s).await.0,
];
assert!(
checks.iter().all(|st| *st == StatusCode::CONFLICT),
"{checks:?}"
);
// Okuma serbest
assert_eq!(
app.get(&format!("/periods/{pid}/plan-items"), &s).await.0,
StatusCode::OK
);
assert_eq!(
app.get(&format!("/periods/{pid}/notes"), &s).await.0,
StatusCode::OK
);
app.cleanup().await;
}
#[tokio::test]
async fn notes_crud_and_context_filter() {
let app = TestApp::new().await;
let s = app.register("Test", None).await;
let pid = open_period(&app, &s, "2026-12-01", "2027-01-01").await;
let notes = format!("/periods/{pid}/notes");
let (st, n) = app
.post(&notes, &s, json!({ "context": "planning", "title": " Tatil birikimi ", "body_md": "- Ocak'ta **5.000 ₺**" }))
.await;
assert_eq!(st, StatusCode::CREATED, "{n}");
assert_eq!(n["title"], "Tatil birikimi");
app.post(&notes, &s, json!({ "context": "review", "title": "Ders" }))
.await;
let (_, planning) = app.get(&format!("{notes}?context=planning"), &s).await;
assert_eq!(planning.as_array().unwrap().len(), 1);
let (_, all) = app.get(&notes, &s).await;
assert_eq!(all.as_array().unwrap().len(), 2);
let (st, _) = app
.post(&notes, &s, json!({ "context": "planning", "title": "" }))
.await;
assert_eq!(st, StatusCode::BAD_REQUEST, "boş başlık");
let path = format!("/notes/{}", n["id"].as_str().unwrap());
let (st, upd) = app
.patch(&path, &s, json!({ "body_md": "Güncel", "version": 1 }))
.await;
assert_eq!(
(st, upd["body_md"].as_str()),
(StatusCode::OK, Some("Güncel"))
);
assert_eq!(app.delete(&path, &s).await.0, StatusCode::NO_CONTENT);
let (_, planning) = app.get(&format!("{notes}?context=planning"), &s).await;
assert_eq!(planning, json!([]));
app.cleanup().await;
}
#[tokio::test]
async fn plan_items_and_notes_are_isolated() {
let app = TestApp::new().await;
let a = app.register("A", None).await;
let b = app.register("B", None).await;
let pid = open_period(&app, &a, "2026-12-01", "2027-01-01").await;
let (_, item) = app
.post(
&format!("/periods/{pid}/plan-items"),
&a,
json!({ "kind": "income", "name": "Maaş", "amount": 100 }),
)
.await;
let (_, note) = app
.post(
&format!("/periods/{pid}/notes"),
&a,
json!({ "context": "planning", "title": "Gizli" }),
)
.await;
let item_path = format!("/plan-items/{}", item["id"].as_str().unwrap());
let note_path = format!("/notes/{}", note["id"].as_str().unwrap());
let checks = [
app.get(&format!("/periods/{pid}/plan-items"), &b).await.0,
app.post(
&format!("/periods/{pid}/plan-items"),
&b,
json!({ "kind": "income", "name": "X", "amount": 1 }),
)
.await
.0,
app.post(
&format!("/periods/{pid}/plan-items/copy-previous"),
&b,
json!({}),
)
.await
.0,
app.patch(&item_path, &b, json!({ "amount": 1 })).await.0,
app.delete(&item_path, &b).await.0,
app.get(&format!("/periods/{pid}/notes"), &b).await.0,
app.patch(&note_path, &b, json!({ "title": "X" })).await.0,
app.delete(&note_path, &b).await.0,
];
assert!(
checks.iter().all(|st| *st == StatusCode::NOT_FOUND),
"{checks:?}"
);
// A'nın verisi değişmedi
let (_, list) = app.get(&format!("/periods/{pid}/plan-items"), &a).await;
assert_eq!(list[0]["amount"], 100);
assert_eq!(list[0]["version"], 1);
app.cleanup().await;
}