From e78f89e5c777fce0df3f29d722975e36806fbe0d Mon Sep 17 00:00:00 2001 From: KJ21-ENG <140263938+KJ21-ENG@users.noreply.github.com> Date: Wed, 2 Sep 2026 02:56:09 +0530 Subject: [PATCH 1/2] Fix unsafe chart group-by assertions --- src/components/Search/chartGroupByConfig.ts | 68 +++--- tests/unit/Search/chartGroupByConfigTest.ts | 235 ++++++++++++++++++++ 2 files changed, 267 insertions(+), 36 deletions(-) create mode 100644 tests/unit/Search/chartGroupByConfigTest.ts diff --git a/src/components/Search/chartGroupByConfig.ts b/src/components/Search/chartGroupByConfig.ts index ffec7aa79997..9efc032ae6b7 100644 --- a/src/components/Search/chartGroupByConfig.ts +++ b/src/components/Search/chartGroupByConfig.ts @@ -2,18 +2,6 @@ import DateUtils from '@libs/DateUtils'; import CONST from '@src/CONST'; -import type { - TransactionCardGroupListItemType, - TransactionCategoryGroupListItemType, - TransactionMemberGroupListItemType, - TransactionMerchantGroupListItemType, - TransactionMonthGroupListItemType, - TransactionQuarterGroupListItemType, - TransactionTagGroupListItemType, - TransactionWeekGroupListItemType, - TransactionWithdrawalIDGroupListItemType, - TransactionYearGroupListItemType, -} from './SearchList/ListItem/types'; import type {GroupedItem, SearchGroupBy} from './types'; type ChartGroupByConfig = { @@ -37,71 +25,79 @@ type ChartGroupByConfig = { const CHART_GROUP_BY_CONFIG: Record = { [CONST.SEARCH.GROUP_BY.FROM]: { titleIconName: 'Users', - getLabel: (item: GroupedItem) => (item as TransactionMemberGroupListItemType).formattedFrom ?? '', - getFilterQuery: (item: GroupedItem) => `from:${(item as TransactionMemberGroupListItemType).accountID}`, + getLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.FROM ? (item.formattedFrom ?? '') : ''), + getFilterQuery: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.FROM ? `from:${item.accountID}` : ''), }, [CONST.SEARCH.GROUP_BY.CARD]: { titleIconName: 'CreditCard', - getLabel: (item: GroupedItem) => (item as TransactionCardGroupListItemType).formattedCardName ?? '', - getFilterQuery: (item: GroupedItem) => `cardID:${(item as TransactionCardGroupListItemType).cardID}`, + getLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.CARD ? (item.formattedCardName ?? '') : ''), + getFilterQuery: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.CARD ? `cardID:${item.cardID}` : ''), }, [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: { titleIconName: 'Send', // eslint-disable-next-line rulesdir/no-default-id-values -- formattedWithdrawalID is a display label, not an Onyx ID - getLabel: (item: GroupedItem) => (item as TransactionWithdrawalIDGroupListItemType).formattedWithdrawalID ?? '', - getFilterQuery: (item: GroupedItem) => `withdrawalID:${(item as TransactionWithdrawalIDGroupListItemType).entryID}`, + getLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID ? (item.formattedWithdrawalID ?? '') : ''), + getFilterQuery: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID ? `withdrawalID:${item.entryID}` : ''), }, [CONST.SEARCH.GROUP_BY.CATEGORY]: { titleIconName: 'Folder', - getLabel: (item: GroupedItem) => (item as TransactionCategoryGroupListItemType).formattedCategory ?? '', - getFilterQuery: (item: GroupedItem) => `category:"${(item as TransactionCategoryGroupListItemType).category}"`, + getLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.CATEGORY ? (item.formattedCategory ?? '') : ''), + getFilterQuery: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.CATEGORY ? `category:"${item.category}"` : ''), }, [CONST.SEARCH.GROUP_BY.MERCHANT]: { titleIconName: 'Basket', - getLabel: (item: GroupedItem) => (item as TransactionMerchantGroupListItemType).formattedMerchant ?? '', - getFilterQuery: (item: GroupedItem) => `merchant:"${(item as TransactionMerchantGroupListItemType).merchant}"`, + getLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.MERCHANT ? (item.formattedMerchant ?? '') : ''), + getFilterQuery: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.MERCHANT ? `merchant:"${item.merchant}"` : ''), }, [CONST.SEARCH.GROUP_BY.TAG]: { titleIconName: 'Tag', - getLabel: (item: GroupedItem) => (item as TransactionTagGroupListItemType).formattedTag ?? '', - getFilterQuery: (item: GroupedItem) => `tag:"${(item as TransactionTagGroupListItemType).tag}"`, + getLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.TAG ? (item.formattedTag ?? '') : ''), + getFilterQuery: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.TAG ? `tag:"${item.tag}"` : ''), }, [CONST.SEARCH.GROUP_BY.MONTH]: { titleIconName: 'Calendar', - getLabel: (item: GroupedItem) => (item as TransactionMonthGroupListItemType).formattedMonth ?? '', + getLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.MONTH ? (item.formattedMonth ?? '') : ''), getShortLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.MONTH ? item.shortFormattedMonth : undefined), getFilterQuery: (item: GroupedItem) => { - const monthItem = item as TransactionMonthGroupListItemType; - const {start, end} = DateUtils.getMonthDateRange(monthItem.year, monthItem.month); + if (item.groupedBy !== CONST.SEARCH.GROUP_BY.MONTH) { + return ''; + } + const {start, end} = DateUtils.getMonthDateRange(item.year, item.month); return `date>=${start} date<=${end}`; }, }, [CONST.SEARCH.GROUP_BY.WEEK]: { titleIconName: 'Calendar', - getLabel: (item: GroupedItem) => (item as TransactionWeekGroupListItemType).formattedWeek ?? '', + getLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.WEEK ? (item.formattedWeek ?? '') : ''), getShortLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.WEEK ? item.shortFormattedWeek : undefined), getFilterQuery: (item: GroupedItem) => { - const weekItem = item as TransactionWeekGroupListItemType; - const {start, end} = DateUtils.getWeekDateRange(weekItem.week); + if (item.groupedBy !== CONST.SEARCH.GROUP_BY.WEEK) { + return ''; + } + const {start, end} = DateUtils.getWeekDateRange(item.week); return `date>=${start} date<=${end}`; }, }, [CONST.SEARCH.GROUP_BY.YEAR]: { titleIconName: 'Calendar', - getLabel: (item: GroupedItem) => (item as TransactionYearGroupListItemType).formattedYear ?? '', + getLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.YEAR ? (item.formattedYear ?? '') : ''), getFilterQuery: (item: GroupedItem) => { - const yearItem = item as TransactionYearGroupListItemType; - const {start, end} = DateUtils.getYearDateRange(yearItem.year); + if (item.groupedBy !== CONST.SEARCH.GROUP_BY.YEAR) { + return ''; + } + const {start, end} = DateUtils.getYearDateRange(item.year); return `date>=${start} date<=${end}`; }, }, [CONST.SEARCH.GROUP_BY.QUARTER]: { titleIconName: 'Calendar', - getLabel: (item: GroupedItem) => (item as TransactionQuarterGroupListItemType).formattedQuarter ?? '', + getLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.QUARTER ? (item.formattedQuarter ?? '') : ''), getShortLabel: (item: GroupedItem) => (item.groupedBy === CONST.SEARCH.GROUP_BY.QUARTER ? item.shortFormattedQuarter : undefined), getFilterQuery: (item: GroupedItem) => { - const quarterItem = item as TransactionQuarterGroupListItemType; - const {start, end} = DateUtils.getQuarterDateRange(quarterItem.year, quarterItem.quarter); + if (item.groupedBy !== CONST.SEARCH.GROUP_BY.QUARTER) { + return ''; + } + const {start, end} = DateUtils.getQuarterDateRange(item.year, item.quarter); return `date>=${start} date<=${end}`; }, }, diff --git a/tests/unit/Search/chartGroupByConfigTest.ts b/tests/unit/Search/chartGroupByConfigTest.ts new file mode 100644 index 000000000000..51955ffd3100 --- /dev/null +++ b/tests/unit/Search/chartGroupByConfigTest.ts @@ -0,0 +1,235 @@ +import CHART_GROUP_BY_CONFIG from '@components/Search/chartGroupByConfig'; +import type { + TransactionCardGroupListItemType, + TransactionCategoryGroupListItemType, + TransactionMemberGroupListItemType, + TransactionMerchantGroupListItemType, + TransactionMonthGroupListItemType, + TransactionQuarterGroupListItemType, + TransactionTagGroupListItemType, + TransactionWeekGroupListItemType, + TransactionWithdrawalIDGroupListItemType, + TransactionYearGroupListItemType, +} from '@components/Search/SearchList/ListItem/types'; +import type {GroupedItem, SearchGroupBy} from '@components/Search/types'; + +import CONST from '@src/CONST'; + +import type HybridAppModuleType from '@expensify/react-native-hybrid-app/src/types'; + +import createMock from '../../utils/createMock'; + +// The Jest environment has no ReactNativeHybridApp module, which DateUtils loads through IntlStore and Log. +jest.mock('@expensify/react-native-hybrid-app', () => { + return { + __esModule: true, + default: { + isHybridApp: jest.fn, Parameters>(() => false), + }, + }; +}); + +type ChartGroupByCase = { + groupBy: SearchGroupBy; + item: GroupedItem; + expectedIcon: (typeof CHART_GROUP_BY_CONFIG)[SearchGroupBy]['titleIconName']; + expectedLabel: string; + expectedShortLabel?: string; + expectedFilterQuery: string; +}; + +const memberItem = createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.FROM, + accountID: 0, + formattedFrom: 'Ada Lovelace', +}); +const cardItem = createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.CARD, + cardID: 123, + formattedCardName: 'Travel card', +}); + +const cases: ChartGroupByCase[] = [ + { + groupBy: CONST.SEARCH.GROUP_BY.FROM, + item: memberItem, + expectedIcon: 'Users', + expectedLabel: 'Ada Lovelace', + expectedFilterQuery: 'from:0', + }, + { + groupBy: CONST.SEARCH.GROUP_BY.CARD, + item: cardItem, + expectedIcon: 'CreditCard', + expectedLabel: 'Travel card', + expectedFilterQuery: 'cardID:123', + }, + { + groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, + entryID: 456, + formattedWithdrawalID: '456', + }), + expectedIcon: 'Send', + expectedLabel: '456', + expectedFilterQuery: 'withdrawalID:456', + }, + { + groupBy: CONST.SEARCH.GROUP_BY.CATEGORY, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.CATEGORY, + category: 'Travel meals', + formattedCategory: 'Travel meals', + }), + expectedIcon: 'Folder', + expectedLabel: 'Travel meals', + expectedFilterQuery: 'category:"Travel meals"', + }, + { + groupBy: CONST.SEARCH.GROUP_BY.MERCHANT, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.MERCHANT, + merchant: 'Coffee shop', + formattedMerchant: 'Coffee shop', + }), + expectedIcon: 'Basket', + expectedLabel: 'Coffee shop', + expectedFilterQuery: 'merchant:"Coffee shop"', + }, + { + groupBy: CONST.SEARCH.GROUP_BY.TAG, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.TAG, + tag: 'Client visit', + formattedTag: 'Client visit', + }), + expectedIcon: 'Tag', + expectedLabel: 'Client visit', + expectedFilterQuery: 'tag:"Client visit"', + }, + { + groupBy: CONST.SEARCH.GROUP_BY.MONTH, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.MONTH, + year: 2025, + month: 6, + formattedMonth: 'June 2025', + shortFormattedMonth: 'Jun 25', + }), + expectedIcon: 'Calendar', + expectedLabel: 'June 2025', + expectedShortLabel: 'Jun 25', + expectedFilterQuery: 'date>=2025-06-01 date<=2025-06-30', + }, + { + groupBy: CONST.SEARCH.GROUP_BY.WEEK, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.WEEK, + week: '2025-06-09', + formattedWeek: 'Jun 9 - Jun 15, 2025', + shortFormattedWeek: 'Jun 9 - 15, 25', + }), + expectedIcon: 'Calendar', + expectedLabel: 'Jun 9 - Jun 15, 2025', + expectedShortLabel: 'Jun 9 - 15, 25', + expectedFilterQuery: 'date>=2025-06-09 date<=2025-06-15', + }, + { + groupBy: CONST.SEARCH.GROUP_BY.YEAR, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.YEAR, + year: 2025, + formattedYear: '2025', + }), + expectedIcon: 'Calendar', + expectedLabel: '2025', + expectedFilterQuery: 'date>=2025-01-01 date<=2025-12-31', + }, + { + groupBy: CONST.SEARCH.GROUP_BY.QUARTER, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.QUARTER, + year: 2025, + quarter: 3, + formattedQuarter: 'Q3 2025', + shortFormattedQuarter: 'Q3 25', + }), + expectedIcon: 'Calendar', + expectedLabel: 'Q3 2025', + expectedShortLabel: 'Q3 25', + expectedFilterQuery: 'date>=2025-07-01 date<=2025-09-30', + }, +]; + +const optionalLabelCases: Array> = [ + { + groupBy: CONST.SEARCH.GROUP_BY.FROM, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.FROM, + accountID: 1, + }), + }, + { + groupBy: CONST.SEARCH.GROUP_BY.CARD, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.CARD, + cardID: 2, + formattedCardName: '', + }), + }, + { + groupBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID, + entryID: 3, + }), + }, + { + groupBy: CONST.SEARCH.GROUP_BY.CATEGORY, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.CATEGORY, + category: '', + formattedCategory: '', + }), + }, + { + groupBy: CONST.SEARCH.GROUP_BY.MERCHANT, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.MERCHANT, + merchant: '', + }), + }, + { + groupBy: CONST.SEARCH.GROUP_BY.TAG, + item: createMock({ + groupedBy: CONST.SEARCH.GROUP_BY.TAG, + tag: '', + formattedTag: '', + }), + }, +]; + +describe('CHART_GROUP_BY_CONFIG', () => { + test.each(cases)('returns the $groupBy chart values for a matching item', ({groupBy, item, expectedIcon, expectedLabel, expectedShortLabel, expectedFilterQuery}) => { + const config = CHART_GROUP_BY_CONFIG[groupBy]; + + expect(config.titleIconName).toBe(expectedIcon); + expect(config.getLabel(item)).toBe(expectedLabel); + expect(config.getShortLabel?.(item)).toBe(expectedShortLabel); + expect(config.getFilterQuery(item)).toBe(expectedFilterQuery); + }); + + test.each(cases)('rejects an item that does not match the $groupBy configuration', ({groupBy}) => { + const config = CHART_GROUP_BY_CONFIG[groupBy]; + const mismatchedItem = groupBy === CONST.SEARCH.GROUP_BY.FROM ? cardItem : memberItem; + + expect(config.getLabel(mismatchedItem)).toBe(''); + expect(config.getShortLabel?.(mismatchedItem)).toBeUndefined(); + expect(config.getFilterQuery(mismatchedItem)).toBe(''); + }); + + test.each(optionalLabelCases)('returns an empty label for an absent or empty optional $groupBy label', ({groupBy, item}) => { + expect(CHART_GROUP_BY_CONFIG[groupBy].getLabel(item)).toBe(''); + }); +}); From 3f7473506de4d95d2a0134e2c974ae02fb74b804 Mon Sep 17 00:00:00 2001 From: KJ21-ENG <140263938+KJ21-ENG@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:07:06 +0530 Subject: [PATCH 2/2] test: mock fetch in money request redirect tests --- tests/ui/IOURequestRedirectToStartPageTest.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/ui/IOURequestRedirectToStartPageTest.tsx b/tests/ui/IOURequestRedirectToStartPageTest.tsx index ac824f014d7e..3c50e8a9455d 100644 --- a/tests/ui/IOURequestRedirectToStartPageTest.tsx +++ b/tests/ui/IOURequestRedirectToStartPageTest.tsx @@ -20,8 +20,11 @@ import {NavigationContainer} from '@react-navigation/native'; import React from 'react'; import Onyx from 'react-native-onyx'; +import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; +TestHelper.setupGlobalFetchMock(); + jest.mock('@userActions/Tab'); jest.mock('@rnmapbox/maps', () => ({ default: jest.fn(),