Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[No QA] Add Pusher Automated Tests #1153

Merged
merged 31 commits into from
Jan 18, 2021
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f0a9e60
change name
marcaaron Nov 6, 2020
1c37dd5
fix created being in the wrong order
marcaaron Nov 6, 2020
d4e9ef1
fix style
marcaaron Nov 10, 2020
29d02a1
fix style then get rid of async
marcaaron Nov 10, 2020
5970909
fix style
marcaaron Nov 10, 2020
c409ac3
Merge remote-tracking branch 'origin' into marcaaron-pusher-tests
marcaaron Nov 12, 2020
ccc3778
fix tests to use Onyx
marcaaron Nov 12, 2020
68f1d37
fix conflicts
marcaaron Dec 2, 2020
478f094
fix API.js
marcaaron Dec 2, 2020
918fa0a
tests broke somehow
marcaaron Dec 2, 2020
781f82b
fix conflicts
marcaaron Jan 1, 2021
da7746e
Fix this test up
marcaaron Jan 1, 2021
f65b202
remove some things
marcaaron Jan 1, 2021
fa53652
add comment
marcaaron Jan 1, 2021
6d59757
fix conflicts
marcaaron Jan 6, 2021
e8e0b3c
fix conflicts
marcaaron Jan 8, 2021
2f8dae6
fix ua mocks
marcaaron Jan 8, 2021
39684fe
remove uneeded
marcaaron Jan 8, 2021
9704aa9
Mock Pusher methods instead of adding the IS_JEST_RUNNING
marcaaron Jan 9, 2021
c6a235a
remove CONFIG dependency
marcaaron Jan 9, 2021
8e55d77
fix comment
marcaaron Jan 9, 2021
7298525
Merge remote-tracking branch 'origin' into marcaaron-pusher-tests
marcaaron Jan 12, 2021
cd18cdb
Fix up comment. Use Onyx.set instead of waitForPromisesToResolve()
marcaaron Jan 12, 2021
2ae40c9
Improve tests. Add TestHelper.
marcaaron Jan 12, 2021
fb69894
fix lint
marcaaron Jan 12, 2021
84bbfb6
Respond to Tims feedback.
marcaaron Jan 12, 2021
422ae85
update react-native-onyx
marcaaron Jan 13, 2021
739bbb7
dont test strict equal since the timestamp will make the test flaky
marcaaron Jan 13, 2021
fefa0ef
remove momnet
marcaaron Jan 13, 2021
b895dce
fix conflicts
marcaaron Jan 16, 2021
acb4b80
add comment
marcaaron Jan 16, 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
5 changes: 5 additions & 0 deletions __mocks__/@react-native-community/push-notification-ios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
addEventListener: jest.fn(),
requestPermissions: jest.fn(() => Promise.resolve()),
getInitialNotification: jest.fn(() => Promise.resolve()),
};
3 changes: 3 additions & 0 deletions __mocks__/pusher-js/react-native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {PusherMock} from 'pusher-js-mock';

export default PusherMock;
9 changes: 9 additions & 0 deletions __mocks__/urbanairship-react-native.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
export default {
setUserNotificationsEnabled: jest.fn(),
};

const EventType = {
NotificationResponse: 'notificationResponse',
PushReceived: 'pushReceived',
};

export {
EventType,
};
10 changes: 8 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"react-native-image-picker": "^2.3.3",
"react-native-keyboard-spacer": "^0.4.1",
"react-native-modal": "^11.5.6",
"react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#783806e7a3d82d78025bb9025767f42c3964eff9",
"react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#c769776165baa6ab8b57b122f49a12225a7bd19a",
"react-native-pdf": "^6.2.2",
"react-native-render-html": "^6.0.0-alpha.10",
"react-native-safe-area-context": "^3.1.4",
Expand Down Expand Up @@ -111,6 +111,7 @@
"jest-circus": "^26.5.2",
"jest-cli": "^26.5.2",
"metro-react-native-babel-preset": "^0.61.0",
"pusher-js-mock": "^0.3.3",
"react-hot-loader": "^4.12.21",
"react-native-svg-transformer": "^0.14.3",
"react-native-version": "^4.0.0",
Expand Down
2 changes: 2 additions & 0 deletions src/libs/actions/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ function addAction(reportID, text, file) {
[newSequenceNumber]: {
actionName: 'ADDCOMMENT',
actorEmail: currentUserEmail,
actorAccountID: currentUserAccountID,
person: [
{
style: 'strong',
Expand All @@ -558,6 +559,7 @@ function addAction(reportID, text, file) {
isFirstItem: false,
isAttachment,
loading: true,
shouldShow: true,
},
});

Expand Down
102 changes: 102 additions & 0 deletions tests/actions/ReportTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import moment from 'moment';
import Onyx from 'react-native-onyx';
import ONYXKEYS from '../../src/ONYXKEYS';
import * as Pusher from '../../src/libs/Pusher/pusher';
import PusherConnectionManager from '../../src/libs/PusherConnectionManager';
import CONFIG from '../../src/CONFIG';
import {addAction, subscribeToReportCommentEvents} from '../../src/libs/actions/Report';
import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
import PushNotification from '../../src/libs/Notification/PushNotification';
import {signInWithTestUser, fetchPersonalDetailsForTestUser} from '../utils/TestHelper';

PushNotification.register = () => {};
PushNotification.deregister = () => {};

describe('actions/Report', () => {
it('should store a new report action in Onyx when one is handled via Pusher', () => {
const TEST_USER_ACCOUNT_ID = 1;
const TEST_USER_LOGIN = 'test@test.com';
const REPORT_ID = 1;
const ACTION_ID = 1;
const REPORT_ACTION = {
actionName: 'ADDCOMMENT',
actorAccountID: TEST_USER_ACCOUNT_ID,
actorEmail: TEST_USER_LOGIN,
automatic: false,
avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
isAttachment: false,
isFirstItem: false,
loading: true,
message: [{type: 'COMMENT', html: 'Testing a comment', text: 'Testing a comment'}],
person: [{type: 'TEXT', style: 'strong', text: 'Test User'}],
sequenceNumber: ACTION_ID,
shouldShow: true,
timestamp: moment().unix(),
};

// When using the Pusher mock the act of calling Pusher.isSubscribed will create a
// channel already in a subscribed state. These methods are normally used to prevent
// duplicated subscriptions, but we don't need them for this test.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// duplicated subscriptions, but we don't need them for this test.
// duplicated subscriptions, but we don't need them for this test so forcing them to return false will make the testing less complex.

This adds a little more clarity.

Pusher.isSubscribed = jest.fn().mockReturnValue(false);
Pusher.isAlreadySubscribing = jest.fn().mockReturnValue(false);

// Connect to Pusher
PusherConnectionManager.init();
Pusher.init({
appKey: CONFIG.PUSHER.APP_KEY,
cluster: CONFIG.PUSHER.CLUSTER,
authEndpoint: `${CONFIG.EXPENSIFY.URL_API_ROOT}api?command=Push_Authenticate`,
});

let reportActions;
Onyx.connect({
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`,
callback: val => reportActions = val,
});

// Set up Onyx with some test user data
return signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN)
.then(() => {
subscribeToReportCommentEvents();
return waitForPromisesToResolve();
})
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
.then(() => fetchPersonalDetailsForTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, {
[TEST_USER_LOGIN]: {
accountID: TEST_USER_ACCOUNT_ID,
email: TEST_USER_LOGIN,
firstName: 'Test',
lastName: 'User',
},
}))
.then(() => {
// This is a fire and forget response, but one it completes we should be able to verify that we
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// This is a fire and forget response, but one it completes we should be able to verify that we
// This is a fire and forget response, but once it completes we should be able to verify that we

// have an "optimistic" report action in Onyx.
addAction(REPORT_ID, 'Testing a comment');
return waitForPromisesToResolve();
})
.then(() => {
const resultAction = reportActions[ACTION_ID];
expect(resultAction).toEqual(REPORT_ACTION);
Copy link
Contributor

Choose a reason for hiding this comment

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

Adding this assertion will help identify that yes, this is definitely an optimistic comment, and not the final result.

expect(resultAction.loading).toEqual(true);

This, of course, is already implied in the current assertion, but it's an extra mental jump that has to be made which isn't totally obvious.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep that makes sense to me. I think it'll be a more interesting test when the clientID stuff comes along. But this is mostly what we are looking for now and proves that the action was handled from the Pusher payload.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, this will look cool once the clientID stuff happens, and it will be a perfect use for these tests!

})
.then(() => {
// Now that we are subscribed we need to simulate a reportComment action Pusher event.
Copy link
Contributor

Choose a reason for hiding this comment

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

// Now that we are subscribed

Subscribed to what? Is this referring to the original Pusher subscription or to Onyx? This reads a little weird because I would expect the thing before this to be code that does subscribing, but the thing before this is adding the report action.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can make this more explicit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It probably made more sense before when the thing immediately before this was subscribeToReportComments()

// Then verify that action was handled correctly and merged into the reportActions.
const channel = Pusher.getChannel('private-user-accountID-1');
channel.emit('reportComment', {
reportID: REPORT_ID,
reportAction: REPORT_ACTION,
});

// Once this happens we should see the comment get processed by the callback and added to the
Copy link
Contributor

Choose a reason for hiding this comment

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

// Once this happens

"this" could be referring to a number of things, so be more explicit

Copy link
Contributor

Choose a reason for hiding this comment

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

the callback

This is also ambiguous. Is it the pusher callback, the onyx callback, or a different callback entirely?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can make this more explicit.

// storage so we must wait for promises to resolve again and then verify the data is in Onyx.
return waitForPromisesToResolve();
Copy link
Contributor

Choose a reason for hiding this comment

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

I can see how maybe this is necessary to prevent any race conditions with the local data stored in reportActions not being up-to-date with Onyx yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right. The callback should happen in realtime and then we set the data async via Onyx.

})
.then(() => {
const resultAction = reportActions[ACTION_ID];

// Verify that our action is no longer in the loading state
REPORT_ACTION.loading = false;
expect(resultAction).toEqual(REPORT_ACTION);
Copy link
Contributor

Choose a reason for hiding this comment

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

Using a different assertion might make it a little more clear what's being tested and prevents the need to modify the contents of REPORT_ACTION (which is not quite intuitive as to why it's necessary to modify REPORT_ACTION).

Suggestion:

expect(resultAction.loading).toEqual(false);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah this is much better

});
});
});
42 changes: 2 additions & 40 deletions tests/actions/SessionTest.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Onyx from 'react-native-onyx';
import {fetchAccountDetails, signIn} from '../../src/libs/actions/Session';
import * as API from '../../src/libs/API';
import HttpUtils from '../../src/libs/HttpUtils';
import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
import ONYXKEYS from '../../src/ONYXKEYS';
import {signInWithTestUser} from '../utils/TestHelper';

// 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,15 +22,6 @@ test('Authenticate is called with saved credentials when a session expires', ()
const TEST_INITIAL_AUTH_TOKEN = 'initialAuthToken';
const TEST_REFRESHED_AUTH_TOKEN = 'refreshedAuthToken';

// Set up mock responses for all APIs that will be called. The next time this command is called it will return
// jsonCode: 200 and the response here.
HttpUtils.xhr.mockImplementation(() => Promise.resolve({
jsonCode: 200,
accountExists: true,
canAccessExpensifyCash: true,
requiresTwoFactorAuth: false,
}));

let credentials;
Onyx.connect({
key: ONYXKEYS.CREDENTIALS,
Expand All @@ -43,36 +34,7 @@ test('Authenticate is called with saved credentials when a session expires', ()
callback: val => session = val,
});

// Simulate user entering their login and populating the credentials.login
fetchAccountDetails(TEST_USER_LOGIN);

// Note: In order for this test to work we must return a promise! It will pass even with
// failing assertions if we remove the return keyword.
return waitForPromisesToResolve()
.then(() => {
// Next we will simulate signing in and make sure all API calls in this flow succeed. Every time we add
// a mockImplementationOnce() we are altering what Network.post() will return.
HttpUtils.xhr

// First call to Authenticate
.mockImplementationOnce(() => Promise.resolve({
jsonCode: 200,
accountID: TEST_USER_ACCOUNT_ID,
authToken: TEST_INITIAL_AUTH_TOKEN,
email: TEST_USER_LOGIN,
}))

// Next call to CreateLogin
.mockImplementationOnce(() => Promise.resolve({
jsonCode: 200,
accountID: TEST_USER_ACCOUNT_ID,
authToken: TEST_INITIAL_AUTH_TOKEN,
email: TEST_USER_LOGIN,
}));

signIn('Password1');
return waitForPromisesToResolve();
})
return signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, 'Password1', TEST_INITIAL_AUTH_TOKEN)
.then(() => {
// Verify that our credentials were saved and that our session data is correct
expect(credentials.login).toBe(TEST_USER_LOGIN);
Expand Down
82 changes: 82 additions & 0 deletions tests/utils/TestHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {signIn, fetchAccountDetails} from '../../src/libs/actions/Session';
import {fetch as fetchPersonalDetails} from '../../src/libs/actions/PersonalDetails';
import HttpUtils from '../../src/libs/HttpUtils';
import waitForPromisesToResolve from './waitForPromisesToResolve';

/**
* Simulate signing in and make sure all API calls in this flow succeed. Every time we add
* a mockImplementationOnce() we are altering what Network.post() will return.
*
* @param {Number} accountID
* @param {String} login
* @param {String} password
* @param {String} authToken
* @return {Promise}
*/
function signInWithTestUser(accountID, login, password = 'Password1', authToken = 'asdfqwerty') {
HttpUtils.xhr = jest.fn();
HttpUtils.xhr.mockImplementation(() => Promise.resolve({
jsonCode: 200,
accountExists: true,
canAccessExpensifyCash: true,
requiresTwoFactorAuth: false,
}));

// Simulate user entering their login and populating the credentials.login
fetchAccountDetails(login);
return waitForPromisesToResolve()
.then(() => {
// First call to Authenticate
HttpUtils.xhr
.mockImplementationOnce(() => Promise.resolve({
jsonCode: 200,
accountID,
authToken,
email: login,
}))

// Next call to CreateLogin
.mockImplementationOnce(() => Promise.resolve({
jsonCode: 200,
accountID,
authToken,
email: login,
}));
signIn(password);
return waitForPromisesToResolve();
});
}

/**
* Fetch and set personal details with provided personalDetailsList
*
* @param {Number} accountID
* @param {String} email
* @param {Object} personalDetailsList
* @returns {Promise}
*/
function fetchPersonalDetailsForTestUser(accountID, email, personalDetailsList) {
// Mock xhr()
HttpUtils.xhr = jest.fn();

// Get the personalDetails
HttpUtils.xhr

// fetchPersonalDetails
.mockImplementationOnce(() => Promise.resolve({
accountID,
email,
personalDetailsList,
}))

// fetchTimezone
.mockImplementationOnce(() => Promise.resolve({}));

fetchPersonalDetails();
return waitForPromisesToResolve();
}

export {
signInWithTestUser,
fetchPersonalDetailsForTestUser,
};