102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
// @vitest-environment node
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../db.server", () => {
|
|
return {
|
|
db: {
|
|
log: {
|
|
groupBy: vi.fn(),
|
|
findMany: vi.fn(),
|
|
create: vi.fn(),
|
|
},
|
|
},
|
|
};
|
|
});
|
|
|
|
import { db } from "../db.server";
|
|
import { getRlsExecutionContext } from "@hivegrid/db/rls-context.server";
|
|
import { createLogLogin, getLastUserLogsByUserIds } from "./logs.db.server";
|
|
|
|
const groupBy = db.log.groupBy as unknown as ReturnType<typeof vi.fn>;
|
|
const findMany = db.log.findMany as unknown as ReturnType<typeof vi.fn>;
|
|
const create = db.log.create as unknown as ReturnType<typeof vi.fn>;
|
|
|
|
describe("getLastUserLogsByUserIds", () => {
|
|
beforeEach(() => {
|
|
groupBy.mockReset();
|
|
findMany.mockReset();
|
|
});
|
|
|
|
it("issues no queries when given an empty user id list", async () => {
|
|
const result = await getLastUserLogsByUserIds([]);
|
|
|
|
expect(result.size).toBe(0);
|
|
expect(groupBy).not.toHaveBeenCalled();
|
|
expect(findMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses a single groupBy and a single findMany regardless of user count (no N+1)", async () => {
|
|
const userIds = ["u1", "u2", "u3", "u4", "u5"];
|
|
const newest: Record<string, Date> = {
|
|
u1: new Date("2026-05-01T10:00:00.000Z"),
|
|
u2: new Date("2026-05-02T10:00:00.000Z"),
|
|
u3: new Date("2026-05-03T10:00:00.000Z"),
|
|
};
|
|
groupBy.mockResolvedValueOnce(
|
|
Object.entries(newest).map(([userId, createdAt]) => ({ userId, _max: { createdAt } }))
|
|
);
|
|
findMany.mockResolvedValueOnce(
|
|
Object.entries(newest).map(([userId, createdAt]) => ({ id: `log-${userId}`, userId, createdAt }))
|
|
);
|
|
|
|
const result = await getLastUserLogsByUserIds(userIds);
|
|
|
|
expect(groupBy).toHaveBeenCalledTimes(1);
|
|
expect(findMany).toHaveBeenCalledTimes(1);
|
|
expect(groupBy).toHaveBeenCalledWith({
|
|
by: ["userId"],
|
|
where: { userId: { in: userIds } },
|
|
_max: { createdAt: true },
|
|
});
|
|
expect(findMany).toHaveBeenCalledWith({
|
|
where: {
|
|
OR: Object.entries(newest).map(([userId, createdAt]) => ({ userId, createdAt })),
|
|
},
|
|
select: { id: true, userId: true, action: true, createdAt: true },
|
|
});
|
|
expect(result.get("u1")).toMatchObject({ id: "log-u1", userId: "u1" });
|
|
expect(result.get("u3")).toMatchObject({ id: "log-u3" });
|
|
expect(result.size).toBe(3);
|
|
});
|
|
|
|
it("skips the findMany when no users have logs", async () => {
|
|
groupBy.mockResolvedValueOnce([{ userId: "u1", _max: { createdAt: null } }]);
|
|
|
|
const result = await getLastUserLogsByUserIds(["u1"]);
|
|
|
|
expect(groupBy).toHaveBeenCalledTimes(1);
|
|
expect(findMany).not.toHaveBeenCalled();
|
|
expect(result.size).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("identity-owned authentication logs", () => {
|
|
it("creates a pre-tenant login log inside the user's RLS context", async () => {
|
|
create.mockImplementationOnce(async () => {
|
|
expect(getRlsExecutionContext()).toMatchObject({ userId: "user_1" });
|
|
return {};
|
|
});
|
|
|
|
await createLogLogin(new Request("https://app.example.test/login"), { id: "user_1" } as never);
|
|
|
|
expect(create).toHaveBeenCalledWith({
|
|
data: {
|
|
userId: "user_1",
|
|
url: "",
|
|
action: "Login",
|
|
details: "",
|
|
},
|
|
});
|
|
});
|
|
});
|