Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ function isHoldActionForTransaction(report: Report, reportTransaction: Transacti
return isProcessingReport;
}

function isChangeWorkspaceAction(report: Report, policies: OnyxCollection<Policy>, reportActions?: ReportAction[]): boolean {
function isChangeWorkspaceAction(report: Report, policies: OnyxCollection<Policy>, 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;
Expand All @@ -594,6 +594,7 @@ function isChangeWorkspaceAction(report: Report, policies: OnyxCollection<Policy
}

const submitterEmail = getLoginByAccountID(report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID);
const isReportSettled = isSettled(report);

// Find available policies - stop early once we find 2 or after checking all
let firstAvailablePolicy: Policy | undefined;
Expand All @@ -603,6 +604,10 @@ function isChangeWorkspaceAction(report: Report, policies: OnyxCollection<Policy
continue;
}

if (isReportSettled && !isPolicyAdmin(policy, currentUserLogin)) {
continue;
}

if (availablePoliciesCount === 0) {
firstAvailablePolicy = policy;
}
Expand Down Expand Up @@ -972,7 +977,7 @@ function getSecondaryReportActions({

options.push(CONST.REPORT.SECONDARY_ACTIONS.PRINT);

if (isChangeWorkspaceAction(report, policies, reportActions)) {
if (isChangeWorkspaceAction(report, policies, currentUserLogin, reportActions)) {
options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE);
}

Expand Down
92 changes: 81 additions & 11 deletions tests/unit/ReportSecondaryActionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3592,6 +3592,7 @@ describe('getSecondaryTransactionThreadActions', () => {
isWorkspaceEligibleForReportChange: MockFunction;
canEditReportPolicy: boolean;
isExported: boolean;
isSettled: boolean;
}>;

const setupMocks = (mocks: MockConfig = {}) => {
Expand All @@ -3603,6 +3604,7 @@ describe('getSecondaryTransactionThreadActions', () => {
isWorkspaceEligibleForReportChange: true,
canEditReportPolicy: true,
isExported: false,
isSettled: false,
};

for (const [method, value] of Object.entries({...defaults, ...mocks})) {
Expand All @@ -3624,87 +3626,155 @@ 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', () => {
setupMocks({isIOUReport: true});
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', () => {
setupMocks({isWorkspaceEligibleForReportChange: false});
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', () => {
setupMocks();
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', () => {
setupMocks();
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', () => {
setupMocks({isWorkspaceEligibleForReportChange: ((_, policy: Policy) => policy?.id === POLICY_ID) as MockFunction});
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', () => {
setupMocks({canEditReportPolicy: false});
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', () => {
setupMocks({isExported: true});
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', () => {
setupMocks();
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', () => {
setupMocks({isIOUReport: true, isCurrentUserSubmitter: true});
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', () => {
setupMocks({isIOUReport: true, isReportManager: true});
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<typeof PolicyUtils>('@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<typeof PolicyUtils>('@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<typeof PolicyUtils>('@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<typeof PolicyUtils>('@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<typeof PolicyUtils>('@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);
});
});
});
Loading