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 () => {
|
it("rejects scheduled broadcasts when no recipients match the target", async () => {
|
||||||
resolveRecipientsMock.mockResolvedValue([]);
|
resolveRecipientsMock.mockResolvedValue([]);
|
||||||
|
|
||||||
@@ -1103,6 +1142,7 @@ describe("notification.updateTaskStatus", () => {
|
|||||||
where: {
|
where: {
|
||||||
id: "task_1",
|
id: "task_1",
|
||||||
OR: [{ userId: "user_1" }, { assigneeId: "user_1" }],
|
OR: [{ userId: "user_1" }, { assigneeId: "user_1" }],
|
||||||
|
category: { in: ["TASK", "APPROVAL"] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(update).toHaveBeenCalledWith({
|
expect(update).toHaveBeenCalledWith({
|
||||||
@@ -1138,11 +1178,87 @@ describe("notification.updateTaskStatus", () => {
|
|||||||
|
|
||||||
expect(db.notification.update).toHaveBeenCalledWith({
|
expect(db.notification.update).toHaveBeenCalledWith({
|
||||||
where: { id: "task_2" },
|
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(1, "user_1", "task_2");
|
||||||
expect(emitTaskStatusChanged).toHaveBeenNthCalledWith(2, "user_3", "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", () => {
|
describe("notification.assignTask", () => {
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export async function updateNotificationTaskStatus(
|
|||||||
where: {
|
where: {
|
||||||
id: input.id,
|
id: input.id,
|
||||||
OR: [{ userId }, { assigneeId: userId }],
|
OR: [{ userId }, { assigneeId: userId }],
|
||||||
|
category: { in: ["TASK", "APPROVAL"] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,7 +147,9 @@ export async function updateNotificationTaskStatus(
|
|||||||
where: { id: input.id },
|
where: { id: input.id },
|
||||||
data: {
|
data: {
|
||||||
taskStatus: input.status,
|
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