180 lines
5.9 KiB
TypeScript
180 lines
5.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
tx: {
|
|
hrRequest: { create: vi.fn(), update: vi.fn() },
|
|
hrRequestTemplate: { findFirst: vi.fn() },
|
|
form: { findFirst: vi.fn(), findMany: vi.fn(), upsert: vi.fn() },
|
|
formSubmission: { create: vi.fn() },
|
|
activity: { create: vi.fn() },
|
|
},
|
|
}));
|
|
|
|
vi.mock("~/utils/db/.server/tenantRls.server", () => ({
|
|
withTenantRLS: async (_tenantId: string, callback: (tx: typeof mocks.tx) => Promise<unknown>) => callback(mocks.tx),
|
|
}));
|
|
vi.mock("@hivegrid/db/audit-events.server", () => ({ createAuditEvent: vi.fn().mockResolvedValue(undefined) }));
|
|
vi.mock("@hivegrid/db/notifications.server", () => ({ createNotificationDelivery: vi.fn().mockResolvedValue(undefined) }));
|
|
|
|
import { listLinkableHrRequestForms, submitRequest } from "./hrRequestService.server";
|
|
import { buildHrRequestFormDefinition } from "./hrM05Forms";
|
|
|
|
const createdRequest = {
|
|
id: "request-1",
|
|
tenantId: "tenant-a",
|
|
employeeUserId: "user-employee",
|
|
templateId: "template-1",
|
|
requestKind: "hardware",
|
|
title: "Neues Notebook",
|
|
details: '{"budget":"1200"}',
|
|
formSubmissionId: null,
|
|
status: "submitted",
|
|
assignedApproverUserId: null,
|
|
};
|
|
|
|
describe("HR request M05 form reuse (AK-P16c)", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.tx.hrRequest.create.mockResolvedValue({ ...createdRequest });
|
|
mocks.tx.hrRequest.update.mockImplementation(async ({ data }: { data: Record<string, unknown> }) => ({ ...createdRequest, ...data }));
|
|
mocks.tx.hrRequestTemplate.findFirst.mockResolvedValue({
|
|
id: "template-1",
|
|
title: "Hardware",
|
|
requestKind: "hardware",
|
|
defaultApproverUserId: null,
|
|
linkedFormId: "form-1",
|
|
});
|
|
mocks.tx.form.findFirst.mockResolvedValue({
|
|
id: "form-1",
|
|
definition: buildHrRequestFormDefinition(),
|
|
});
|
|
mocks.tx.form.upsert.mockResolvedValue({ id: "system-form-1" });
|
|
mocks.tx.formSubmission.create.mockResolvedValue({ id: "submission-1" });
|
|
mocks.tx.activity.create.mockResolvedValue({ id: "activity-1" });
|
|
});
|
|
|
|
it("persists a FormSubmission and wires formSubmissionId when the template links a form", async () => {
|
|
const row = await submitRequest({
|
|
tenantId: "tenant-a",
|
|
employeeUserId: "user-employee",
|
|
templateId: "template-1",
|
|
requestKind: "hardware",
|
|
title: "Neues Notebook",
|
|
details: '{"budget":"1200"}',
|
|
formValues: {
|
|
requestKind: "hardware",
|
|
title: "Neues Notebook",
|
|
details: '{"budget":"1200"}',
|
|
},
|
|
});
|
|
|
|
expect(mocks.tx.formSubmission.create).toHaveBeenCalledWith({
|
|
data: {
|
|
formId: "form-1",
|
|
tenantId: "tenant-a",
|
|
createdByUserId: "user-employee",
|
|
payload: { values: { title: "Neues Notebook", details: '{"budget":"1200"}', requestKind: "hardware" } },
|
|
status: "received",
|
|
linkedEntityType: "hr_request",
|
|
linkedEntityId: "request-1",
|
|
},
|
|
select: { id: true },
|
|
});
|
|
expect(mocks.tx.hrRequest.update).toHaveBeenCalledWith({ where: { id: "request-1" }, data: { formSubmissionId: "submission-1" } });
|
|
expect(row.formSubmissionId).toBe("submission-1");
|
|
});
|
|
|
|
it("fails closed when a selected template has no linked form", async () => {
|
|
mocks.tx.hrRequestTemplate.findFirst
|
|
.mockResolvedValueOnce({
|
|
id: "template-1",
|
|
title: "Hardware",
|
|
requestKind: "hardware",
|
|
defaultApproverUserId: null,
|
|
})
|
|
.mockResolvedValueOnce({ linkedFormId: null });
|
|
|
|
await expect(
|
|
submitRequest({
|
|
tenantId: "tenant-a",
|
|
employeeUserId: "user-employee",
|
|
templateId: "template-1",
|
|
requestKind: "hardware",
|
|
title: "Neues Notebook",
|
|
formValues: {
|
|
requestKind: "hardware",
|
|
title: "Neues Notebook",
|
|
details: "",
|
|
},
|
|
}),
|
|
).rejects.toThrow("hr.m05.templateFormRequired");
|
|
|
|
expect(mocks.tx.formSubmission.create).not.toHaveBeenCalled();
|
|
expect(mocks.tx.hrRequest.update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails closed when the linked form no longer exists", async () => {
|
|
mocks.tx.form.findFirst.mockResolvedValue(null);
|
|
|
|
await expect(
|
|
submitRequest({
|
|
tenantId: "tenant-a",
|
|
employeeUserId: "user-employee",
|
|
templateId: "template-1",
|
|
requestKind: "hardware",
|
|
title: "Neues Notebook",
|
|
formValues: {
|
|
requestKind: "hardware",
|
|
title: "Neues Notebook",
|
|
details: "",
|
|
},
|
|
}),
|
|
).rejects.toThrow("hr.m05.templateFormUnavailable");
|
|
|
|
expect(mocks.tx.formSubmission.create).not.toHaveBeenCalled();
|
|
expect(mocks.tx.hrRequest.update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses the system M05 form for a request without a template", async () => {
|
|
mocks.tx.hrRequest.create.mockResolvedValue({
|
|
...createdRequest,
|
|
templateId: null,
|
|
});
|
|
|
|
await submitRequest({
|
|
tenantId: "tenant-a",
|
|
employeeUserId: "user-employee",
|
|
requestKind: "hardware",
|
|
title: "Neues Notebook",
|
|
formValues: {
|
|
requestKind: "hardware",
|
|
title: "Neues Notebook",
|
|
details: "",
|
|
},
|
|
});
|
|
|
|
expect(mocks.tx.form.upsert).toHaveBeenCalled();
|
|
expect(mocks.tx.formSubmission.create).toHaveBeenCalledWith({
|
|
data: expect.objectContaining({
|
|
formId: "system-form-1",
|
|
linkedEntityType: "hr_request",
|
|
linkedEntityId: "request-1",
|
|
}),
|
|
select: { id: true },
|
|
});
|
|
});
|
|
|
|
it("lists only published hr_request forms as linkable", async () => {
|
|
mocks.tx.form.findMany.mockResolvedValue([{ id: "form-1", name: "Spesenantrag" }]);
|
|
|
|
const forms = await listLinkableHrRequestForms("tenant-a");
|
|
|
|
expect(mocks.tx.form.findMany).toHaveBeenCalledWith({
|
|
where: { tenantId: "tenant-a", subtype: "hr_request", status: "published" },
|
|
select: { id: true, name: true },
|
|
orderBy: { name: "asc" },
|
|
});
|
|
expect(forms).toEqual([{ id: "form-1", name: "Spesenantrag" }]);
|
|
});
|
|
});
|