diff --git a/__mocks__/@react-native-community/push-notification-ios.js b/__mocks__/@react-native-community/push-notification-ios.js
new file mode 100644
index 000000000000..0fe8354b9e08
--- /dev/null
+++ b/__mocks__/@react-native-community/push-notification-ios.js
@@ -0,0 +1,5 @@
+export default {
+    addEventListener: jest.fn(),
+    requestPermissions: jest.fn(() => Promise.resolve()),
+    getInitialNotification: jest.fn(() => Promise.resolve()),
+};
diff --git a/__mocks__/pusher-js/react-native.js b/__mocks__/pusher-js/react-native.js
new file mode 100644
index 000000000000..0cb6afb4bb1d
--- /dev/null
+++ b/__mocks__/pusher-js/react-native.js
@@ -0,0 +1,3 @@
+import {PusherMock} from 'pusher-js-mock';
+
+export default PusherMock;
diff --git a/__mocks__/urbanairship-react-native.js b/__mocks__/urbanairship-react-native.js
index fe99d594f483..481cec4e40c9 100644
--- a/__mocks__/urbanairship-react-native.js
+++ b/__mocks__/urbanairship-react-native.js
@@ -1,3 +1,12 @@
 export default {
     setUserNotificationsEnabled: jest.fn(),
 };
+
+const EventType = {
+    NotificationResponse: 'notificationResponse',
+    PushReceived: 'pushReceived',
+};
+
+export {
+    EventType,
+};
diff --git a/package-lock.json b/package-lock.json
index 9d8163176eb5..853eb7123d25 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20725,6 +20725,12 @@
         "tweetnacl": "^1.0.3"
       }
     },
+    "pusher-js-mock": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/pusher-js-mock/-/pusher-js-mock-0.3.3.tgz",
+      "integrity": "sha512-Qn8u167Qm+pU+ZhE/vTLL7msh3Zk6C4im4fXC/Vxsjv2anXRYhhBwt+eqFVTfGvhb0ZcwPtL8w5dkwKAFSiMGQ==",
+      "dev": true
+    },
     "q": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -21032,8 +21038,8 @@
       }
     },
     "react-native-onyx": {
-      "version": "git+https://github.com/Expensify/react-native-onyx.git#783806e7a3d82d78025bb9025767f42c3964eff9",
-      "from": "git+https://github.com/Expensify/react-native-onyx.git#783806e7a3d82d78025bb9025767f42c3964eff9",
+      "version": "git+https://github.com/Expensify/react-native-onyx.git#bbed584f0e9f9ce128361c7138220738d8525a6a",
+      "from": "git+https://github.com/Expensify/react-native-onyx.git#bbed584f0e9f9ce128361c7138220738d8525a6a",
       "requires": {
         "@react-native-community/async-storage": "^1.12.1",
         "expensify-common": "git+https://github.com/Expensify/expensify-common.git#cd9f195ed1fd340e7e890c41672a97af4f2956ca",
diff --git a/package.json b/package.json
index a098aeb9a86e..b426b969e494 100644
--- a/package.json
+++ b/package.json
@@ -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#bbed584f0e9f9ce128361c7138220738d8525a6a",
     "react-native-pdf": "^6.2.2",
     "react-native-render-html": "^6.0.0-alpha.10",
     "react-native-safe-area-context": "^3.1.4",
@@ -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",
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index 6f576e8ef871..3baf0a210921 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -529,6 +529,7 @@ function addAction(reportID, text, file) {
         [newSequenceNumber]: {
             actionName: 'ADDCOMMENT',
             actorEmail: currentUserEmail,
+            actorAccountID: currentUserAccountID,
             person: [
                 {
                     style: 'strong',
@@ -553,6 +554,7 @@ function addAction(reportID, text, file) {
             isFirstItem: false,
             isAttachment,
             loading: true,
+            shouldShow: true,
         },
     });
 
diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js
new file mode 100644
index 000000000000..fbf7efe341e1
--- /dev/null
+++ b/tests/actions/ReportTest.js
@@ -0,0 +1,100 @@
+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',
+            message: [{type: 'COMMENT', html: 'Testing a comment', text: 'Testing a comment'}],
+            person: [{type: 'TEXT', style: 'strong', text: 'Test User'}],
+            sequenceNumber: ACTION_ID,
+            shouldShow: true,
+        };
+
+        // 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 so forcing them to
+        // return false will make the testing less complex.
+        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();
+            })
+            .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 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.message).toEqual(REPORT_ACTION.message);
+                expect(resultAction.person).toEqual(REPORT_ACTION.person);
+                expect(resultAction.loading).toEqual(true);
+            })
+            .then(() => {
+                // We subscribed to the Pusher channel above and now we need to simulate a reportComment action
+                // Pusher event so we can 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 a reportComment event is emitted to the Pusher channel we should see the comment get processed
+                // by the Pusher callback and added to the storage so we must wait for promises to resolve again and
+                // then verify the data is in Onyx.
+                return waitForPromisesToResolve();
+            })
+            .then(() => {
+                const resultAction = reportActions[ACTION_ID];
+
+                // Verify that our action is no longer in the loading state
+                expect(resultAction.loading).toEqual(false);
+            });
+    });
+});
diff --git a/tests/actions/SessionTest.js b/tests/actions/SessionTest.js
index 279731bf2b81..9f6d6ee5ad31 100644
--- a/tests/actions/SessionTest.js
+++ b/tests/actions/SessionTest.js
@@ -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', () => ({
@@ -23,15 +23,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,
@@ -44,39 +35,8 @@ test('Authenticate is called with saved credentials when a session expires', ()
         callback: val => session = val,
     });
 
-    // When the user enters their login and calls GetAccountStatus
-    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(() => {
-            // Then the login should exist in credentials
-            expect(credentials.login).toBe(TEST_USER_LOGIN);
-
-            // Note: Every time we add a mockImplementationOnce() we are altering the API response.
-            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,
-                }));
-
-            // When we sign in
-            signIn('Password1');
-            return waitForPromisesToResolve();
-        })
+    // When we sign in with the test user
+    return signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN, 'Password1', TEST_INITIAL_AUTH_TOKEN)
         .then(() => {
             // Then our re-authentication credentials should be generated and our session data
             // have the correct information + initial authToken.
diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js
new file mode 100644
index 000000000000..03fcf6757163
--- /dev/null
+++ b/tests/utils/TestHelper.js
@@ -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,
+};
