diff --git a/Mobile-Expensify b/Mobile-Expensify index b5aed26cfd0f..1f700f157c62 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit b5aed26cfd0f80b6dd5628761a2fc3dfaba18d49 +Subproject commit 1f700f157c6287873cf71aa5e76a71c72c16fbcf diff --git a/android/app/build.gradle b/android/app/build.gradle index 1cb7659f1203..9583f3840b0c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -114,8 +114,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009031006 - versionName "9.3.10-6" + versionCode 1009031200 + versionName "9.3.12-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 3c32e9a474b8..ec3e010bf49e 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.3.10 + 9.3.12 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 9.3.10.6 + 9.3.12.0 FullStory OrgId diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index e8023b059e64..97d459f47822 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.3.10 + 9.3.12 CFBundleVersion - 9.3.10.6 + 9.3.12.0 NSExtension NSExtensionPointIdentifier diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist index ad2db7e3903c..72f03e5298b8 100644 --- a/ios/ShareViewController/Info.plist +++ b/ios/ShareViewController/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.3.10 + 9.3.12 CFBundleVersion - 9.3.10.6 + 9.3.12.0 NSExtension NSExtensionAttributes diff --git a/package-lock.json b/package-lock.json index 648159844b30..8a2f4fbd67f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.3.10-6", + "version": "9.3.12-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.3.10-6", + "version": "9.3.12-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index fc39334b4f5a..9d98a65de9d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.3.10-6", + "version": "9.3.12-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/hooks/useCompanyCards.ts b/src/hooks/useCompanyCards.ts index 7975b56f90ec..62be8d43c3c1 100644 --- a/src/hooks/useCompanyCards.ts +++ b/src/hooks/useCompanyCards.ts @@ -26,6 +26,7 @@ type UseCompanyCardsResult = Partial<{ cardList: AssignableCardsList; assignedCards: CardList; cardNames: string[]; + cardNamesToEncryptedCardNumberMapping: Record; allCardFeeds: CombinedCardFeeds; companyCardFeeds: CompanyFeeds; selectedFeed: CombinedCardFeed; @@ -67,7 +68,16 @@ function useCompanyCards({policyID, feedName: feedNameProp}: UseCompanyCardsProp } const {cardList, ...assignedCards} = cardsList ?? {}; - const cardNames = cardFeedType === 'directFeed' ? (selectedFeed?.accountList ?? []) : Object.keys(cardList ?? {}); + const cardNamesToEncryptedCardNumberMapping: Record = {}; + + for (const cardName of selectedFeed?.accountList ?? []) { + cardNamesToEncryptedCardNumberMapping[cardName] = cardName; + } + for (const [cardName, encryptedCardNumber] of Object.entries(cardList ?? {})) { + cardNamesToEncryptedCardNumberMapping[cardName] = encryptedCardNumber; + } + + const cardNames = Object.keys(cardNamesToEncryptedCardNumberMapping); const onyxMetadata = { cardListMetadata, @@ -91,6 +101,7 @@ function useCompanyCards({policyID, feedName: feedNameProp}: UseCompanyCardsProp cardList, assignedCards, cardNames, + cardNamesToEncryptedCardNumberMapping, selectedFeed, bankName, cardFeedType, diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable/index.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable/index.tsx index 8599f5ec8c96..1439e3fff41f 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable/index.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable/index.tsx @@ -14,7 +14,7 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {resetFailedWorkspaceCompanyCardAssignment, resetFailedWorkspaceCompanyCardUnassignment} from '@libs/actions/CompanyCards'; -import {getDefaultCardName} from '@libs/CardUtils'; +import {getDefaultCardName, getPlaidInstitutionId} from '@libs/CardUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; import WorkspaceCompanyCardPageEmptyState from '@pages/workspace/companyCards/WorkspaceCompanyCardPageEmptyState'; import WorkspaceCompanyCardsFeedAddedEmptyPage from '@pages/workspace/companyCards/WorkspaceCompanyCardsFeedAddedEmptyPage'; @@ -59,17 +59,15 @@ function WorkspaceCompanyCardsTable({policyID, isPolicyLoaded, domainOrWorkspace const { feedName, bankName, - cardList, assignedCards, - cardNames, - cardFeedType, + cardNamesToEncryptedCardNumberMapping, selectedFeed, isInitiallyLoadingFeeds, isNoFeed, isFeedPending, onyxMetadata: {cardListMetadata, lastSelectedFeedMetadata}, } = companyCards; - const isDirectCardFeed = cardFeedType === 'directFeed'; + const isPlaidCardFeed = !!getPlaidInstitutionId(feedName); const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY, {canBeMissing: false}); const [personalDetails, personalDetailsMetadata] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); @@ -79,7 +77,7 @@ function WorkspaceCompanyCardsTable({policyID, isPolicyLoaded, domainOrWorkspace const hasNoAssignedCard = Object.keys(assignedCards ?? {}).length === 0; const isLoadingFeed = (!feedName && isInitiallyLoadingFeeds) || !isPolicyLoaded || isLoadingOnyxValue(lastSelectedFeedMetadata); - const isLoadingCards = cardFeedType === 'directFeed' ? selectedFeed?.accountList === undefined : isLoadingOnyxValue(cardListMetadata) || cardList === undefined; + const isLoadingCards = Object.keys(cardNamesToEncryptedCardNumberMapping ?? {}).length === 0 ? isLoadingOnyxValue(cardListMetadata) : false; const isLoadingPage = !isOffline && (isLoadingFeed || isLoadingOnyxValue(personalDetailsMetadata)); const showCards = !isInitiallyLoadingFeeds && !isFeedPending && !isNoFeed && !isLoadingFeed; @@ -115,9 +113,7 @@ function WorkspaceCompanyCardsTable({policyID, isPolicyLoaded, domainOrWorkspace const cardsData: WorkspaceCompanyCardTableItemData[] = isLoadingCards ? [] - : (cardNames?.map((cardName) => { - // For direct feeds cardID equals cardName, for commercial feeds it's looked up from cardList - const encryptedCardNumber = isDirectCardFeed ? cardName : (cardList?.[cardName] ?? ''); + : (Object.entries(cardNamesToEncryptedCardNumberMapping ?? {}).map(([cardName, encryptedCardNumber]) => { const failedCompanyCardAssignment = failedCompanyCardAssignments?.[encryptedCardNumber]; if (failedCompanyCardAssignment) { @@ -251,7 +247,7 @@ function WorkspaceCompanyCardsTable({policyID, isPolicyLoaded, domainOrWorkspace item={item} policyID={policyID ?? String(CONST.DEFAULT_NUMBER_ID)} CardFeedIcon={cardFeedIcon} - isPlaidCardFeed={isDirectCardFeed} + isPlaidCardFeed={isPlaidCardFeed} onAssignCard={onAssignCard} isAssigningCardDisabled={isAssigningCardDisabled} shouldUseNarrowTableLayout={shouldUseNarrowTableLayout} diff --git a/tests/unit/hooks/useCompanyCards.test.ts b/tests/unit/hooks/useCompanyCards.test.ts index bcb619bef221..d4e210ae9bef 100644 --- a/tests/unit/hooks/useCompanyCards.test.ts +++ b/tests/unit/hooks/useCompanyCards.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import {renderHook} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; import useCardFeeds from '@hooks/useCardFeeds'; @@ -105,85 +106,93 @@ describe('useCompanyCards', () => { await Onyx.clear(); }); - describe('cardFeedType determination', () => { - it('should return cardFeedType as customFeed for VCF feeds without accountList', async () => { + describe('cardNamesToEncryptedCardNumberMapping derivation', () => { + it('should derive cardNamesToEncryptedCardNumberMapping from cardList for custom feeds', async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockCustomFeed); (useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]); (useCardsList as jest.Mock).mockReturnValue([mockCardsList, {status: 'loaded'}]); const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - expect(result.current.cardFeedType).toBe('customFeed'); + // For custom feeds without accountList, cardNamesToEncryptedCardNumberMapping comes from cardList + expect(result.current.cardNamesToEncryptedCardNumberMapping).toEqual({ + card1: 'card1', + card2: 'card2', + }); expect(result.current.feedName).toBe(mockCustomFeed); }); - it('should return cardFeedType as directFeed for OAuth feeds with accountList', async () => { + it('should derive cardNamesToEncryptedCardNumberMapping from accountList for OAuth feeds', async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockOAuthFeed); (useCardFeeds as jest.Mock).mockReturnValue([mockOAuthFeedData, {status: 'loaded'}, undefined]); (useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]); const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - expect(result.current.cardFeedType).toBe('directFeed'); + // For OAuth feeds with accountList, card names map to themselves + expect(result.current.cardNamesToEncryptedCardNumberMapping).toEqual({ + 'CREDIT CARD...6607': 'CREDIT CARD...6607', + 'CREDIT CARD...5501': 'CREDIT CARD...5501', + }); expect(result.current.feedName).toBe(mockOAuthFeed); expect(result.current.selectedFeed?.accountList).toEqual(['CREDIT CARD...6607', 'CREDIT CARD...5501']); }); - it('should return cardFeedType as directFeed for Plaid feeds', async () => { + it('should derive cardNamesToEncryptedCardNumberMapping from accountList for Plaid feeds', async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockPlaidFeed); (useCardFeeds as jest.Mock).mockReturnValue([mockPlaidFeedData, {status: 'loaded'}, undefined]); (useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]); const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - expect(result.current.cardFeedType).toBe('directFeed'); + // For Plaid feeds with accountList, card names map to themselves + expect(result.current.cardNamesToEncryptedCardNumberMapping).toEqual({ + 'Plaid Checking 0000': 'Plaid Checking 0000', + 'Plaid Credit Card 3333': 'Plaid Credit Card 3333', + }); expect(result.current.feedName).toBe(mockPlaidFeed); }); - }); - describe('cardNames derivation', () => { - it('should derive cardNames from cardList for custom feeds', async () => { + it('should return empty cardNamesToEncryptedCardNumberMapping when no cardList or accountList', async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockCustomFeed); (useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]); - (useCardsList as jest.Mock).mockReturnValue([mockCardsList, {status: 'loaded'}]); - - const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - - expect(result.current.cardFeedType).toBe('customFeed'); - expect(result.current.cardNames).toEqual(['card1', 'card2']); - }); - - it('should derive cardNames from accountList for OAuth feeds (direct feeds)', async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockOAuthFeed); - (useCardFeeds as jest.Mock).mockReturnValue([mockOAuthFeedData, {status: 'loaded'}, undefined]); (useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]); const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - expect(result.current.cardFeedType).toBe('directFeed'); - expect(result.current.cardNames).toEqual(['CREDIT CARD...6607', 'CREDIT CARD...5501']); + expect(result.current.cardNamesToEncryptedCardNumberMapping).toEqual({}); }); - it('should derive cardNames from accountList for Plaid feeds (direct feeds)', async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockPlaidFeed); - (useCardFeeds as jest.Mock).mockReturnValue([mockPlaidFeedData, {status: 'loaded'}, undefined]); - (useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]); - - const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - - expect(result.current.cardFeedType).toBe('directFeed'); - expect(result.current.cardNames).toEqual(['Plaid Checking 0000', 'Plaid Credit Card 3333']); - }); + it('should merge accountList and cardList entries, with cardList taking precedence', async () => { + // Create a feed that has both accountList AND cardList entries + const feedWithBoth: CompanyCardFeedWithDomainID = `${CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE}#${domainID}` as CompanyCardFeedWithDomainID; + const feedDataWithAccountList = { + [feedWithBoth]: { + ...mockOAuthFeedData[mockOAuthFeed], + accountList: ['CARD A', 'CARD B'], + }, + }; + const cardsListWithEncrypted = { + cardList: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'CARD A': 'encrypted_A', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'CARD C': 'encrypted_C', + }, + }; - it('should return empty cardNames when cardList is undefined for custom feeds', async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockCustomFeed); - (useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]); - (useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]); + await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, feedWithBoth); + (useCardFeeds as jest.Mock).mockReturnValue([feedDataWithAccountList, {status: 'loaded'}, undefined]); + (useCardsList as jest.Mock).mockReturnValue([cardsListWithEncrypted, {status: 'loaded'}]); const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - expect(result.current.cardFeedType).toBe('customFeed'); - expect(result.current.cardNames).toEqual([]); + // accountList entries map to themselves, but cardList entries override with encrypted values + expect(result.current.cardNamesToEncryptedCardNumberMapping).toEqual({ + 'CARD A': 'encrypted_A', // cardList overrides accountList + 'CARD B': 'CARD B', // from accountList only + 'CARD C': 'encrypted_C', // from cardList only + }); }); }); @@ -195,7 +204,7 @@ describe('useCompanyCards', () => { const {result} = renderHook(() => useCompanyCards({policyID: undefined})); expect(result.current.feedName).toBeUndefined(); - expect(result.current.cardFeedType).toBeUndefined(); + expect(result.current.cardNamesToEncryptedCardNumberMapping).toBeUndefined(); expect(result.current.onyxMetadata).toBeDefined(); }); }); @@ -212,7 +221,11 @@ describe('useCompanyCards', () => { // Should use provided feedName, not lastSelectedFeed expect(result.current.feedName).toBe(mockOAuthFeed); - expect(result.current.cardFeedType).toBe('directFeed'); + // OAuth feed has accountList, so cardNamesToEncryptedCardNumberMapping should be populated + expect(result.current.cardNamesToEncryptedCardNumberMapping).toEqual({ + 'CREDIT CARD...6607': 'CREDIT CARD...6607', + 'CREDIT CARD...5501': 'CREDIT CARD...5501', + }); }); }); @@ -253,20 +266,23 @@ describe('useCompanyCards', () => { // For commercial feeds, cardList contains {cardName: encryptedNumber} expect(result.current.cardList).toEqual(mockCardsListWithEncryptedNumbers.cardList); - // cardNames should be the keys (display names) - expect(result.current.cardNames).toEqual(['490901XXXXXX1234', '490901XXXXXX5678']); + // cardNamesToEncryptedCardNumberMapping should map display names to encrypted values + expect(result.current.cardNamesToEncryptedCardNumberMapping).toEqual({ + '490901XXXXXX1234': 'v12:74E3CA3C4C0FA02F4C754FEN4RYP3ED1', + '490901XXXXXX5678': 'v12:74E3CA3C4C0FA02F4C754FEN4RYP3ED2', + }); }); - it('should have cardList where keys differ from values for commercial feeds', async () => { + it('should have cardNamesToEncryptedCardNumberMapping where keys differ from values for commercial feeds', async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockCustomFeed); (useCardFeeds as jest.Mock).mockReturnValue([mockCustomFeedData, {status: 'loaded'}, undefined]); (useCardsList as jest.Mock).mockReturnValue([mockCardsListWithEncryptedNumbers, {status: 'loaded'}]); const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - const cardList = result.current.cardList ?? {}; - const cardNames = Object.keys(cardList); - const encryptedNumbers = Object.values(cardList); + const cardNamesMap = result.current.cardNamesToEncryptedCardNumberMapping ?? {}; + const cardNames = Object.keys(cardNamesMap); + const encryptedNumbers = Object.values(cardNamesMap); // In commercial feeds, the display name (key) should differ from encrypted value (value) for (const [index, name] of cardNames.entries()) { @@ -274,40 +290,43 @@ describe('useCompanyCards', () => { } }); - it('should not have cardList for direct feeds (accountList is used instead)', async () => { + it('should populate cardNamesToEncryptedCardNumberMapping from accountList for direct feeds', async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockPlaidFeed); (useCardFeeds as jest.Mock).mockReturnValue([mockPlaidFeedData, {status: 'loaded'}, undefined]); (useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]); const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - // Direct feeds use accountList, not cardList + // Direct feeds use accountList, cardList should be undefined expect(result.current.cardList).toBeUndefined(); expect(result.current.selectedFeed?.accountList).toBeDefined(); - expect(result.current.cardNames).toEqual(['Plaid Checking 0000', 'Plaid Credit Card 3333']); + // cardNamesToEncryptedCardNumberMapping maps card names to themselves for direct feeds + expect(result.current.cardNamesToEncryptedCardNumberMapping).toEqual({ + 'Plaid Checking 0000': 'Plaid Checking 0000', + 'Plaid Credit Card 3333': 'Plaid Credit Card 3333', + }); }); }); describe('card ID consistency', () => { - it('should ensure direct feed cardNames can be used as identifiers', async () => { + it('should ensure direct feed cardNamesToEncryptedCardNumberMapping maps names to themselves', async () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicyID}`, mockOAuthFeed); (useCardFeeds as jest.Mock).mockReturnValue([mockOAuthFeedData, {status: 'loaded'}, undefined]); (useCardsList as jest.Mock).mockReturnValue([undefined, {status: 'loaded'}]); const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - // For direct feeds, cardNames ARE the identifiers (no encryption) - const cardNames = result.current.cardNames ?? []; - expect(cardNames).toEqual(['CREDIT CARD...6607', 'CREDIT CARD...5501']); + // For direct feeds, card names map to themselves (no encryption) + const cardNamesMap = result.current.cardNamesToEncryptedCardNumberMapping ?? {}; + expect(Object.keys(cardNamesMap)).toEqual(['CREDIT CARD...6607', 'CREDIT CARD...5501']); - // Each card name is both the display value AND the identifier - for (const name of cardNames) { - expect(typeof name).toBe('string'); - expect(name.length).toBeGreaterThan(0); + // Each card name maps to itself + for (const [name, encrypted] of Object.entries(cardNamesMap)) { + expect(name).toBe(encrypted); } }); - it('should ensure commercial feed cardList maps display names to encrypted identifiers', async () => { + it('should ensure commercial feed cardNamesToEncryptedCardNumberMapping maps display names to encrypted identifiers', async () => { const commercialCardsList = { cardList: { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -323,16 +342,16 @@ describe('useCompanyCards', () => { const {result} = renderHook(() => useCompanyCards({policyID: mockPolicyID})); - const cardList = result.current.cardList ?? {}; + const cardNamesMap = result.current.cardNamesToEncryptedCardNumberMapping ?? {}; // Display names are keys - expect(Object.keys(cardList)).toEqual(['VISA - 1234', 'VISA - 5678']); + expect(Object.keys(cardNamesMap)).toEqual(['VISA - 1234', 'VISA - 5678']); // Encrypted identifiers are values - expect(Object.values(cardList)).toEqual(['enc_abc123', 'enc_def456']); + expect(Object.values(cardNamesMap)).toEqual(['enc_abc123', 'enc_def456']); // Lookup: given a display name, get the encrypted identifier - expect(cardList['VISA - 1234']).toBe('enc_abc123'); + expect(cardNamesMap['VISA - 1234']).toBe('enc_abc123'); }); }); });