From d3992e56616d438e89f9ab1504715ed43a465d53 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Fri, 10 Jul 2026 15:52:33 -0400 Subject: [PATCH] test(api): cover internal-org participant filter in automation-failure notifiers Adds regression cases for `notifyAutomationFailures` and `notifyBulkAutomationFailures` asserting the recipient `member.findMany` query applies `orgParticipantMemberWhereForFlag`: platform admins are excluded for customer orgs (isInternal false) and included for internal orgs (isInternal true). Guards against a future change to the participation helper silently routing automation-failure notifications to the wrong recipients (cubic P3 on the release PR #3392). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01XzEVwhhoFghng6SDMGecMq --- .../src/tasks/task-notifier.service.spec.ts | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/apps/api/src/tasks/task-notifier.service.spec.ts b/apps/api/src/tasks/task-notifier.service.spec.ts index 87effb9d58..0132fa2730 100644 --- a/apps/api/src/tasks/task-notifier.service.spec.ts +++ b/apps/api/src/tasks/task-notifier.service.spec.ts @@ -312,4 +312,105 @@ describe('TaskNotifierService', () => { expect(triggerEmailMock).not.toHaveBeenCalled(); }); }); + + // Regression: the automation-failure recipient query must apply the + // internal-org participant filter (orgParticipantMemberWhereForFlag) so + // platform admins are excluded from customer orgs but included in internal + // ones. A change to that helper must not silently reroute these notifications. + const CUSTOMER_PARTICIPANT_WHERE = { + AND: [{ user: { OR: [{ role: { not: 'admin' } }, { role: null }] } }], + }; + + const memberWhere = () => mockDb.member.findMany.mock.calls[0][0].where; + + describe('notifyAutomationFailures — participant filter', () => { + it('excludes platform admins for a customer org (isInternal false)', async () => { + mockDb.organization.findUnique.mockResolvedValue({ + name: 'Acme', + isInternal: false, + }); + mockDb.task.findUnique.mockResolvedValue({ assignee: null }); + mockDb.member.findMany.mockResolvedValue([]); + + await service.notifyAutomationFailures({ + organizationId: 'org_1', + taskId: 'tsk_1', + taskTitle: '2FA', + failedCount: 1, + totalCount: 2, + taskStatusChanged: false, + }); + + expect(memberWhere()).toEqual({ + organizationId: 'org_1', + deactivated: false, + ...CUSTOMER_PARTICIPANT_WHERE, + }); + }); + + it('includes platform admins for an internal org (isInternal true)', async () => { + mockDb.organization.findUnique.mockResolvedValue({ + name: 'Comp AI', + isInternal: true, + }); + mockDb.task.findUnique.mockResolvedValue({ assignee: null }); + mockDb.member.findMany.mockResolvedValue([]); + + await service.notifyAutomationFailures({ + organizationId: 'org_1', + taskId: 'tsk_1', + taskTitle: '2FA', + failedCount: 1, + totalCount: 2, + taskStatusChanged: false, + }); + + expect(memberWhere()).toEqual({ + organizationId: 'org_1', + deactivated: false, + }); + }); + }); + + describe('notifyBulkAutomationFailures — participant filter', () => { + const bulkParams = { + organizationId: 'org_1', + tasks: [ + { taskId: 'tsk_1', taskTitle: 't1', failedCount: 1, totalCount: 2 }, + ], + }; + + it('excludes platform admins for a customer org (isInternal false)', async () => { + mockDb.organization.findUnique.mockResolvedValue({ + name: 'Acme', + isInternal: false, + }); + mockDb.task.findMany.mockResolvedValue([{ id: 'tsk_1', assignee: null }]); + mockDb.member.findMany.mockResolvedValue([]); + + await service.notifyBulkAutomationFailures(bulkParams); + + expect(memberWhere()).toEqual({ + organizationId: 'org_1', + deactivated: false, + ...CUSTOMER_PARTICIPANT_WHERE, + }); + }); + + it('includes platform admins for an internal org (isInternal true)', async () => { + mockDb.organization.findUnique.mockResolvedValue({ + name: 'Comp AI', + isInternal: true, + }); + mockDb.task.findMany.mockResolvedValue([{ id: 'tsk_1', assignee: null }]); + mockDb.member.findMany.mockResolvedValue([]); + + await service.notifyBulkAutomationFailures(bulkParams); + + expect(memberWhere()).toEqual({ + organizationId: 'org_1', + deactivated: false, + }); + }); + }); });