145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
import { NotFoundException } from "@nestjs/common";
|
|
import { AuthService } from "../src/auth/auth.service";
|
|
|
|
const model = (order: string[]) => ({
|
|
findMany: jest.fn(),
|
|
findUnique: jest.fn(),
|
|
create: jest.fn(),
|
|
update: jest.fn(),
|
|
updateMany: jest.fn(),
|
|
upsert: jest.fn(),
|
|
delete: jest.fn(async () => {
|
|
order.push("user.delete");
|
|
}),
|
|
deleteMany: jest.fn(async () => undefined),
|
|
});
|
|
|
|
const createService = () => {
|
|
const order: string[] = [];
|
|
const tx = {
|
|
account: model(order),
|
|
transactionRaw: model(order),
|
|
transactionDerived: model(order),
|
|
rule: model(order),
|
|
ruleExecution: model(order),
|
|
exportLog: model(order),
|
|
auditLog: model(order),
|
|
googleConnection: model(order),
|
|
emailVerificationToken: model(order),
|
|
passwordResetToken: model(order),
|
|
refreshToken: model(order),
|
|
session: model(order),
|
|
socialAccount: model(order),
|
|
subscription: model(order),
|
|
abuseEvent: model(order),
|
|
taxReturn: model(order),
|
|
taxDocument: model(order),
|
|
user: model(order),
|
|
};
|
|
tx.taxDocument.deleteMany.mockImplementation(async () => {
|
|
order.push("taxDocument.deleteMany");
|
|
});
|
|
tx.ruleExecution.deleteMany.mockImplementation(async () => {
|
|
order.push("ruleExecution.deleteMany");
|
|
});
|
|
tx.transactionDerived.deleteMany.mockImplementation(async () => {
|
|
order.push("transactionDerived.deleteMany");
|
|
});
|
|
tx.transactionRaw.deleteMany.mockImplementation(async () => {
|
|
order.push("transactionRaw.deleteMany");
|
|
});
|
|
tx.account.deleteMany.mockImplementation(async () => {
|
|
order.push("account.deleteMany");
|
|
});
|
|
tx.rule.deleteMany.mockImplementation(async () => {
|
|
order.push("rule.deleteMany");
|
|
});
|
|
|
|
const prisma = {
|
|
user: { findUnique: jest.fn() },
|
|
$transaction: jest.fn((callback: (client: typeof tx) => Promise<void>) => callback(tx)),
|
|
};
|
|
const service = new AuthService(
|
|
prisma as any,
|
|
{ sign: jest.fn(), verify: jest.fn() } as any,
|
|
{ sendVerificationEmail: jest.fn(), sendPasswordResetEmail: jest.fn() } as any,
|
|
{ decrypt: jest.fn() } as any,
|
|
);
|
|
return { service, prisma, tx, order };
|
|
};
|
|
|
|
describe("AuthService account deletion", () => {
|
|
it("deletes dependent personal data before deleting the user", async () => {
|
|
const { service, prisma, tx, order } = createService();
|
|
prisma.user.findUnique.mockResolvedValue({ id: "user_1" });
|
|
tx.account.findMany.mockResolvedValue([{ id: "acct_1" }]);
|
|
tx.transactionRaw.findMany.mockResolvedValue([{ id: "tx_1" }]);
|
|
tx.rule.findMany.mockResolvedValue([{ id: "rule_1" }]);
|
|
tx.taxReturn.findMany.mockResolvedValue([{ id: "return_1" }]);
|
|
|
|
await expect(service.deleteAccount("user_1")).resolves.toEqual({
|
|
message: "Account and associated personal data deleted.",
|
|
});
|
|
|
|
expect(tx.ruleExecution.deleteMany).toHaveBeenCalledWith({
|
|
where: {
|
|
OR: [
|
|
{ ruleId: { in: ["rule_1"] } },
|
|
{ transactionId: { in: ["tx_1"] } },
|
|
],
|
|
},
|
|
});
|
|
expect(tx.user.delete).toHaveBeenCalledWith({ where: { id: "user_1" } });
|
|
expect(order.slice(0, 6)).toEqual([
|
|
"taxDocument.deleteMany",
|
|
"ruleExecution.deleteMany",
|
|
"transactionDerived.deleteMany",
|
|
"transactionRaw.deleteMany",
|
|
"account.deleteMany",
|
|
"rule.deleteMany",
|
|
]);
|
|
expect(order[order.length - 1]).toBe("user.delete");
|
|
});
|
|
|
|
it("throws when the user no longer exists", async () => {
|
|
const { service, prisma } = createService();
|
|
prisma.user.findUnique.mockResolvedValue(null);
|
|
|
|
await expect(service.deleteAccount("missing_user")).rejects.toBeInstanceOf(NotFoundException);
|
|
expect(prisma.$transaction).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("AuthService session-bound tokens", () => {
|
|
it("creates a server-side session and binds tokens to it", async () => {
|
|
const { service, prisma } = createService();
|
|
(prisma as any).session = {
|
|
create: jest.fn().mockResolvedValue({ id: "session_1" }),
|
|
};
|
|
(prisma as any).refreshToken = {
|
|
create: jest.fn().mockResolvedValue({ id: "refresh_1" }),
|
|
};
|
|
const jwt = (service as any).jwtService;
|
|
jwt.sign.mockReturnValue("access_token");
|
|
|
|
const result = await service.issueTokensForUser("user_1", {
|
|
ipAddress: "127.0.0.1",
|
|
userAgent: "jest",
|
|
});
|
|
|
|
expect(result).toEqual({ accessToken: "access_token", refreshToken: expect.any(String), sessionNonce: expect.any(String) });
|
|
expect((prisma as any).session.create).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({
|
|
userId: "user_1",
|
|
ipHash: expect.any(String),
|
|
userAgentHash: expect.any(String),
|
|
nonceHash: expect.stringMatching(/^[a-f0-9]{64}$/),
|
|
}),
|
|
});
|
|
expect(jwt.sign).toHaveBeenCalledWith({ sub: "user_1", sid: "session_1" });
|
|
expect((prisma as any).refreshToken.create).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({ userId: "user_1", sessionId: "session_1" }),
|
|
});
|
|
});
|
|
});
|