3644e081b1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
8.2 KiB
TypeScript
208 lines
8.2 KiB
TypeScript
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([]);
|
||
});
|
||
});
|