feat(platform): checkpoint current implementation state
This commit is contained in:
@@ -72,6 +72,21 @@ function createManagerCaller(db: Record<string, unknown>) {
|
||||
});
|
||||
}
|
||||
|
||||
function createAdminCaller(db: Record<string, unknown>) {
|
||||
return createCaller({
|
||||
session: {
|
||||
user: { email: "admin@example.com", name: "Admin", image: null },
|
||||
expires: "2099-01-01T00:00:00.000Z",
|
||||
},
|
||||
db: db as never,
|
||||
dbUser: {
|
||||
id: "user_admin",
|
||||
systemRole: SystemRole.ADMIN,
|
||||
permissionOverrides: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── Sample data ──────────────────────────────────────────────────────────────
|
||||
|
||||
function sampleNotification(overrides: Record<string, unknown> = {}) {
|
||||
@@ -281,6 +296,45 @@ describe("notification.create", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults task-like managed notifications to OPEN when no taskStatus is provided", async () => {
|
||||
const created = sampleNotification({
|
||||
userId: "target_user",
|
||||
category: "TASK",
|
||||
taskStatus: "OPEN",
|
||||
});
|
||||
const db = withUserLookup(
|
||||
{
|
||||
notification: {
|
||||
create: vi.fn().mockResolvedValue(created),
|
||||
findUnique: vi.fn().mockResolvedValue(created),
|
||||
},
|
||||
},
|
||||
"user_mgr",
|
||||
);
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
const result = await caller.create({
|
||||
userId: "target_user",
|
||||
type: "TASK_CREATED",
|
||||
title: "Review proposal",
|
||||
category: "TASK",
|
||||
});
|
||||
|
||||
expect(db.notification.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
category: "TASK",
|
||||
taskStatus: "OPEN",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
id: "notif_1",
|
||||
category: "TASK",
|
||||
taskStatus: "OPEN",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects creation by a regular user (FORBIDDEN)", async () => {
|
||||
const db = withUserLookup({
|
||||
notification: {
|
||||
@@ -293,6 +347,36 @@ describe("notification.create", () => {
|
||||
caller.create({ userId: "target", type: "INFO", title: "Nope" }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("maps missing notification recipients to a not found error", async () => {
|
||||
const db = withUserLookup(
|
||||
{
|
||||
notification: {
|
||||
create: vi.fn().mockRejectedValue({
|
||||
code: "P2003",
|
||||
message: "Foreign key constraint failed",
|
||||
meta: { field_name: "Notification_userId_fkey" },
|
||||
}),
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
},
|
||||
"user_mgr",
|
||||
);
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
|
||||
await expect(caller.create({
|
||||
userId: "user_missing",
|
||||
type: "INFO",
|
||||
title: "Test notification",
|
||||
})).rejects.toMatchObject({
|
||||
code: "NOT_FOUND",
|
||||
message: "Notification recipient user not found",
|
||||
});
|
||||
|
||||
expect(db.notification.findUnique).not.toHaveBeenCalled();
|
||||
expect(emitNotificationCreated).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── createBroadcast ────────────────────────────────────────────────────────
|
||||
@@ -590,6 +674,75 @@ describe("notification.createBroadcast", () => {
|
||||
expect(emitTaskAssigned).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("maps missing broadcast recipients during fan-out to not found errors", async () => {
|
||||
resolveRecipientsMock.mockResolvedValue(["user_a", "user_missing"]);
|
||||
|
||||
const txCreateBroadcast = vi.fn().mockResolvedValue({
|
||||
id: "broadcast_tx_missing_recipient",
|
||||
title: "Ops update",
|
||||
createdAt: new Date("2026-03-30T10:00:00Z"),
|
||||
});
|
||||
const txUpdateBroadcast = vi.fn();
|
||||
const txCreateNotification = vi.fn()
|
||||
.mockResolvedValueOnce({ id: "notif_a", userId: "user_a" })
|
||||
.mockRejectedValueOnce(
|
||||
Object.assign(new Error("Foreign key constraint failed"), {
|
||||
code: "P2003",
|
||||
meta: { field_name: "Notification_userId_fkey" },
|
||||
}),
|
||||
);
|
||||
const tx = {
|
||||
notificationBroadcast: {
|
||||
create: txCreateBroadcast,
|
||||
update: txUpdateBroadcast,
|
||||
},
|
||||
notification: {
|
||||
create: txCreateNotification,
|
||||
},
|
||||
};
|
||||
const outerCreateBroadcast = vi.fn();
|
||||
const outerUpdateBroadcast = vi.fn();
|
||||
const outerCreateNotification = vi.fn();
|
||||
const db = {
|
||||
$transaction: vi.fn(async (callback: (db: typeof tx) => Promise<unknown>) => callback(tx)),
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
notificationBroadcast: {
|
||||
create: outerCreateBroadcast,
|
||||
update: outerUpdateBroadcast,
|
||||
},
|
||||
notification: {
|
||||
create: outerCreateNotification,
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
|
||||
await expect(caller.createBroadcast({
|
||||
title: "Ops update",
|
||||
body: "Email everyone",
|
||||
channel: "both",
|
||||
targetType: "all",
|
||||
})).rejects.toMatchObject({
|
||||
code: "NOT_FOUND",
|
||||
message: "Broadcast recipient user not found",
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
|
||||
expect(txCreateBroadcast).toHaveBeenCalledTimes(1);
|
||||
expect(txCreateNotification).toHaveBeenCalledTimes(2);
|
||||
expect(txUpdateBroadcast).not.toHaveBeenCalled();
|
||||
expect(outerCreateBroadcast).not.toHaveBeenCalled();
|
||||
expect(outerUpdateBroadcast).not.toHaveBeenCalled();
|
||||
expect(outerCreateNotification).not.toHaveBeenCalled();
|
||||
expect(db.user.findUnique).not.toHaveBeenCalled();
|
||||
expect(sendEmailMock).not.toHaveBeenCalled();
|
||||
expect(emitNotificationCreated).not.toHaveBeenCalled();
|
||||
expect(emitTaskAssigned).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits recipient SSE only after an immediate broadcast commits", async () => {
|
||||
resolveRecipientsMock.mockResolvedValue(["user_a", "user_b"]);
|
||||
|
||||
@@ -1301,6 +1454,78 @@ describe("notification.updateTaskStatus", () => {
|
||||
});
|
||||
|
||||
describe("notification.assignTask", () => {
|
||||
it("returns NOT_FOUND when assigning a missing task", async () => {
|
||||
const update = vi.fn();
|
||||
const db = {
|
||||
notification: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
update,
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
|
||||
await expect(caller.assignTask({ id: "task_missing", assigneeId: "user_4" })).rejects.toMatchObject({
|
||||
code: "NOT_FOUND",
|
||||
message: "Task not found",
|
||||
});
|
||||
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
expect(emitTaskAssigned).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("maps missing task recipients to a not found error without side effects", async () => {
|
||||
const db = {
|
||||
notification: {
|
||||
create: vi.fn().mockRejectedValue({
|
||||
code: "P2003",
|
||||
message: "Foreign key constraint failed",
|
||||
meta: { field_name: "Notification_userId_fkey" },
|
||||
}),
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
|
||||
await expect(caller.createTask({
|
||||
userId: "user_missing",
|
||||
title: "Review proposal",
|
||||
channel: "in_app",
|
||||
})).rejects.toMatchObject({
|
||||
code: "NOT_FOUND",
|
||||
message: "Task recipient user not found",
|
||||
});
|
||||
|
||||
expect(db.notification.findUnique).not.toHaveBeenCalled();
|
||||
expect(emitNotificationCreated).not.toHaveBeenCalled();
|
||||
expect(emitTaskAssigned).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects assigning non-task notifications", async () => {
|
||||
const update = vi.fn();
|
||||
const db = {
|
||||
notification: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
id: "notif_9",
|
||||
category: "REMINDER",
|
||||
assigneeId: null,
|
||||
}),
|
||||
update,
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
|
||||
await expect(caller.assignTask({ id: "notif_9", assigneeId: "user_4" })).rejects.toMatchObject({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Only tasks and approvals can be assigned",
|
||||
});
|
||||
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
expect(emitTaskAssigned).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reassigns a task and emits the assignment event for the new assignee", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "task_9",
|
||||
@@ -1365,6 +1590,103 @@ describe("notification.assignTask", () => {
|
||||
});
|
||||
expect(emitTaskAssigned).not.toHaveBeenCalledWith("user_missing", "task_9");
|
||||
});
|
||||
|
||||
it("returns NOT_FOUND when the task disappears before reassignment is persisted", async () => {
|
||||
const findUnique = vi.fn().mockResolvedValue({
|
||||
id: "task_9",
|
||||
category: "TASK",
|
||||
assigneeId: "user_2",
|
||||
});
|
||||
const update = vi.fn().mockRejectedValue(
|
||||
Object.assign(new Error("Record to update not found"), {
|
||||
code: "P2025",
|
||||
}),
|
||||
);
|
||||
const db = {
|
||||
notification: {
|
||||
findUnique,
|
||||
update,
|
||||
},
|
||||
};
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
|
||||
await expect(caller.assignTask({ id: "task_9", assigneeId: "user_4" })).rejects.toMatchObject({
|
||||
code: "NOT_FOUND",
|
||||
message: "Task not found",
|
||||
});
|
||||
|
||||
expect(findUnique).toHaveBeenCalledWith({ where: { id: "task_9" } });
|
||||
expect(update).toHaveBeenCalledWith({
|
||||
where: { id: "task_9" },
|
||||
data: { assigneeId: "user_4" },
|
||||
});
|
||||
expect(emitTaskAssigned).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("notification.executeTaskAction", () => {
|
||||
it("rejects dismissed tasks before executing their domain action", async () => {
|
||||
const updateAssignment = vi.fn();
|
||||
const db = withUserLookup({
|
||||
notification: {
|
||||
findFirst: vi.fn().mockResolvedValue({
|
||||
id: "task_1",
|
||||
userId: "user_1",
|
||||
assigneeId: null,
|
||||
taskAction: "confirm_assignment:assign_1",
|
||||
taskStatus: "DISMISSED",
|
||||
}),
|
||||
update: vi.fn(),
|
||||
},
|
||||
assignment: {
|
||||
findUnique: vi.fn(),
|
||||
update: updateAssignment,
|
||||
},
|
||||
});
|
||||
|
||||
const caller = createAdminCaller(db);
|
||||
|
||||
await expect(caller.executeTaskAction({ id: "task_1" })).rejects.toMatchObject({
|
||||
code: "PRECONDITION_FAILED",
|
||||
message: "This task has been dismissed",
|
||||
});
|
||||
|
||||
expect(updateAssignment).not.toHaveBeenCalled();
|
||||
expect(db.notification.update).not.toHaveBeenCalled();
|
||||
expect(emitTaskCompleted).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects task action execution when transactional persistence support is unavailable", async () => {
|
||||
const updateVacation = vi.fn();
|
||||
const db = withUserLookup({
|
||||
notification: {
|
||||
findFirst: vi.fn().mockResolvedValue({
|
||||
id: "task_1",
|
||||
userId: "user_1",
|
||||
assigneeId: null,
|
||||
taskAction: "approve_vacation:vac_1",
|
||||
taskStatus: "OPEN",
|
||||
}),
|
||||
update: vi.fn(),
|
||||
},
|
||||
vacation: {
|
||||
findUnique: vi.fn(),
|
||||
update: updateVacation,
|
||||
},
|
||||
});
|
||||
|
||||
const caller = createAdminCaller(db);
|
||||
|
||||
await expect(caller.executeTaskAction({ id: "task_1" })).rejects.toMatchObject({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Task action execution requires transactional persistence support.",
|
||||
});
|
||||
|
||||
expect(updateVacation).not.toHaveBeenCalled();
|
||||
expect(db.notification.update).not.toHaveBeenCalled();
|
||||
expect(emitTaskCompleted).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── reminders ──────────────────────────────────────────────────────────────
|
||||
@@ -1467,6 +1789,28 @@ describe("notification.updateReminder", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns NOT_FOUND when the reminder is missing or belongs to another user", async () => {
|
||||
const update = vi.fn();
|
||||
const db = withUserLookup({
|
||||
notification: {
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
update,
|
||||
},
|
||||
});
|
||||
|
||||
const caller = createProtectedCaller(db);
|
||||
|
||||
await expect(caller.updateReminder({
|
||||
id: "rem_missing",
|
||||
title: "Updated reminder",
|
||||
})).rejects.toMatchObject({
|
||||
code: "NOT_FOUND",
|
||||
message: "Reminder not found or you do not have permission",
|
||||
});
|
||||
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("notification.deleteReminder", () => {
|
||||
@@ -1492,6 +1836,25 @@ describe("notification.deleteReminder", () => {
|
||||
});
|
||||
expect(deleteFn).toHaveBeenCalledWith({ where: { id: "rem_1" } });
|
||||
});
|
||||
|
||||
it("returns NOT_FOUND when the reminder is missing or belongs to another user", async () => {
|
||||
const deleteFn = vi.fn();
|
||||
const db = withUserLookup({
|
||||
notification: {
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
delete: deleteFn,
|
||||
},
|
||||
});
|
||||
|
||||
const caller = createProtectedCaller(db);
|
||||
|
||||
await expect(caller.deleteReminder({ id: "rem_missing" })).rejects.toMatchObject({
|
||||
code: "NOT_FOUND",
|
||||
message: "Reminder not found or you do not have permission",
|
||||
});
|
||||
|
||||
expect(deleteFn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("notification.listReminders", () => {
|
||||
|
||||
Reference in New Issue
Block a user