diff --git a/src/libs/actions/connections/index.ts b/src/libs/actions/connections/index.ts index 99b30b127c900..1d2a1865d0442 100644 --- a/src/libs/actions/connections/index.ts +++ b/src/libs/actions/connections/index.ts @@ -280,6 +280,14 @@ function isConnectionUnverified(policy: OnyxEntry, connectionName: Polic return !(policy?.connections?.[connectionName]?.lastSync?.isConnected ?? true); } +/** + * Determines whether to use updateNetSuiteTokens (preserves config) or connectPolicyToNetSuite (full init) + * based on the connection's authentication and verification state. + */ +function shouldUseUpdateNetSuiteTokens(policy: OnyxEntry): boolean { + return isAuthenticationError(policy, CONST.POLICY.CONNECTIONS.NAME.NETSUITE) && !isConnectionUnverified(policy, CONST.POLICY.CONNECTIONS.NAME.NETSUITE); +} + function setConnectionError(policyID: string, connectionName: PolicyConnectionName, errorMessage?: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { connections: { @@ -359,4 +367,5 @@ export { isConnectionInProgress, hasSynchronizationErrorMessage, setConnectionError, + shouldUseUpdateNetSuiteTokens, }; diff --git a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/subPages/NetSuiteTokenInputForm.tsx b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/subPages/NetSuiteTokenInputForm.tsx index 671722aa583ed..286e34f50e055 100644 --- a/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/subPages/NetSuiteTokenInputForm.tsx +++ b/src/pages/workspace/accounting/netsuite/NetSuiteTokenInput/subPages/NetSuiteTokenInputForm.tsx @@ -10,7 +10,7 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isAuthenticationError, isConnectionUnverified} from '@libs/actions/connections'; +import {shouldUseUpdateNetSuiteTokens} from '@libs/actions/connections'; import {connectPolicyToNetSuite, updateNetSuiteTokens} from '@libs/actions/connections/NetSuiteCommands'; import {isMobileSafari} from '@libs/Browser'; import {addErrorMessage} from '@libs/ErrorUtils'; @@ -49,7 +49,7 @@ function NetSuiteTokenInputForm({onNext, policyID}: CustomSubPageTokenInputProps return; } - if (isAuthenticationError(policy, CONST.POLICY.CONNECTIONS.NAME.NETSUITE) && !isConnectionUnverified(policy, CONST.POLICY.CONNECTIONS.NAME.NETSUITE)) { + if (shouldUseUpdateNetSuiteTokens(policy)) { updateNetSuiteTokens(policyID, formValues); } else { connectPolicyToNetSuite(policyID, formValues); diff --git a/tests/actions/connections/NetSuite.ts b/tests/actions/connections/NetSuite.ts new file mode 100644 index 0000000000000..eec23cb6d1fe9 --- /dev/null +++ b/tests/actions/connections/NetSuite.ts @@ -0,0 +1,160 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {shouldUseUpdateNetSuiteTokens} from '@libs/actions/connections'; +import {connectPolicyToNetSuite, updateNetSuiteTokens} from '@libs/actions/connections/NetSuiteCommands'; +// eslint-disable-next-line no-restricted-syntax -- this is required to allow mocking +import * as API from '@libs/API'; +import type {WriteCommand} from '@libs/API/types'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy} from '@src/types/onyx'; +import type {AnyOnyxData} from '@src/types/onyx/Request'; +import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; + +jest.mock('@libs/API'); + +const writeSpy = jest.spyOn(API, 'write'); + +const MOCK_POLICY_ID = 'MOCK_POLICY_ID'; +const MOCK_CREDENTIALS = { + netSuiteAccountID: 'account-123', + netSuiteTokenID: 'token-123', + netSuiteTokenSecret: 'secret-123', +}; + +function getFirstWriteCall(): {command: WriteCommand; onyxData?: AnyOnyxData} { + const call = writeSpy.mock.calls.at(0); + if (!call) { + throw new Error('API.write was not called'); + } + const [command, , onyxData] = call; + return {command, onyxData}; +} + +function createPolicy(options: {isAuthError?: boolean; verified?: boolean}): OnyxEntry { + return { + id: MOCK_POLICY_ID, + connections: { + netsuite: { + verified: options.verified ?? false, + lastSync: { + isAuthenticationError: options.isAuthError ?? false, + }, + }, + }, + } as unknown as Policy; +} + +describe('actions/connections/NetSuite', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + return Onyx.clear().then(waitForBatchedUpdates); + }); + + describe('connectPolicyToNetSuite', () => { + it('writes the ConnectPolicyToNetSuite command', () => { + connectPolicyToNetSuite(MOCK_POLICY_ID, MOCK_CREDENTIALS); + + const {command} = getFirstWriteCall(); + expect(command).toBe(WRITE_COMMANDS.CONNECT_POLICY_TO_NETSUITE); + }); + + it('passes the policyID and credentials as parameters', () => { + connectPolicyToNetSuite(MOCK_POLICY_ID, MOCK_CREDENTIALS); + + const call = writeSpy.mock.calls.at(0); + expect(call).toBeDefined(); + const params = call?.[1]; + expect(params).toEqual({ + policyID: MOCK_POLICY_ID, + ...MOCK_CREDENTIALS, + }); + }); + + it('sets optimistic sync progress data', () => { + connectPolicyToNetSuite(MOCK_POLICY_ID, MOCK_CREDENTIALS); + + const {onyxData} = getFirstWriteCall(); + const optimisticUpdate = onyxData?.optimisticData?.at(0); + + expect(optimisticUpdate?.onyxMethod).toBe(Onyx.METHOD.MERGE); + expect(optimisticUpdate?.key).toBe(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${MOCK_POLICY_ID}`); + expect(optimisticUpdate?.value).toEqual( + expect.objectContaining({ + stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.NETSUITE_SYNC_CONNECTION, + connectionName: CONST.POLICY.CONNECTIONS.NAME.NETSUITE, + }), + ); + }); + }); + + describe('updateNetSuiteTokens', () => { + it('writes the UpdateNetSuiteTokens command', () => { + updateNetSuiteTokens(MOCK_POLICY_ID, MOCK_CREDENTIALS); + + const {command} = getFirstWriteCall(); + expect(command).toBe(WRITE_COMMANDS.UPDATE_NETSUITE_TOKENS); + }); + + it('passes the policyID and credentials as parameters', () => { + updateNetSuiteTokens(MOCK_POLICY_ID, MOCK_CREDENTIALS); + + const call = writeSpy.mock.calls.at(0); + expect(call).toBeDefined(); + const params = call?.[1]; + expect(params).toEqual({ + policyID: MOCK_POLICY_ID, + ...MOCK_CREDENTIALS, + }); + }); + + it('sets optimistic sync progress data', () => { + updateNetSuiteTokens(MOCK_POLICY_ID, MOCK_CREDENTIALS); + + const {onyxData} = getFirstWriteCall(); + const optimisticUpdate = onyxData?.optimisticData?.at(0); + + expect(optimisticUpdate?.onyxMethod).toBe(Onyx.METHOD.MERGE); + expect(optimisticUpdate?.key).toBe(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${MOCK_POLICY_ID}`); + expect(optimisticUpdate?.value).toEqual( + expect.objectContaining({ + stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.NETSUITE_SYNC_CONNECTION, + connectionName: CONST.POLICY.CONNECTIONS.NAME.NETSUITE, + }), + ); + }); + }); + + describe('shouldUseUpdateNetSuiteTokens', () => { + it('returns false for unverified connection with auth error (regression case)', () => { + const policy = createPolicy({isAuthError: true, verified: false}); + expect(shouldUseUpdateNetSuiteTokens(policy)).toBe(false); + }); + + it('returns true for verified connection with auth error', () => { + const policy = createPolicy({isAuthError: true, verified: true}); + expect(shouldUseUpdateNetSuiteTokens(policy)).toBe(true); + }); + + it('returns false when there is no auth error', () => { + const policy = createPolicy({isAuthError: false, verified: true}); + expect(shouldUseUpdateNetSuiteTokens(policy)).toBe(false); + }); + + it('returns false when policy is undefined', () => { + expect(shouldUseUpdateNetSuiteTokens(undefined)).toBe(false); + }); + + it('returns false for unverified connection without auth error', () => { + const policy = createPolicy({isAuthError: false, verified: false}); + expect(shouldUseUpdateNetSuiteTokens(policy)).toBe(false); + }); + }); +});