fix(api): harden notification task status updates
This commit is contained in:
@@ -336,6 +336,45 @@ describe("notification.createBroadcast", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects broadcasts with no recipients before opening a broadcast transaction", async () => {
|
||||
resolveRecipientsMock.mockResolvedValue([]);
|
||||
|
||||
const txCreateBroadcast = vi.fn();
|
||||
const txUpdateBroadcast = vi.fn();
|
||||
const tx = {
|
||||
notificationBroadcast: {
|
||||
create: txCreateBroadcast,
|
||||
update: txUpdateBroadcast,
|
||||
},
|
||||
};
|
||||
const transaction = vi.fn(async (callback: (db: typeof tx) => Promise<unknown>) => callback(tx));
|
||||
const create = vi.fn();
|
||||
const update = vi.fn();
|
||||
const db = {
|
||||
$transaction: transaction,
|
||||
notificationBroadcast: {
|
||||
create,
|
||||
update,
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
|
||||
await expect(caller.createBroadcast({
|
||||
title: "Ops update",
|
||||
targetType: "all",
|
||||
})).rejects.toMatchObject({
|
||||
code: "BAD_REQUEST",
|
||||
message: "No recipients matched the broadcast target.",
|
||||
});
|
||||
|
||||
expect(transaction).not.toHaveBeenCalled();
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
expect(txCreateBroadcast).not.toHaveBeenCalled();
|
||||
expect(txUpdateBroadcast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects scheduled broadcasts when no recipients match the target", async () => {
|
||||
resolveRecipientsMock.mockResolvedValue([]);
|
||||
|
||||
@@ -1103,6 +1142,7 @@ describe("notification.updateTaskStatus", () => {
|
||||
where: {
|
||||
id: "task_1",
|
||||
OR: [{ userId: "user_1" }, { assigneeId: "user_1" }],
|
||||
category: { in: ["TASK", "APPROVAL"] },
|
||||
},
|
||||
});
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
@@ -1138,11 +1178,87 @@ describe("notification.updateTaskStatus", () => {
|
||||
|
||||
expect(db.notification.update).toHaveBeenCalledWith({
|
||||
where: { id: "task_2" },
|
||||
data: { taskStatus: "IN_PROGRESS" },
|
||||
data: {
|
||||
taskStatus: "IN_PROGRESS",
|
||||
completedAt: null,
|
||||
completedBy: null,
|
||||
},
|
||||
});
|
||||
expect(emitTaskStatusChanged).toHaveBeenNthCalledWith(1, "user_1", "task_2");
|
||||
expect(emitTaskStatusChanged).toHaveBeenNthCalledWith(2, "user_3", "task_2");
|
||||
});
|
||||
|
||||
it("clears completion metadata when reopening a completed task", async () => {
|
||||
const db = withUserLookup({
|
||||
notification: {
|
||||
findFirst: vi.fn().mockResolvedValue({
|
||||
id: "task_3",
|
||||
userId: "user_1",
|
||||
assigneeId: "user_3",
|
||||
taskStatus: "DONE",
|
||||
completedAt: new Date("2026-04-01T10:00:00Z"),
|
||||
completedBy: "user_2",
|
||||
}),
|
||||
update: vi.fn().mockResolvedValue({
|
||||
id: "task_3",
|
||||
taskStatus: "OPEN",
|
||||
completedAt: null,
|
||||
completedBy: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const caller = createProtectedCaller(db);
|
||||
const result = await caller.updateTaskStatus({ id: "task_3", status: "OPEN" });
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: "task_3",
|
||||
taskStatus: "OPEN",
|
||||
completedAt: null,
|
||||
completedBy: null,
|
||||
});
|
||||
expect(db.notification.update).toHaveBeenCalledWith({
|
||||
where: { id: "task_3" },
|
||||
data: {
|
||||
taskStatus: "OPEN",
|
||||
completedAt: null,
|
||||
completedBy: null,
|
||||
},
|
||||
});
|
||||
expect(emitTaskStatusChanged).toHaveBeenNthCalledWith(1, "user_1", "task_3");
|
||||
expect(emitTaskStatusChanged).toHaveBeenNthCalledWith(2, "user_3", "task_3");
|
||||
expect(emitTaskCompleted).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects task status updates for non-task notifications", async () => {
|
||||
const findFirst = vi.fn().mockResolvedValue(null);
|
||||
const update = vi.fn();
|
||||
const db = withUserLookup({
|
||||
notification: {
|
||||
findFirst,
|
||||
update,
|
||||
},
|
||||
});
|
||||
|
||||
const caller = createProtectedCaller(db);
|
||||
await expect(caller.updateTaskStatus({ id: "notif_info_1", status: "DONE" })).rejects.toMatchObject(
|
||||
{
|
||||
code: "NOT_FOUND",
|
||||
message: "Task not found or you do not have permission",
|
||||
},
|
||||
);
|
||||
|
||||
expect(findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: "notif_info_1",
|
||||
OR: [{ userId: "user_1" }, { assigneeId: "user_1" }],
|
||||
category: { in: ["TASK", "APPROVAL"] },
|
||||
},
|
||||
});
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
expect(emitTaskCompleted).not.toHaveBeenCalled();
|
||||
expect(emitTaskStatusChanged).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("notification.assignTask", () => {
|
||||
|
||||
@@ -131,6 +131,7 @@ export async function updateNotificationTaskStatus(
|
||||
where: {
|
||||
id: input.id,
|
||||
OR: [{ userId }, { assigneeId: userId }],
|
||||
category: { in: ["TASK", "APPROVAL"] },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -146,7 +147,9 @@ export async function updateNotificationTaskStatus(
|
||||
where: { id: input.id },
|
||||
data: {
|
||||
taskStatus: input.status,
|
||||
...(isCompleting ? { completedAt: new Date(), completedBy: userId } : {}),
|
||||
...(isCompleting
|
||||
? { completedAt: new Date(), completedBy: userId }
|
||||
: { completedAt: null, completedBy: null }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user