ledgerone_backend/test/accounts.service.spec.ts

116 lines
4.5 KiB
TypeScript

import { BadRequestException } from "@nestjs/common";
import { AccountsService } from "../src/accounts/accounts.service";
import { createPrismaMock } from "./utils/mock-prisma";
const createService = () => {
const prisma = createPrismaMock();
const plaid = { createLinkToken: jest.fn(), syncBalancesForUser: jest.fn() };
const teller = { syncBalancesForUser: jest.fn() };
const opaqueIds = {
encode: jest.fn((_kind: string, _userId: string, id: string) => `opaque_account_${id}`),
decode: jest.fn((_kind: string, _userId: string, handle: string) => handle.replace("opaque_account_", "")),
};
const planLimits = { assertCanAddAccounts: jest.fn() };
return {
prisma,
service: new AccountsService(prisma as any, plaid as any, teller as any, opaqueIds as any, planLimits as any),
};
};
describe("AccountsService ownership", () => {
it("marks an account as joint for an active household member", async () => {
const { prisma, service } = createService();
prisma.account.findFirst.mockResolvedValue({ id: "acct_1", userId: "user_1", isActive: true });
prisma.householdMember.findFirst.mockResolvedValue({ householdId: "household_1", userId: "user_1", status: "active" });
prisma.account.update.mockResolvedValue({
id: "acct_1",
institutionName: "Bank",
accountType: "checking",
mask: "1234",
householdId: "household_1",
ownerUserId: null,
ownershipType: "joint",
isActive: true,
createdAt: new Date("2026-07-16T00:00:00.000Z"),
});
prisma.auditLog.create.mockResolvedValue({});
const result = await service.updateOwnership("user_1", "opaque_account_acct_1", {
ownershipType: "joint",
householdId: "household_1",
});
expect(result.id).toBe("opaque_account_acct_1");
expect(prisma.account.update).toHaveBeenCalledWith({
where: { id: "acct_1" },
data: {
ownershipType: "joint",
householdId: "household_1",
ownerUserId: null,
},
select: expect.any(Object),
});
expect(prisma.auditLog.create).toHaveBeenCalledWith({
data: expect.objectContaining({
action: "account.ownership.update",
metadata: expect.objectContaining({ ownershipType: "joint", householdId: "household_1" }),
}),
});
});
it("marks an account as theirs only when the owner is another household member", async () => {
const { prisma, service } = createService();
prisma.account.findFirst.mockResolvedValue({ id: "acct_1", userId: "user_1", isActive: true });
prisma.householdMember.findFirst
.mockResolvedValueOnce({ householdId: "household_1", userId: "user_1", status: "active" })
.mockResolvedValueOnce({ householdId: "household_1", userId: "user_2", status: "active" });
prisma.account.update.mockResolvedValue({
id: "acct_1",
householdId: "household_1",
ownerUserId: "user_2",
ownershipType: "theirs",
});
prisma.auditLog.create.mockResolvedValue({});
await service.updateOwnership("user_1", "opaque_account_acct_1", {
ownershipType: "theirs",
householdId: "household_1",
ownerUserId: "user_2",
});
expect(prisma.account.update).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({ ownershipType: "theirs", ownerUserId: "user_2" }),
}));
});
it("blocks shared ownership without active household membership", async () => {
const { prisma, service } = createService();
prisma.account.findFirst.mockResolvedValue({ id: "acct_1", userId: "user_1", isActive: true });
prisma.householdMember.findFirst.mockResolvedValue(null);
await expect(service.updateOwnership("user_1", "opaque_account_acct_1", {
ownershipType: "joint",
householdId: "household_1",
})).rejects.toBeInstanceOf(BadRequestException);
expect(prisma.account.update).not.toHaveBeenCalled();
});
it("resets household ownership when marked mine", async () => {
const { prisma, service } = createService();
prisma.account.findFirst.mockResolvedValue({ id: "acct_1", userId: "user_1", isActive: true });
prisma.account.update.mockResolvedValue({
id: "acct_1",
householdId: null,
ownerUserId: "user_1",
ownershipType: "mine",
});
prisma.auditLog.create.mockResolvedValue({});
await service.updateOwnership("user_1", "opaque_account_acct_1", { ownershipType: "mine" });
expect(prisma.account.update).toHaveBeenCalledWith(expect.objectContaining({
data: { ownershipType: "mine", householdId: null, ownerUserId: "user_1" },
}));
});
});