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: 9 additions & 0 deletions src/libs/actions/connections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,14 @@ function isConnectionUnverified(policy: OnyxEntry<Policy>, 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<Policy>): 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: {
Expand Down Expand Up @@ -359,4 +367,5 @@ export {
isConnectionInProgress,
hasSynchronizationErrorMessage,
setConnectionError,
shouldUseUpdateNetSuiteTokens,
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
160 changes: 160 additions & 0 deletions tests/actions/connections/NetSuite.ts
Original file line number Diff line number Diff line change
@@ -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<Policy> {
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);
});
});
});
Loading