116 lines
4.5 KiB
TypeScript
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" },
|
|
}));
|
|
});
|
|
});
|