feat(categories): kategori ağacı, varsayılan Türkçe set ve yönetim ekranı
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { Types } from "mongoose";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { CategoryModel } from "@/models/category";
|
||||
import { setupSeededDb, teardownDb } from "@/test/seeded-db";
|
||||
import {
|
||||
createCategory,
|
||||
installDefaultCategories,
|
||||
listCategories,
|
||||
setCategoryArchived,
|
||||
updateCategory,
|
||||
} from "./service";
|
||||
|
||||
let ids: Awaited<ReturnType<typeof setupSeededDb>>;
|
||||
let cakir = "";
|
||||
let annemler = "";
|
||||
|
||||
beforeAll(async () => {
|
||||
ids = await setupSeededDb();
|
||||
cakir = ids.household("Çakır Ailesi");
|
||||
annemler = ids.household("Annemler");
|
||||
});
|
||||
afterAll(teardownDb);
|
||||
|
||||
const byKey = (key: string, householdId = cakir) =>
|
||||
CategoryModel.findOne({ householdId, defaultKey: key })
|
||||
.lean()
|
||||
.then((c) => String(c!._id));
|
||||
const form = (over: Partial<Parameters<typeof createCategory>[2]> = {}) => ({
|
||||
kind: "expense" as const,
|
||||
name: "Test",
|
||||
parentId: null,
|
||||
icon: "tag" as const,
|
||||
color: "blue" as const,
|
||||
...over,
|
||||
});
|
||||
|
||||
describe("varsayılan kategoriler", () => {
|
||||
it("tekrar yüklemede kopya oluşmaz", async () => {
|
||||
const before = await CategoryModel.countDocuments({ householdId: cakir });
|
||||
const added = await installDefaultCategories(
|
||||
new Types.ObjectId(cakir),
|
||||
new Types.ObjectId(ids.user("admin")),
|
||||
);
|
||||
expect(added).toBe(0);
|
||||
expect(await CategoryModel.countDocuments({ householdId: cakir })).toBe(before);
|
||||
});
|
||||
|
||||
it("alt kategoriler doğru ancestors zinciriyle yüklenir", async () => {
|
||||
const elektrik = await CategoryModel.findOne({
|
||||
householdId: cakir,
|
||||
defaultKey: "faturalar.elektrik",
|
||||
}).lean();
|
||||
const faturalar = await byKey("faturalar");
|
||||
expect(elektrik?.ancestors.map(String)).toEqual([faturalar]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("izolasyon ve roller", () => {
|
||||
it("başka hanenin kategorilerini okuyamaz ve değiştiremez", async () => {
|
||||
expect(await listCategories(ids.user("fatma"), cakir)).toBeNull();
|
||||
const kira = await byKey("kira");
|
||||
// fatma kendi hanesinin kimliğiyle, Çakır'ın kategorisini hedefler → bulunamaz.
|
||||
const r = await updateCategory(
|
||||
ids.user("fatma"),
|
||||
annemler,
|
||||
kira,
|
||||
form({ name: "Ele geçirildi" }),
|
||||
);
|
||||
expect(r.ok).toBe(false);
|
||||
expect((await CategoryModel.findById(kira).lean())?.name).toBe("Kira");
|
||||
const r2 = await setCategoryArchived(ids.user("fatma"), annemler, kira, true);
|
||||
expect(r2.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("izleyici ekleyemez, üye ekler", async () => {
|
||||
expect((await createCategory(ids.user("zeynep"), cakir, form({ name: "Harçlık" }))).ok).toBe(
|
||||
false,
|
||||
);
|
||||
expect((await createCategory(ids.user("emre"), cakir, form({ name: "Harçlık" }))).ok).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("başka hanenin kategorisini üst kategori yapamaz", async () => {
|
||||
const annemlerMarket = await byKey("market", annemler);
|
||||
const r = await createCategory(
|
||||
ids.user("ayse"),
|
||||
cakir,
|
||||
form({ name: "Sızma", parentId: annemlerMarket }),
|
||||
);
|
||||
expect(r.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ağaç kuralları", () => {
|
||||
it("aynı düzeyde aynı ad (Türkçe normalize) reddedilir", async () => {
|
||||
const r = await createCategory(ids.user("ayse"), cakir, form({ name: " KİRA " }));
|
||||
expect(r.ok).toBe(false);
|
||||
// farklı düzeyde aynı ad serbest
|
||||
const market = await byKey("market");
|
||||
expect(
|
||||
(await createCategory(ids.user("ayse"), cakir, form({ name: "Kira", parentId: market }))).ok,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("taşıma alt ağacın ancestors alanlarını günceller", async () => {
|
||||
const ulasim = await byKey("ulasim");
|
||||
const yakit = await byKey("ulasim.yakit");
|
||||
const created = await createCategory(
|
||||
ids.user("ayse"),
|
||||
cakir,
|
||||
form({ name: "LPG", parentId: yakit }),
|
||||
);
|
||||
const lpg = created.ok ? created.data.id : "";
|
||||
const ev = await byKey("ev");
|
||||
// Yakıt'ı (alt kategorisi LPG ile) "Ev ve mobilya" altına taşı.
|
||||
const r = await updateCategory(
|
||||
ids.user("ayse"),
|
||||
cakir,
|
||||
yakit,
|
||||
form({ name: "Yakıt", parentId: ev, icon: "fuel" }),
|
||||
);
|
||||
expect(r.ok).toBe(true);
|
||||
expect((await CategoryModel.findById(yakit).lean())?.ancestors.map(String)).toEqual([ev]);
|
||||
expect((await CategoryModel.findById(lpg).lean())?.ancestors.map(String)).toEqual([ev, yakit]);
|
||||
expect((await CategoryModel.findById(yakit).lean())?.parentId?.toString()).not.toBe(ulasim);
|
||||
});
|
||||
|
||||
it("döngü ve tür uyuşmazlığı reddedilir", async () => {
|
||||
const market = await byKey("market");
|
||||
const gida = await byKey("market.gida");
|
||||
expect(
|
||||
(
|
||||
await updateCategory(
|
||||
ids.user("ayse"),
|
||||
cakir,
|
||||
market,
|
||||
form({ name: "Market", parentId: gida }),
|
||||
)
|
||||
).ok,
|
||||
).toBe(false);
|
||||
const maas = await byKey("maas");
|
||||
expect(
|
||||
(await updateCategory(ids.user("ayse"), cakir, gida, form({ name: "Gıda", parentId: maas })))
|
||||
.ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("arşivleme alt ağaçla birlikte yapılır; üstü arşivliyken alt kategori geri alınamaz", async () => {
|
||||
const saglik = await byKey("saglik");
|
||||
const ilac = await byKey("saglik.ilac");
|
||||
expect((await setCategoryArchived(ids.user("emre"), cakir, saglik, true)).ok).toBe(true);
|
||||
const active = (await listCategories(ids.user("emre"), cakir))!.map((c) => c.id);
|
||||
expect(active).not.toContain(saglik);
|
||||
expect(active).not.toContain(ilac);
|
||||
expect((await setCategoryArchived(ids.user("emre"), cakir, ilac, false)).ok).toBe(false);
|
||||
expect((await setCategoryArchived(ids.user("emre"), cakir, saglik, false)).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user