Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/integrations/hrService.object-access.test.ts
T

234 lines
7.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
tx: {
hrEmployeeProfile: {
findFirst: vi.fn(),
findMany: vi.fn(),
},
hrEmployeeDocument: {
create: vi.fn(),
},
file: {
findFirst: vi.fn(),
},
fileEntityLink: {
create: vi.fn(),
},
activity: {
create: vi.fn(),
},
tenantUser: {
findFirst: vi.fn(),
findMany: vi.fn(),
},
userRole: {
findMany: vi.fn(),
},
},
}));
vi.mock("~/utils/db/.server/tenantRls.server", () => ({
withTenantRLS: (_tenantId: string, callback: (tx: typeof mocks.tx) => unknown) => callback(mocks.tx),
}));
vi.mock("~/utils/db.server", () => ({
db: { user: { findMany: vi.fn().mockResolvedValue([]) } },
}));
vi.mock("@hivegrid/db/audit-events.server", () => ({ createAuditEvent: vi.fn() }));
vi.mock("./hrFieldCrypto.server", () => ({ getHrFieldCrypto: vi.fn() }));
import {
createDocumentWithLink,
listHrMemberOptions,
listDirectoryForViewer,
requireActiveHrMember,
requireEmployeeObjectAccess,
} from "./hrService.server";
describe("HR employee object access", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.tx.hrEmployeeProfile.findMany.mockResolvedValue([]);
mocks.tx.hrEmployeeProfile.findFirst.mockResolvedValue(null);
mocks.tx.hrEmployeeDocument.create.mockResolvedValue({
id: "document-1",
tenantId: "tenant-1",
employeeUserId: "employee-1",
uploadedByUserId: "employee-1",
title: "Bescheinigung",
documentKind: "certificate",
fileId: "file-1",
visibility: "employee",
uploadedByOffice: false,
validUntil: null,
isDemoData: false,
createdAt: new Date("2026-07-25T12:00:00.000Z"),
updatedAt: new Date("2026-07-25T12:00:00.000Z"),
archivedAt: null,
});
mocks.tx.file.findFirst.mockResolvedValue({ id: "file-1" });
mocks.tx.fileEntityLink.create.mockResolvedValue({ id: "link-1" });
mocks.tx.activity.create.mockResolvedValue({ id: "activity-1" });
mocks.tx.tenantUser.findFirst.mockResolvedValue({ id: "membership-1" });
mocks.tx.tenantUser.findMany.mockResolvedValue([]);
mocks.tx.userRole.findMany.mockResolvedValue([]);
});
it("limits the regular directory query to self and direct reports", async () => {
await listDirectoryForViewer({
tenantId: "tenant-1",
viewerUserId: "manager-1",
canReadAll: false,
});
expect(mocks.tx.hrEmployeeProfile.findMany).toHaveBeenCalledWith({
where: {
tenantId: "tenant-1",
OR: [{ userId: "manager-1" }, { managerUserId: "manager-1" }],
},
select: {
userId: true,
position: true,
team: true,
locationId: true,
managerUserId: true,
},
orderBy: [{ team: "asc" }, { userId: "asc" }],
});
});
it("does not infer tenant-wide HR visibility from a generic tenant role", async () => {
mocks.tx.hrEmployeeProfile.findFirst.mockResolvedValue(null);
await expect(
requireEmployeeObjectAccess({
tenantId: "tenant-1",
viewerUserId: "tenant-admin-1",
employeeUserId: "employee-1",
canReadAll: false,
}),
).rejects.toThrow();
expect(mocks.tx.hrEmployeeProfile.findFirst).toHaveBeenCalledWith({
where: {
tenantId: "tenant-1",
userId: "employee-1",
managerUserId: "tenant-admin-1",
},
select: { userId: true, managerUserId: true },
});
});
it("allows self, direct-report and explicit HR-wide access without changing tenant scope", async () => {
mocks.tx.hrEmployeeProfile.findFirst
.mockResolvedValueOnce({ userId: "employee-1", managerUserId: "manager-1" })
.mockResolvedValueOnce({ userId: "employee-2", managerUserId: "someone-else" });
await expect(
requireEmployeeObjectAccess({
tenantId: "tenant-1",
viewerUserId: "manager-1",
employeeUserId: "manager-1",
canReadAll: false,
}),
).resolves.toEqual({ isSelf: true, isDirectReport: false });
expect(mocks.tx.hrEmployeeProfile.findFirst).not.toHaveBeenCalled();
await expect(
requireEmployeeObjectAccess({
tenantId: "tenant-1",
viewerUserId: "manager-1",
employeeUserId: "employee-1",
canReadAll: false,
}),
).resolves.toEqual({ isSelf: false, isDirectReport: true });
await expect(
requireEmployeeObjectAccess({
tenantId: "tenant-1",
viewerUserId: "hr-1",
employeeUserId: "employee-2",
canReadAll: true,
}),
).resolves.toEqual({ isSelf: false, isDirectReport: false });
});
it("creates the HR document and user link inside one tenant transaction", async () => {
await createDocumentWithLink({
tenantId: "tenant-1",
employeeUserId: "employee-1",
uploadedByUserId: "employee-1",
title: " Bescheinigung ",
documentKind: "certificate",
fileId: "file-1",
});
expect(mocks.tx.hrEmployeeDocument.create).toHaveBeenCalledWith({
data: expect.objectContaining({
tenantId: "tenant-1",
employeeUserId: "employee-1",
uploadedByUserId: "employee-1",
title: "Bescheinigung",
fileId: "file-1",
}),
});
expect(mocks.tx.fileEntityLink.create).toHaveBeenCalledWith({
data: {
tenantId: "tenant-1",
createdByUserId: "employee-1",
fileId: "file-1",
entityType: "user",
entityId: "employee-1",
linkRole: "hr_employee_document",
},
});
});
it("rejects a file from outside the tenant before creating a document", async () => {
mocks.tx.file.findFirst.mockResolvedValue(null);
await expect(
createDocumentWithLink({
tenantId: "tenant-1",
employeeUserId: "employee-1",
uploadedByUserId: "employee-1",
title: "Bescheinigung",
documentKind: "certificate",
fileId: "foreign-file",
}),
).rejects.toThrow();
expect(mocks.tx.hrEmployeeDocument.create).not.toHaveBeenCalled();
expect(mocks.tx.fileEntityLink.create).not.toHaveBeenCalled();
});
it("resolves picker labels with team, position and roles", async () => {
mocks.tx.tenantUser.findMany.mockResolvedValue([
{
userId: "employee-1",
user: { firstName: "Mira", lastName: "Beispiel", email: "mira@example.test" },
},
]);
mocks.tx.hrEmployeeProfile.findMany.mockResolvedValue([
{ userId: "employee-1", team: "Montage", position: "Teamleitung" },
]);
mocks.tx.userRole.findMany.mockResolvedValue([
{ userId: "employee-1", role: { name: "Projektleiter" } },
]);
await expect(listHrMemberOptions("tenant-1")).resolves.toEqual([
{
userId: "employee-1",
displayName: "Mira Beispiel",
email: "mira@example.test",
team: "Montage",
position: "Teamleitung",
roleNames: ["Projektleiter"],
},
]);
});
it("rejects a posted picker target outside the active tenant", async () => {
mocks.tx.tenantUser.findFirst.mockResolvedValue(null);
await expect(requireActiveHrMember("tenant-1", "foreign-user")).rejects.toThrow();
});
});