Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bb59f93
retry a request if auth+crendentials are not ready
marcochavezf Nov 2, 2021
bb69c23
disable circular dep between Log-Api-Network
marcochavezf Nov 2, 2021
8552b0a
- add Onyx initialization
marcochavezf Nov 2, 2021
79bcfb6
add unit test to check if auth+cred are not ready
marcochavezf Nov 2, 2021
2bbddeb
add documentation to isReady
marcochavezf Nov 2, 2021
9c2ff14
stop logging the user out in enhanceParameters
marcochavezf Nov 2, 2021
56b0bb8
Update tests/unit/StoreNetworkTest.js
marcochavezf Nov 3, 2021
ab15fa6
Update src/libs/Network.js
marcochavezf Nov 3, 2021
acfc39a
Update tests/actions/SessionTest.js
marcochavezf Nov 3, 2021
d0f9acc
Update tests/actions/ReportTest.js
marcochavezf Nov 3, 2021
480b81c
add suggestions to API.js and StoreNetworkTest.js
marcochavezf Nov 3, 2021
b4b694e
remove code to pause and clear queue in
marcochavezf Nov 3, 2021
78da9e4
add delay to AsyncStorage.getItem()
marcochavezf Nov 4, 2021
3713f70
remove comment in canMakeRequest
marcochavezf Nov 4, 2021
9ac5627
check if the network request is retried in test
marcochavezf Nov 4, 2021
a67d885
restore mock async-storage to its original verison
marcochavezf Nov 4, 2021
60df933
add Onyx mock to add delay in connect callbacks
marcochavezf Nov 4, 2021
0b2f305
fix jsdoc param for GetAccountStatus
marcochavezf Nov 4, 2021
1e0a163
fix test to retry request if Network is not ready
marcochavezf Nov 4, 2021
ec2ee08
add missing optional param in jsdoc
marcochavezf Nov 4, 2021
62ef020
add waitForPromisesToResolve in test
marcochavezf Nov 4, 2021
1226f64
Revert "add waitForPromisesToResolve in test"
marcochavezf Nov 4, 2021
623c8a3
fix callback params in mock Onyx
marcochavezf Nov 4, 2021
ea99e0b
change runOnlyPendingTimers to advanceTimersByTime
marcochavezf Nov 4, 2021
63d35a8
add PROCESS_REQUEST_DELAY_MS to Network
marcochavezf Nov 4, 2021
b8a580b
update comment in test that retrires network req
marcochavezf Nov 5, 2021
1f23139
Revert "add missing optional param in jsdoc"
marcochavezf Nov 4, 2021
61f2df0
Merge remote-tracking branch 'refs/remotes/origin/main'
marcochavezf Nov 8, 2021
c6fb00f
remove duplicated lodashGet
marcochavezf Nov 8, 2021
7f22cab
disable prefer-onyx-connect-in-libs in Onyx mock
marcochavezf Nov 8, 2021
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
26 changes: 26 additions & 0 deletions __mocks__/react-native-onyx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* eslint-disable rulesdir/prefer-onyx-connect-in-libs */
import Onyx, {withOnyx} from 'react-native-onyx';

let connectCallbackDelay = 0;
function addDelayToConnectCallback(delay) {
connectCallbackDelay = delay;
}

export default {
...Onyx,
connect: mapping => Onyx.connect({
...mapping,
callback: (...params) => {
if (connectCallbackDelay > 0) {
setTimeout(() => {
mapping.callback(...params);
}, connectCallbackDelay);
} else {
mapping.callback(...params);
}
},
}),
addDelayToConnectCallback,
};
export {withOnyx};
Comment thread
marcaaron marked this conversation as resolved.
/* eslint-enable rulesdir/prefer-onyx-connect-in-libs */
43 changes: 23 additions & 20 deletions src/libs/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,40 @@ import CONFIG from '../CONFIG';
import ONYXKEYS from '../ONYXKEYS';
// eslint-disable-next-line import/no-cycle
import redirectToSignIn from './actions/SignInRedirect';
import * as Network from './Network';
import isViaExpensifyCashNative from './isViaExpensifyCashNative';

// eslint-disable-next-line import/no-cycle
import * as Network from './Network';
// eslint-disable-next-line import/no-cycle
import LogUtil from './Log';
// eslint-disable-next-line import/no-cycle
import * as Session from './actions/Session';

let isAuthenticating;
let credentials;
let authToken;

function checkRequiredDataAndSetNetworkReady() {
if (_.isUndefined(authToken) || _.isUndefined(credentials)) {
return;
}

Network.setIsReady(true);
}

Onyx.connect({
key: ONYXKEYS.CREDENTIALS,
callback: val => credentials = val,
callback: (val) => {
credentials = val || null;
checkRequiredDataAndSetNetworkReady();
},
});

let authToken;
Onyx.connect({
key: ONYXKEYS.SESSION,
callback: val => authToken = val ? val.authToken : null,
callback: (val) => {
authToken = lodashGet(val, 'authToken', null);
checkRequiredDataAndSetNetworkReady();
},
});

/**
Expand Down Expand Up @@ -59,18 +74,6 @@ function addDefaultValuesToParameters(command, parameters) {
const finalParameters = {...parameters};

if (isAuthTokenRequired(command) && !parameters.authToken) {
// If we end up here with no authToken it means we are trying to make an API request before we are signed in.
// In this case, we should cancel the current request by pausing the queue and clearing the remaining requests.
if (!authToken) {
redirectToSignIn();
Comment thread
marcaaron marked this conversation as resolved.

LogUtil.info('A request was made without an authToken', false, {command, parameters});
Network.pauseRequestQueue();
Network.clearRequestQueue();
Network.unpauseRequestQueue();
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nickmurray47 comment makes me wonder if we should add a test for this. But maybe it is already covered.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the manual test would just be...

  • Mess up the authToken and make sure you are reauthenticated
  • Mess up the credentials and authToken and make sure you are logged out

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like adding both of those!


finalParameters.authToken = authToken;
}

Expand Down Expand Up @@ -197,7 +200,7 @@ Network.registerErrorHandler((queuedRequest, error) => {

/**
* @param {Object} parameters
* @param {String} [parameters.useExpensifyLogin]
* @param {Boolean} [parameters.useExpensifyLogin]
* @param {String} parameters.partnerName
* @param {String} parameters.partnerPassword
* @param {String} parameters.partnerUserID
Expand Down Expand Up @@ -448,6 +451,7 @@ function Get(parameters, shouldUseSecure = false) {
/**
* @param {Object} parameters
* @param {String} parameters.email
* @param {Boolean} parameters.forceNetworkRequest
* @returns {Promise}
*/
function GetAccountStatus(parameters) {
Expand Down Expand Up @@ -1015,14 +1019,13 @@ function DeleteBankAccount(parameters) {

/**
* @param {Object} parameters
* @param {String[]} data
* @returns {Promise}
*/
function Mobile_GetConstants(parameters) {
const commandName = 'Mobile_GetConstants';
requireParameters(['data'], parameters, commandName);

// Stringinfy the parameters object as we cannot send an object via FormData
// Stringify the parameters object as we cannot send an object via FormData
const finalParameters = parameters;
finalParameters.data = JSON.stringify(parameters.data);

Expand Down
30 changes: 22 additions & 8 deletions src/libs/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import HttpUtils from './HttpUtils';
import ONYXKEYS from '../ONYXKEYS';
import * as ActiveClientManager from './ActiveClientManager';
import CONST from '../CONST';
// eslint-disable-next-line import/no-cycle
import LogUtil from './Log';
import * as NetworkRequestQueue from './actions/NetworkRequestQueue';

let isReady = false;
let isQueuePaused = false;

// Queue for network requests so we don't lose actions done by the user while offline
Expand All @@ -25,6 +28,8 @@ let onError = () => {};
let didLoadPersistedRequests;
let isOffline;

const PROCESS_REQUEST_DELAY_MS = 1000;

/**
* Process the offline NETWORK_REQUEST_QUEUE
* @param {Array<Object> | null} persistedRequests - Requests
Expand Down Expand Up @@ -96,15 +101,29 @@ Onyx.connect({
callback: val => email = val ? val.email : null,
});

/**
* @param {Boolean} val
*/
function setIsReady(val) {
isReady = val;
}

/**
* Checks to see if a request can be made.
*
* @param {Object} request
* @param {String} request.type
* @param {String} request.command
* @param {Object} request.data
* @param {Boolean} request.data.forceNetworkRequest
* @return {Boolean}
*/
function canMakeRequest(request) {
if (!isReady) {
LogUtil.hmmm('Trying to make a request when Network is not ready', {command: request.command, type: request.type});
Comment thread
marcaaron marked this conversation as resolved.
return false;
}

// These requests are always made even when the queue is paused
if (request.data.forceNetworkRequest === true) {
return true;
Expand Down Expand Up @@ -202,13 +221,6 @@ function processNetworkRequestQueue() {
? enhanceParameters(queuedRequest.command, requestData)
: requestData;

// Check to see if the queue has paused again. It's possible that a call to enhanceParameters()
// has paused the queue and if this is the case we must return. We don't retry these requests
// since if a request is made without an authToken we sign out the user.
if (!canMakeRequest(queuedRequest)) {
return;
}

HttpUtils.xhr(queuedRequest.command, finalParameters, queuedRequest.type, queuedRequest.shouldUseSecure)
.then(response => onResponse(queuedRequest, response))
.catch(error => onError(queuedRequest, error));
Expand All @@ -232,7 +244,7 @@ function processNetworkRequestQueue() {
}

// Process our write queue very often
setInterval(processNetworkRequestQueue, 1000);
setInterval(processNetworkRequestQueue, PROCESS_REQUEST_DELAY_MS);

/**
* @param {Object} request
Expand Down Expand Up @@ -331,9 +343,11 @@ function registerErrorHandler(callback) {
export {
post,
pauseRequestQueue,
PROCESS_REQUEST_DELAY_MS,
unpauseRequestQueue,
registerParameterEnhancer,
clearRequestQueue,
registerResponseHandler,
registerErrorHandler,
setIsReady,
};
13 changes: 12 additions & 1 deletion tests/actions/ReportTest.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import _ from 'underscore';
import Onyx from 'react-native-onyx';
import lodashGet from 'lodash/get';
import {
beforeEach, beforeAll, afterEach, jest, describe, it, expect,
} from '@jest/globals';
import ONYXKEYS from '../../src/ONYXKEYS';
import * as Pusher from '../../src/libs/Pusher/pusher';
import PusherConnectionManager from '../../src/libs/PusherConnectionManager';
Expand Down Expand Up @@ -30,8 +34,15 @@ describe('actions/Report', () => {
cluster: CONFIG.PUSHER.CLUSTER,
authEndpoint: `${CONFIG.EXPENSIFY.URL_API_ROOT}api?command=Push_Authenticate`,
});

Onyx.init({
keys: ONYXKEYS,
registerStorageEventListener: () => {},
});
});

beforeEach(() => Onyx.clear().then(waitForPromisesToResolve));

afterEach(() => {
// Unsubscribe from account channel after each test since we subscribe in the function
// subscribeToUserEvents and we don't want duplicate event subscriptions.
Expand Down Expand Up @@ -130,7 +141,7 @@ describe('actions/Report', () => {
let reportIsPinned;
Onyx.connect({
key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`,
callback: val => reportIsPinned = val.isPinned,
callback: val => reportIsPinned = lodashGet(val, 'isPinned'),
});

// Set up Onyx with some test user data
Expand Down
8 changes: 8 additions & 0 deletions tests/actions/SessionTest.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Onyx from 'react-native-onyx';
import {beforeEach, jest, test} from '@jest/globals';
import * as API from '../../src/libs/API';
import HttpUtils from '../../src/libs/HttpUtils';
import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
Expand All @@ -16,6 +17,13 @@ jest.mock('../../src/libs/Notification/PushNotification', () => ({
// We test HttpUtils.xhr() since this means that our API command turned into a network request and isn't only queued.
HttpUtils.xhr = jest.fn();

Onyx.init({
keys: ONYXKEYS,
registerStorageEventListener: () => {},
});

beforeEach(() => Onyx.clear().then(waitForPromisesToResolve));

test('Authenticate is called with saved credentials when a session expires', () => {
// Given a test user and set of authToken with subscriptions to session and credentials
const TEST_USER_LOGIN = 'test@testguy.com';
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/NetworkTest.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import _ from 'underscore';
import Onyx from 'react-native-onyx';

import {
beforeEach, jest, test, expect, afterEach,
} from '@jest/globals';
import * as API from '../../src/libs/API';
import {signInWithTestUser} from '../utils/TestHelper';
import HttpUtils from '../../src/libs/HttpUtils';
import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
import ONYXKEYS from '../../src/ONYXKEYS';
import * as Network from '../../src/libs/Network';
import {fetchAccountDetails} from '../../src/libs/actions/Session';

// Set up manual mocks for methods used in the actions so our test does not fail.
jest.mock('../../src/libs/Notification/PushNotification', () => ({
Expand All @@ -22,6 +28,12 @@ Onyx.init({

beforeEach(() => Onyx.clear().then(waitForPromisesToResolve));

afterEach(() => {
Network.setIsReady(false);
Onyx.addDelayToConnectCallback(0);
jest.clearAllMocks();
});

test('failing to reauthenticate while offline should not log out user', () => {
// Given a test user login and account ID
const TEST_USER_LOGIN = 'test@testguy.com';
Expand Down Expand Up @@ -209,3 +221,53 @@ test('consecutive API calls eventually succeed when authToken is expired', () =>
expect(chatList).toEqual(TEST_CHAT_LIST);
});
});

test('retry network request if auth and credentials are not read from Onyx yet', () => {
// For this test we're having difficulty creating a situation where Onyx.connect() has not yet run
// because some Onyx.connect callbacks are already registered in API.js (which happens before this
// unit test is setup), so in order to test an scenario where the auth token and credentials hasn't
// been read from storage we set Network.setIsReady(false) and trigger an update in the Onyx.connect
// callbacks registered in API.js merging an empty object.

// Given a test user login and account ID
const TEST_USER_LOGIN = 'test@testguy.com';

// Given a delay to the Onyx.connect callbacks
const ONYX_DELAY_MS = 3000;
Onyx.addDelayToConnectCallback(ONYX_DELAY_MS);

// Given initial state to Network
Network.setIsReady(false);

// Given any initial value to trigger an update
Onyx.merge(ONYXKEYS.CREDENTIALS, {});
Onyx.merge(ONYXKEYS.SESSION, {});
Comment thread
marcaaron marked this conversation as resolved.

// Given some mock functions to track the isReady
// flag in Network and the http requests made
const spyNetworkSetIsReady = jest.spyOn(Network, 'setIsReady');
const spyHttpUtilsXhr = jest.spyOn(HttpUtils, 'xhr').mockImplementation(() => Promise.resolve({}));

// When we make an arbitrary request that can be retried
// And we wait for the Onyx.callbacks to be set
fetchAccountDetails(TEST_USER_LOGIN);
Comment thread
marcochavezf marked this conversation as resolved.
return waitForPromisesToResolve().then(() => {
// Then we expect not having the Network ready and not making an http request
expect(spyNetworkSetIsReady).not.toHaveBeenCalled();
expect(spyHttpUtilsXhr).not.toHaveBeenCalled();

// When we resolve Onyx.connect callbacks
jest.advanceTimersByTime(ONYX_DELAY_MS);

// Then we should expect call Network.setIsReady(true)
// And We should expect not making an http request yet
expect(spyNetworkSetIsReady).toHaveBeenLastCalledWith(true);
expect(spyHttpUtilsXhr).not.toHaveBeenCalled();

// When we run processNetworkRequestQueue in the setInterval of Network.js
jest.advanceTimersByTime(Network.PROCESS_REQUEST_DELAY_MS);

// Then we should expect a retry of the network request
expect(spyHttpUtilsXhr).toHaveBeenCalled();
});
});