Files
butcem/src/features/households/service.int.test.ts
T

208 lines
8.2 KiB
TypeScript
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.
import mongoose, { Types } from "mongoose";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { getDb, getMongoClient } from "@/lib/db/mongo";
import { AuditLogModel } from "@/models/audit-log";
import { HouseholdModel } from "@/models/household";
import { HouseholdMemberModel } from "@/models/household-member";
import { runSeed } from "../../../scripts/seed/run";
import {
adminCreateHousehold,
adminSetMemberRole,
getHouseholdDetail,
listMemberships,
removeAllMembershipsOfUser,
removeMember,
resolveMembership,
setMemberRole,
updateHouseholdSettings,
} from "./service";
/**
* VERİ İZOLASYONU (Bölüm 5): başka hanenin verisine erişim imkânsız olmalı.
* Seed verisi: Çakır Ailesi (admin+ayse owner, emre member, zeynep viewer, ozan member),
* Annemler (fatma owner, mehmet member, ayse viewer), burak = hanesi olmayan admin.
*/
const ids = new Map<string, string>();
let cakir = "";
let annemler = "";
const settings = {
name: "Çakır Ailesi",
budgetMonthStartDay: 15,
installmentBudgetMode: "purchase_month" as const,
dailyPlanMode: "even" as const,
includePrivateAccountsInTotals: false,
extraStoreSavingThresholdMinor: 25_000,
};
beforeAll(async () => {
expect(getDb().databaseName).toBe("butcem_test");
await getDb().dropDatabase();
await runSeed({ log: () => {} });
for (const u of await getDb().collection("user").find().toArray())
ids.set(String(u.username), String(u._id));
cakir = String((await HouseholdModel.findOne({ name: "Çakır Ailesi" }))!._id);
annemler = String((await HouseholdModel.findOne({ name: "Annemler" }))!._id);
});
afterAll(async () => {
await getDb().dropDatabase();
await mongoose.disconnect();
await getMongoClient().close();
});
const id = (username: string) => ids.get(username)!;
describe("üyelik çözümleme (izolasyon çekirdeği)", () => {
it("üye olmayan kullanıcı başka hanenin üyeliğini çözemez", async () => {
expect(await resolveMembership(id("fatma"), cakir)).toBeNull();
expect(await resolveMembership(id("emre"), annemler)).toBeNull();
});
it("rol yetmezse null döner", async () => {
expect((await resolveMembership(id("ayse"), annemler, "viewer"))?.role).toBe("viewer");
expect(await resolveMembership(id("ayse"), annemler, "member")).toBeNull();
expect(await resolveMembership(id("zeynep"), cakir, "member")).toBeNull();
expect((await resolveMembership(id("emre"), cakir, "member"))?.role).toBe("member");
});
it("hanesi olmayan admin hiçbir haneyi göremez (admin ayrıcalığı yok)", async () => {
expect(await listMemberships(id("burak"))).toEqual([]);
expect(await resolveMembership(id("burak"), cakir)).toBeNull();
expect(await getHouseholdDetail(id("burak"), cakir)).toBeNull();
});
it("geçersiz ve uydurma kimlikler reddedilir", async () => {
const forged = new Types.ObjectId().toHexString();
expect(await resolveMembership(id("ayse"), forged)).toBeNull();
expect(await resolveMembership(id("ayse"), "not-an-id")).toBeNull();
expect(await resolveMembership(id("ayse"), { $ne: null } as unknown as string)).toBeNull();
expect(await resolveMembership("", cakir)).toBeNull();
});
it("kullanıcı yalnızca kendi hanelerini listeler", async () => {
expect((await listMemberships(id("ayse"))).map((m) => [m.name, m.role])).toEqual([
["Annemler", "viewer"],
["Çakır Ailesi", "owner"],
]);
expect((await listMemberships(id("fatma"))).map((m) => m.name)).toEqual(["Annemler"]);
});
});
describe("hane ayrıntısı ve ayarlar", () => {
it("başka hanenin ayrıntısı okunamaz", async () => {
expect(await getHouseholdDetail(id("fatma"), cakir)).toBeNull();
const own = await getHouseholdDetail(id("emre"), cakir);
expect(own?.members.map((m) => m.username).sort()).toEqual([
"admin",
"ayse",
"emre",
"ozan",
"zeynep",
]);
});
it("başka hanenin sahibi, üye ve izleyici ayarları değiştiremez", async () => {
for (const u of ["fatma", "emre", "zeynep", "burak"]) {
const res = await updateHouseholdSettings(id(u), cakir, settings);
expect(res.ok, u).toBe(false);
}
const h = await HouseholdModel.findById(cakir).lean();
expect(h?.settings?.budgetMonthStartDay).toBe(1);
});
it("sahip ayarları değiştirir ve denetim kaydı yazılır", async () => {
const res = await updateHouseholdSettings(id("ayse"), cakir, settings);
expect(res.ok).toBe(true);
const h = await HouseholdModel.findById(cakir).lean();
expect(h?.settings?.budgetMonthStartDay).toBe(15);
const audit = await AuditLogModel.findOne({
action: "household.settings.update",
householdId: cakir,
}).lean();
expect(audit?.actorId.toString()).toBe(id("ayse"));
expect((audit?.diff as Record<string, unknown>).budgetMonthStartDay).toEqual({
from: 1,
to: 15,
});
});
});
describe("üyelik değişiklikleri", () => {
it("üye ve izleyici rol değiştiremez, üye çıkaramaz", async () => {
expect((await setMemberRole(id("emre"), cakir, id("zeynep"), "owner")).ok).toBe(false);
expect((await removeMember(id("zeynep"), cakir, id("emre"))).ok).toBe(false);
});
it("bir hanenin sahibi başka hanedeki üyeliği etkileyemez", async () => {
// ayse Çakır'ın sahibi; fatma Çakır üyesi değil → işlem bulunamaz, Annemler'deki rolü değişmez.
expect((await setMemberRole(id("ayse"), cakir, id("fatma"), "viewer")).ok).toBe(false);
expect((await removeMember(id("ayse"), cakir, id("mehmet"))).ok).toBe(false);
// fatma Annemler'in sahibi; Çakır üzerinde yetkisi yok.
expect((await setMemberRole(id("fatma"), cakir, id("emre"), "viewer")).ok).toBe(false);
const fatma = await HouseholdMemberModel.findOne({
householdId: annemler,
userId: id("fatma"),
}).lean();
const mehmet = await HouseholdMemberModel.findOne({
householdId: annemler,
userId: id("mehmet"),
}).lean();
const emre = await HouseholdMemberModel.findOne({
householdId: cakir,
userId: id("emre"),
}).lean();
expect([fatma?.role, mehmet?.role, emre?.role]).toEqual(["owner", "member", "member"]);
});
it("son sahip düşürülemez; birden çok sahip varken düşürülebilir", async () => {
expect((await adminSetMemberRole(id("admin"), annemler, id("fatma"), "member")).ok).toBe(false);
expect((await setMemberRole(id("fatma"), annemler, id("fatma"), "member")).ok).toBe(false);
// Çakır'da iki sahip var (admin + ayse).
expect((await setMemberRole(id("ayse"), cakir, id("admin"), "member")).ok).toBe(true);
// Artık ayse son sahip.
expect((await setMemberRole(id("ayse"), cakir, id("ayse"), "member")).ok).toBe(false);
expect((await setMemberRole(id("ayse"), cakir, id("admin"), "owner")).ok).toBe(true);
expect(
await AuditLogModel.countDocuments({ action: "household.member.role", householdId: cakir }),
).toBe(2);
});
it("sahip üyeyi haneden çıkarır; kullanıcı artık haneyi göremez", async () => {
expect((await removeMember(id("ayse"), cakir, id("ozan"))).ok).toBe(true);
expect(await resolveMembership(id("ozan"), cakir)).toBeNull();
expect(await listMemberships(id("ozan"))).toEqual([]);
});
});
describe("admin işlemleri", () => {
it("hane + ilk sahip + denetim kaydı tek transaction'da oluşur", async () => {
const res = await adminCreateHousehold(id("admin"), {
name: "Yazlık",
ownerUserId: id("burak"),
});
expect(res.ok).toBe(true);
const hid = res.ok ? res.data.id : "";
expect((await resolveMembership(id("burak"), hid, "owner"))?.name).toBe("Yazlık");
expect(
await AuditLogModel.countDocuments({ action: "household.create", householdId: hid }),
).toBe(1);
});
it("var olmayan kullanıcıyla hane oluşturulmaz", async () => {
const before = await HouseholdModel.countDocuments();
const res = await adminCreateHousehold(id("admin"), {
name: "Hayalet",
ownerUserId: new Types.ObjectId().toHexString(),
});
expect(res.ok).toBe(false);
expect(await HouseholdModel.countDocuments()).toBe(before);
});
it("kullanıcı silinince tüm üyelikleri temizlenir", async () => {
expect(await removeAllMembershipsOfUser(id("mehmet"))).toBe(1);
expect(await listMemberships(id("mehmet"))).toEqual([]);
});
});