Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9dafcc140 |
@@ -1,7 +1,12 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { returnAssignment, validateReturnAssignmentInput } from "@/lib/assignments";
|
import {
|
||||||
|
returnAssignment,
|
||||||
|
transferAssignment,
|
||||||
|
validateReturnAssignmentInput,
|
||||||
|
validateTransferAssignmentInput,
|
||||||
|
} from "@/lib/assignments";
|
||||||
|
|
||||||
// First dynamic-segment ([id]) route in the codebase — see
|
// First dynamic-segment ([id]) route in the codebase — see
|
||||||
// docs/asset-license-management.md's "Assignment (chốt 2026-08-02)" section,
|
// docs/asset-license-management.md's "Assignment (chốt 2026-08-02)" section,
|
||||||
@@ -39,7 +44,34 @@ export async function PATCH(
|
|||||||
return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 });
|
return NextResponse.json({ error: "Request body must be a JSON object" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { returnedAt } = body as Record<string, unknown>;
|
const { returnedAt, employeeId } = body as Record<string, unknown>;
|
||||||
|
|
||||||
|
// Dispatch on body shape — a body carrying employeeId is a transfer
|
||||||
|
// request, everything else is the existing return behavior. The two are
|
||||||
|
// mutually exclusive: don't guess intent if a caller sends both (see
|
||||||
|
// docs/asset-license-management.md's "Assignment (chốt 2026-08-02)"
|
||||||
|
// section, bullet right after the transfer rule, for why this stays a
|
||||||
|
// single PATCH route rather than an action-based sub-route).
|
||||||
|
if (employeeId !== undefined && returnedAt !== undefined) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Provide either employeeId (transfer) or returnedAt (return), not both" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (employeeId !== undefined) {
|
||||||
|
const transferValidation = validateTransferAssignmentInput({ employeeId });
|
||||||
|
if (!transferValidation.ok) {
|
||||||
|
return NextResponse.json({ error: transferValidation.error }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const transferred = await transferAssignment(params.id, transferValidation.value);
|
||||||
|
if (!transferred.ok) {
|
||||||
|
return NextResponse.json({ error: transferred.error }, { status: transferred.status });
|
||||||
|
}
|
||||||
|
return NextResponse.json(transferred.value, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
const validation = validateReturnAssignmentInput({ returnedAt });
|
const validation = validateReturnAssignmentInput({ returnedAt });
|
||||||
|
|
||||||
if (!validation.ok) {
|
if (!validation.ok) {
|
||||||
|
|||||||
+127
-3
@@ -43,15 +43,21 @@ function dateString(message: string) {
|
|||||||
.transform((value) => new Date(value));
|
.transform((value) => new Date(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shared between createAssignmentSchema's employeeId field and
|
||||||
|
// transferAssignmentSchema below — kept as a standalone Zod schema (rather
|
||||||
|
// than inlined in both places) so the two stay byte-for-byte in sync on the
|
||||||
|
// exact error message, instead of two copies drifting apart over time.
|
||||||
|
const employeeIdSchema = z
|
||||||
|
.string({ required_error: "employeeId is required and must be a valid UUID string" })
|
||||||
|
.uuid("employeeId is required and must be a valid UUID string");
|
||||||
|
|
||||||
const createAssignmentSchema = z
|
const createAssignmentSchema = z
|
||||||
.object({
|
.object({
|
||||||
// FK to Employee — only a UUID *format* check here, whether it
|
// FK to Employee — only a UUID *format* check here, whether it
|
||||||
// references a real employee is left to Prisma's own FK constraint at
|
// references a real employee is left to Prisma's own FK constraint at
|
||||||
// insert time (see createAssignment()'s P2003 handling below). Mirrors
|
// insert time (see createAssignment()'s P2003 handling below). Mirrors
|
||||||
// src/lib/requests.ts's employee_id handling.
|
// src/lib/requests.ts's employee_id handling.
|
||||||
employeeId: z
|
employeeId: employeeIdSchema,
|
||||||
.string({ required_error: "employeeId is required and must be a valid UUID string" })
|
|
||||||
.uuid("employeeId is required and must be a valid UUID string"),
|
|
||||||
// Exactly one of assetItemId/licenseId must be set — see the .refine()
|
// Exactly one of assetItemId/licenseId must be set — see the .refine()
|
||||||
// below for the XOR check. Unlike other nullable FKs in this codebase
|
// below for the XOR check. Unlike other nullable FKs in this codebase
|
||||||
// (e.g. AssetItem.purchaseOrderId), whether these reference a real row
|
// (e.g. AssetItem.purchaseOrderId), whether these reference a real row
|
||||||
@@ -137,6 +143,35 @@ export function validateReturnAssignmentInput(input: {
|
|||||||
return { ok: true, value: result.data };
|
return { ok: true, value: result.data };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transferAssignmentSchema = z.object({
|
||||||
|
// Reuses employeeIdSchema (see its doc comment above) — the new employee
|
||||||
|
// to hand the same AssetItem/License over to. assetItemId/licenseId are
|
||||||
|
// deliberately NOT accepted here: transferAssignment() reuses whichever
|
||||||
|
// one the original Assignment already had (see that function's doc
|
||||||
|
// comment), the caller doesn't get to change it via a transfer.
|
||||||
|
employeeId: employeeIdSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TransferValidationResult =
|
||||||
|
| { ok: true; value: { employeeId: string } }
|
||||||
|
| { ok: false; error: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates raw (untyped) assignment-transfer input. employeeId must be a
|
||||||
|
* valid UUID string (same format check as validateCreateAssignmentInput's
|
||||||
|
* employeeId field).
|
||||||
|
*/
|
||||||
|
export function validateTransferAssignmentInput(input: {
|
||||||
|
employeeId?: unknown;
|
||||||
|
}): TransferValidationResult {
|
||||||
|
const result = transferAssignmentSchema.safeParse(input);
|
||||||
|
if (!result.success) {
|
||||||
|
const message = result.error.issues[0]?.message ?? "Invalid input";
|
||||||
|
return { ok: false, error: message };
|
||||||
|
}
|
||||||
|
return { ok: true, value: result.data };
|
||||||
|
}
|
||||||
|
|
||||||
function toRecord(row: {
|
function toRecord(row: {
|
||||||
id: string;
|
id: string;
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
@@ -407,3 +442,92 @@ export async function returnAssignment(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TransferAssignmentResult =
|
||||||
|
| { ok: true; value: AssignmentRecord }
|
||||||
|
| { ok: false; status: 404 | 400; error: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transfers an Assignment's underlying AssetItem/License to a new employee —
|
||||||
|
* docs/asset-license-management.md's "## Assignment (chốt 2026-08-02)":
|
||||||
|
* "Chuyển tài sản giữa nhân viên ... = đóng Assignment cũ (returnedAt = now)
|
||||||
|
* + tạo Assignment mới, cả hai trong 1 transaction." Closes the existing
|
||||||
|
* Assignment row (returnedAt = now) and creates a brand-new one for the new
|
||||||
|
* employee, reusing the SAME assetItemId/licenseId as the original, all
|
||||||
|
* inside a single `prisma.$transaction()`.
|
||||||
|
*
|
||||||
|
* Deliberately does NOT touch AssetItem.status or License.seatsUsed — unlike
|
||||||
|
* createAssignment()/returnAssignment(), a transfer is a same-resource
|
||||||
|
* handover, not a release-then-reacquire: the AssetItem never becomes
|
||||||
|
* "in_stock" and the License seat is never freed at any point during a
|
||||||
|
* transfer (no race window for a third party to grab either one mid-transfer
|
||||||
|
* — that's the whole point of doing this in one transaction instead of a
|
||||||
|
* separate PATCH-return followed by a POST-create, which is exactly the gap
|
||||||
|
* this function closes). AssetItem.status stays "assigned" throughout;
|
||||||
|
* seatsUsed is net zero (same seat, new holder).
|
||||||
|
*
|
||||||
|
* Race-condition note: mirrors returnAssignment()'s exact race-closing
|
||||||
|
* pattern — double-transfer (or transfer-racing-return) is closed by the
|
||||||
|
* same atomic, WHERE-guarded `updateMany` (`where: { id, returnedAt: null }`)
|
||||||
|
* on the OLD Assignment row. Only the first caller to commit gets
|
||||||
|
* `count: 1`; a second, concurrent call against the same id gets `count: 0`
|
||||||
|
* and is rejected as "already returned" — same semantics as
|
||||||
|
* returnAssignment(), since from the old Assignment row's point of view a
|
||||||
|
* transfer's first half *is* a return. The `findUnique` right before the
|
||||||
|
* `updateMany` is NOT the race-closing check by itself (same caveat as
|
||||||
|
* createAssignment()/returnAssignment()'s doc comments) — it only exists to
|
||||||
|
* read assetItemId/licenseId for the new row and to distinguish "not found"
|
||||||
|
* from "already returned" in the common (non-racing) case.
|
||||||
|
*/
|
||||||
|
export async function transferAssignment(
|
||||||
|
id: string,
|
||||||
|
input: { employeeId: string }
|
||||||
|
): Promise<TransferAssignmentResult> {
|
||||||
|
try {
|
||||||
|
const created = await prisma.$transaction(async (tx) => {
|
||||||
|
const assignment = await tx.assignment.findUnique({ where: { id } });
|
||||||
|
if (!assignment) {
|
||||||
|
throw new AssignmentNotFoundError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic race-closing gate — see this function's doc comment. Also
|
||||||
|
// doubles as the "already returned" rejection in the non-racing case.
|
||||||
|
const claim = await tx.assignment.updateMany({
|
||||||
|
where: { id, returnedAt: null },
|
||||||
|
data: { returnedAt: new Date() },
|
||||||
|
});
|
||||||
|
if (claim.count === 0) {
|
||||||
|
throw new AssignmentBusinessError("Assignment has already been returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.assignment.create({
|
||||||
|
data: {
|
||||||
|
employeeId: input.employeeId,
|
||||||
|
assetItemId: assignment.assetItemId,
|
||||||
|
licenseId: assignment.licenseId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: true, value: toRecord(created) };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AssignmentNotFoundError) {
|
||||||
|
return { ok: false, status: 404, error: "Assignment not found" };
|
||||||
|
}
|
||||||
|
if (error instanceof AssignmentBusinessError) {
|
||||||
|
return { ok: false, status: 400, error: error.message };
|
||||||
|
}
|
||||||
|
// Only employeeId can fail with a raw FK violation here — assetItemId/
|
||||||
|
// licenseId are reused verbatim from an already-valid Assignment row,
|
||||||
|
// so they're guaranteed to still reference existing rows (nothing in
|
||||||
|
// this codebase deletes AssetItem/License rows that have Assignments).
|
||||||
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
error: "employeeId does not reference an existing employee",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import {
|
|||||||
createAssignment,
|
createAssignment,
|
||||||
listAssignments,
|
listAssignments,
|
||||||
returnAssignment,
|
returnAssignment,
|
||||||
|
transferAssignment,
|
||||||
validateCreateAssignmentInput,
|
validateCreateAssignmentInput,
|
||||||
validateReturnAssignmentInput,
|
validateReturnAssignmentInput,
|
||||||
|
validateTransferAssignmentInput,
|
||||||
} from "@/lib/assignments";
|
} from "@/lib/assignments";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
@@ -542,6 +544,149 @@ describe("returnAssignment", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("transferAssignment", () => {
|
||||||
|
it("transfers an AssetItem assignment to a new employee, keeping status assigned throughout", async () => {
|
||||||
|
const employee1 = await createTestEmployee({ name: "Employee 1" });
|
||||||
|
const employee2 = await createTestEmployee({ name: "Employee 2" });
|
||||||
|
const modelId = await createTestModelId();
|
||||||
|
const assetItem = await createTestAssetItem({ modelId });
|
||||||
|
|
||||||
|
const created = await createAssignment({ employeeId: employee1.id, assetItemId: assetItem.id });
|
||||||
|
expect(created.ok).toBe(true);
|
||||||
|
if (!created.ok) return;
|
||||||
|
|
||||||
|
const transferred = await transferAssignment(created.value.id, { employeeId: employee2.id });
|
||||||
|
expect(transferred.ok).toBe(true);
|
||||||
|
if (!transferred.ok) return;
|
||||||
|
expect(transferred.value.employee_id).toBe(employee2.id);
|
||||||
|
expect(transferred.value.asset_item_id).toBe(assetItem.id);
|
||||||
|
expect(transferred.value.returned_at).toBeNull();
|
||||||
|
|
||||||
|
// The AssetItem must remain "assigned" throughout the transfer — never
|
||||||
|
// flipped to in_stock mid-way (that's the whole point of doing this in
|
||||||
|
// one transaction rather than a separate return + create).
|
||||||
|
const item = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } });
|
||||||
|
expect(item.status).toBe("assigned");
|
||||||
|
|
||||||
|
const original = await prisma.assignment.findUniqueOrThrow({ where: { id: created.value.id } });
|
||||||
|
expect(original.returnedAt).not.toBeNull();
|
||||||
|
|
||||||
|
const rows = await prisma.assignment.findMany({ where: { assetItemId: assetItem.id } });
|
||||||
|
expect(rows).toHaveLength(2);
|
||||||
|
const newRow = rows.find((row) => row.id !== created.value.id);
|
||||||
|
expect(newRow).toBeDefined();
|
||||||
|
expect(newRow?.employeeId).toBe(employee2.id);
|
||||||
|
expect(newRow?.returnedAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("transfers a License assignment to a new employee without changing seatsUsed", async () => {
|
||||||
|
const employee1 = await createTestEmployee({ name: "Employee 1" });
|
||||||
|
const employee2 = await createTestEmployee({ name: "Employee 2" });
|
||||||
|
const licenseId = await createTestLicenseId({ seatsTotal: 5, seatsUsed: 2 });
|
||||||
|
|
||||||
|
const created = await createAssignment({ employeeId: employee1.id, licenseId });
|
||||||
|
expect(created.ok).toBe(true);
|
||||||
|
if (!created.ok) return;
|
||||||
|
|
||||||
|
let license = await prisma.license.findUniqueOrThrow({ where: { id: licenseId } });
|
||||||
|
expect(license.seatsUsed).toBe(3);
|
||||||
|
|
||||||
|
const transferred = await transferAssignment(created.value.id, { employeeId: employee2.id });
|
||||||
|
expect(transferred.ok).toBe(true);
|
||||||
|
if (!transferred.ok) return;
|
||||||
|
expect(transferred.value.employee_id).toBe(employee2.id);
|
||||||
|
expect(transferred.value.license_id).toBe(licenseId);
|
||||||
|
|
||||||
|
license = await prisma.license.findUniqueOrThrow({ where: { id: licenseId } });
|
||||||
|
expect(license.seatsUsed).toBe(3);
|
||||||
|
|
||||||
|
const original = await prisma.assignment.findUniqueOrThrow({ where: { id: created.value.id } });
|
||||||
|
expect(original.returnedAt).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects transferring an already-returned assignment", async () => {
|
||||||
|
const employee1 = await createTestEmployee({ name: "Employee 1" });
|
||||||
|
const employee2 = await createTestEmployee({ name: "Employee 2" });
|
||||||
|
const modelId = await createTestModelId();
|
||||||
|
const assetItem = await createTestAssetItem({ modelId });
|
||||||
|
|
||||||
|
const created = await createAssignment({ employeeId: employee1.id, assetItemId: assetItem.id });
|
||||||
|
expect(created.ok).toBe(true);
|
||||||
|
if (!created.ok) return;
|
||||||
|
|
||||||
|
const returned = await returnAssignment(created.value.id, {});
|
||||||
|
expect(returned.ok).toBe(true);
|
||||||
|
|
||||||
|
const transferred = await transferAssignment(created.value.id, { employeeId: employee2.id });
|
||||||
|
expect(transferred.ok).toBe(false);
|
||||||
|
if (transferred.ok) return;
|
||||||
|
expect(transferred.status).toBe(400);
|
||||||
|
expect(transferred.error).toMatch(/already been returned/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 404 when transferring a nonexistent assignment", async () => {
|
||||||
|
const employee = await createTestEmployee();
|
||||||
|
const result = await transferAssignment("00000000-0000-0000-0000-000000000000", {
|
||||||
|
employeeId: employee.id,
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (result.ok) return;
|
||||||
|
expect(result.status).toBe(404);
|
||||||
|
expect(result.error).toMatch(/not found/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a clean 400 when transferring to a nonexistent employeeId", async () => {
|
||||||
|
const employee1 = await createTestEmployee({ name: "Employee 1" });
|
||||||
|
const modelId = await createTestModelId();
|
||||||
|
const assetItem = await createTestAssetItem({ modelId });
|
||||||
|
|
||||||
|
const created = await createAssignment({ employeeId: employee1.id, assetItemId: assetItem.id });
|
||||||
|
expect(created.ok).toBe(true);
|
||||||
|
if (!created.ok) return;
|
||||||
|
|
||||||
|
const transferred = await transferAssignment(created.value.id, {
|
||||||
|
employeeId: "00000000-0000-0000-0000-000000000000",
|
||||||
|
});
|
||||||
|
expect(transferred.ok).toBe(false);
|
||||||
|
if (transferred.ok) return;
|
||||||
|
expect(transferred.status).toBe(400);
|
||||||
|
expect(transferred.error).toMatch(/employeeId/i);
|
||||||
|
expect(transferred.error).toMatch(/does not reference an existing employee/i);
|
||||||
|
|
||||||
|
// Original assignment must not have been closed — the whole transfer
|
||||||
|
// must have rolled back.
|
||||||
|
const original = await prisma.assignment.findUniqueOrThrow({ where: { id: created.value.id } });
|
||||||
|
expect(original.returnedAt).toBeNull();
|
||||||
|
const item = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } });
|
||||||
|
expect(item.status).toBe("assigned");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateTransferAssignmentInput", () => {
|
||||||
|
it("accepts a valid employeeId", () => {
|
||||||
|
const result = validateTransferAssignmentInput({
|
||||||
|
employeeId: "11111111-1111-1111-1111-111111111111",
|
||||||
|
});
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a missing employeeId", () => {
|
||||||
|
const result = validateTransferAssignmentInput({});
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toMatch(/employeeId/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a non-UUID employeeId", () => {
|
||||||
|
const result = validateTransferAssignmentInput({ employeeId: "not-a-uuid" });
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (!result.ok) {
|
||||||
|
expect(result.error).toMatch(/employeeId/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("listAssignments", () => {
|
describe("listAssignments", () => {
|
||||||
it("lists created assignments, newest first", async () => {
|
it("lists created assignments, newest first", async () => {
|
||||||
const employee = await createTestEmployee();
|
const employee = await createTestEmployee();
|
||||||
@@ -736,6 +881,53 @@ describe("PATCH /api/assignments/[id]", () => {
|
|||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body).toEqual({ error: "Unauthorized" });
|
expect(body).toEqual({ error: "Unauthorized" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("transfers an assignment to a new employee on valid employeeId input", async () => {
|
||||||
|
const employee1 = await createTestEmployee({ name: "Employee 1" });
|
||||||
|
const employee2 = await createTestEmployee({ name: "Employee 2" });
|
||||||
|
const modelId = await createTestModelId();
|
||||||
|
const assetItem = await createTestAssetItem({ modelId });
|
||||||
|
const createRes = await POST(postRequest({ employeeId: employee1.id, assetItemId: assetItem.id }));
|
||||||
|
const created = await createRes.json();
|
||||||
|
|
||||||
|
const res = await PATCH(patchRequest({ employeeId: employee2.id }), { params: { id: created.id } });
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.employee_id).toBe(employee2.id);
|
||||||
|
expect(body.asset_item_id).toBe(assetItem.id);
|
||||||
|
expect(body.returned_at).toBeNull();
|
||||||
|
|
||||||
|
const item = await prisma.assetItem.findUniqueOrThrow({ where: { id: assetItem.id } });
|
||||||
|
expect(item.status).toBe("assigned");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a body with both employeeId and returnedAt with a mutual-exclusivity 400", async () => {
|
||||||
|
const employee1 = await createTestEmployee({ name: "Employee 1" });
|
||||||
|
const employee2 = await createTestEmployee({ name: "Employee 2" });
|
||||||
|
const modelId = await createTestModelId();
|
||||||
|
const assetItem = await createTestAssetItem({ modelId });
|
||||||
|
const createRes = await POST(postRequest({ employeeId: employee1.id, assetItemId: assetItem.id }));
|
||||||
|
const created = await createRes.json();
|
||||||
|
|
||||||
|
const res = await PATCH(
|
||||||
|
patchRequest({ employeeId: employee2.id, returnedAt: "2026-08-01" }),
|
||||||
|
{ params: { id: created.id } }
|
||||||
|
);
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.error).toMatch(/either employeeId.*or returnedAt/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 401 for a transfer request without a session", async () => {
|
||||||
|
const employee = await createTestEmployee();
|
||||||
|
mockedAuth.mockResolvedValue(null);
|
||||||
|
const res = await PATCH(patchRequest({ employeeId: employee.id }), {
|
||||||
|
params: { id: "00000000-0000-0000-0000-000000000000" },
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(401);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body).toEqual({ error: "Unauthorized" });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("auth gating on /api/assignments", () => {
|
describe("auth gating on /api/assignments", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user