diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index ba1866aad9153..d361b3b6eded9 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -580,7 +580,7 @@ function isHoldActionForTransaction(report: Report, reportTransaction: Transacti return isProcessingReport; } -function isChangeWorkspaceAction(report: Report, policies: OnyxCollection, reportActions?: ReportAction[]): boolean { +function isChangeWorkspaceAction(report: Report, policies: OnyxCollection, currentUserLogin: string, reportActions?: ReportAction[]): boolean { // We can't move the iou report to the workspace if both users from the iou report create the expense if (isIOUReportUtils(report) && doesReportContainRequestsFromMultipleUsers(report)) { return false; @@ -594,6 +594,7 @@ function isChangeWorkspaceAction(report: Report, policies: OnyxCollection { isWorkspaceEligibleForReportChange: MockFunction; canEditReportPolicy: boolean; isExported: boolean; + isSettled: boolean; }>; const setupMocks = (mocks: MockConfig = {}) => { @@ -3603,6 +3604,7 @@ describe('getSecondaryTransactionThreadActions', () => { isWorkspaceEligibleForReportChange: true, canEditReportPolicy: true, isExported: false, + isSettled: false, }; for (const [method, value] of Object.entries({...defaults, ...mocks})) { @@ -3624,7 +3626,7 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport({type: CONST.REPORT.TYPE.IOU}); const policies = createPolicies(POLICY_ID); - expect(isChangeWorkspaceAction(report, policies)).toBe(false); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(false); }); it('should return false when IOU report and user is neither submitter nor manager', () => { @@ -3632,7 +3634,7 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport({type: CONST.REPORT.TYPE.IOU}); const policies = createPolicies(POLICY_ID); - expect(isChangeWorkspaceAction(report, policies)).toBe(false); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(false); }); it('should return false when there are no available policies', () => { @@ -3640,7 +3642,7 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport(); const policies = createPolicies(POLICY_ID); - expect(isChangeWorkspaceAction(report, policies)).toBe(false); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(false); }); it('should return false when only one available policy and it is the same as current report policy', () => { @@ -3648,7 +3650,7 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport({policyID: POLICY_ID}); const policies = createPolicies(POLICY_ID); - expect(isChangeWorkspaceAction(report, policies)).toBe(false); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(false); }); it('should return true when only one available policy but report has no policy', () => { @@ -3656,7 +3658,7 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport({policyID: undefined}); const policies = createPolicies(POLICY_ID); - expect(isChangeWorkspaceAction(report, policies)).toBe(true); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(true); }); it('should return true when only one available policy and it is different from current report policy', () => { @@ -3664,7 +3666,7 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport({policyID: OLD_POLICY_ID}); const policies = createPolicies(POLICY_ID, OLD_POLICY_ID); - expect(isChangeWorkspaceAction(report, policies)).toBe(true); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(true); }); it('should return false when cannot edit report policy', () => { @@ -3672,7 +3674,7 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport({policyID: OLD_POLICY_ID}); const policies = createPolicies(POLICY_ID, OLD_POLICY_ID); - expect(isChangeWorkspaceAction(report, policies)).toBe(false); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(false); }); it('should return false when report is exported', () => { @@ -3680,7 +3682,7 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport({policyID: OLD_POLICY_ID}); const policies = createPolicies(POLICY_ID, OLD_POLICY_ID); - expect(isChangeWorkspaceAction(report, policies, [])).toBe(false); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL, [])).toBe(false); }); it('should return true when multiple available policies exist', () => { @@ -3688,7 +3690,7 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport({policyID: OLD_POLICY_ID}); const policies = createPolicies(POLICY_ID, OLD_POLICY_ID, 'another_policy'); - expect(isChangeWorkspaceAction(report, policies)).toBe(true); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(true); }); it('should return true when IOU report with single user and user is submitter', () => { @@ -3696,7 +3698,7 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport({type: CONST.REPORT.TYPE.IOU, policyID: OLD_POLICY_ID}); const policies = createPolicies(POLICY_ID, OLD_POLICY_ID); - expect(isChangeWorkspaceAction(report, policies)).toBe(true); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(true); }); it('should return true when IOU report with single user and user is manager', () => { @@ -3704,7 +3706,75 @@ describe('getSecondaryTransactionThreadActions', () => { const report = createReport({type: CONST.REPORT.TYPE.IOU, policyID: OLD_POLICY_ID}); const policies = createPolicies(POLICY_ID, OLD_POLICY_ID); - expect(isChangeWorkspaceAction(report, policies)).toBe(true); + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(true); + }); + + it('should return true when report is settled and currentUserLogin is admin of available policies', () => { + setupMocks({isSettled: true}); + const mockedIsPolicyAdmin = jest.requireMock('@libs/PolicyUtils').isPolicyAdmin as jest.Mock; + mockedIsPolicyAdmin.mockReturnValue(true); + + const report = createReport({policyID: OLD_POLICY_ID}); + const policies = createPolicies(POLICY_ID, OLD_POLICY_ID); + + expect(isChangeWorkspaceAction(report, policies, ADMIN_EMAIL)).toBe(true); + }); + + it('should return false when report is settled and currentUserLogin is not admin of any policy', () => { + setupMocks({isSettled: true}); + const mockedIsPolicyAdmin = jest.requireMock('@libs/PolicyUtils').isPolicyAdmin as jest.Mock; + mockedIsPolicyAdmin.mockReturnValue(false); + + const report = createReport({policyID: OLD_POLICY_ID}); + const policies = createPolicies(POLICY_ID, OLD_POLICY_ID); + + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(false); + }); + + it('should filter policies by admin role using currentUserLogin when report is settled', () => { + setupMocks({isSettled: true}); + const mockedIsPolicyAdmin = jest.requireMock('@libs/PolicyUtils').isPolicyAdmin as jest.Mock; + mockedIsPolicyAdmin.mockImplementation((policy: Policy, login?: string) => { + return login === ADMIN_EMAIL && policy?.id === POLICY_ID; + }); + + const report = createReport({policyID: OLD_POLICY_ID}); + const policies = createPolicies(POLICY_ID, OLD_POLICY_ID); + + // Admin user sees the one eligible policy (POLICY_ID) which differs from report's OLD_POLICY_ID + expect(isChangeWorkspaceAction(report, policies, ADMIN_EMAIL)).toBe(true); + // Non-admin user has all policies filtered out + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(false); + }); + + it('should not filter policies by admin role when report is not settled', () => { + setupMocks({isSettled: false}); + const mockedIsPolicyAdmin = jest.requireMock('@libs/PolicyUtils').isPolicyAdmin as jest.Mock; + mockedIsPolicyAdmin.mockReturnValue(false); + + const report = createReport({policyID: OLD_POLICY_ID}); + const policies = createPolicies(POLICY_ID, OLD_POLICY_ID); + + // Even though isPolicyAdmin returns false, non-settled reports skip the admin check + expect(isChangeWorkspaceAction(report, policies, EMPLOYEE_EMAIL)).toBe(true); + }); + + it('should pass currentUserLogin to isPolicyAdmin for each candidate policy when settled', () => { + setupMocks({isSettled: true}); + const mockedIsPolicyAdmin = jest.requireMock('@libs/PolicyUtils').isPolicyAdmin as jest.Mock; + mockedIsPolicyAdmin.mockReturnValue(true); + + const report = createReport({policyID: OLD_POLICY_ID}); + const policies = createPolicies(POLICY_ID, OLD_POLICY_ID); + const testLogin = 'specific-user@mail.com'; + + isChangeWorkspaceAction(report, policies, testLogin); + + const callsWithLogin = mockedIsPolicyAdmin.mock.calls.filter( + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (call: unknown[]) => call.at(1) === testLogin, + ); + expect(callsWithLogin.length).toBeGreaterThan(0); }); }); });