From eb54a7f69df638bedd93df240b90a167306b7877 Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 11 May 2021 15:19:05 +0200 Subject: [PATCH 01/12] Delete Account: started adding unit tests --- __mocks__/gatsby-plugin-firebase.js | 8 ++- .../constants/functions.js | 15 ++++++ .../functions/functions.js | 54 +++++++++++++++++++ .../functions/httpsCallableResult.js | 19 +++++++ .../builder.settings.deleteAccount.test.js | 36 +++++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 __mocks__/gatsby-plugin-firebase/constants/functions.js create mode 100644 __mocks__/gatsby-plugin-firebase/functions/functions.js create mode 100644 __mocks__/gatsby-plugin-firebase/functions/httpsCallableResult.js create mode 100644 src/pages/app/__tests__/builder.settings.deleteAccount.test.js diff --git a/__mocks__/gatsby-plugin-firebase.js b/__mocks__/gatsby-plugin-firebase.js index 0e0b5955b..556b9917d 100644 --- a/__mocks__/gatsby-plugin-firebase.js +++ b/__mocks__/gatsby-plugin-firebase.js @@ -1,7 +1,9 @@ import Auth from './gatsby-plugin-firebase/auth/auth'; import Database from './gatsby-plugin-firebase/database/database'; +import Functions from './gatsby-plugin-firebase/functions/functions'; import AuthConstants from './gatsby-plugin-firebase/constants/auth'; import DatabaseConstants from './gatsby-plugin-firebase/constants/database'; +import FunctionsConstants from './gatsby-plugin-firebase/constants/functions'; class FirebaseStub { static auth() { @@ -11,6 +13,10 @@ class FirebaseStub { static database() { return Database.instance; } + + static functions() { + return Functions.instance; + } } FirebaseStub.database.ServerValue = {}; @@ -21,4 +27,4 @@ Object.defineProperty(FirebaseStub.database.ServerValue, 'TIMESTAMP', { }); export default FirebaseStub; -export { AuthConstants, DatabaseConstants }; +export { AuthConstants, DatabaseConstants, FunctionsConstants }; diff --git a/__mocks__/gatsby-plugin-firebase/constants/functions.js b/__mocks__/gatsby-plugin-firebase/constants/functions.js new file mode 100644 index 000000000..6ab0c54bd --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/constants/functions.js @@ -0,0 +1,15 @@ +const deleteUserFunctionName = 'deleteUser'; + +const defaultDelayInMilliseconds = 2000; + +class Functions { + static get deleteUserFunctionName() { + return deleteUserFunctionName; + } + + static get defaultDelayInMilliseconds() { + return defaultDelayInMilliseconds; + } +} + +export default Functions; diff --git a/__mocks__/gatsby-plugin-firebase/functions/functions.js b/__mocks__/gatsby-plugin-firebase/functions/functions.js new file mode 100644 index 000000000..3a4d6b1dd --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/functions/functions.js @@ -0,0 +1,54 @@ +/* eslint-disable no-underscore-dangle */ +import { v4 as uuidv4 } from 'uuid'; + +import FunctionsConstants from '../constants/functions'; +import HttpsCallableResult from './httpsCallableResult'; +import { delay } from '../../../src/utils/index'; + +const singleton = Symbol(''); +const singletonEnforcer = Symbol(''); + +const deleteUser = async () => { + await delay(FunctionsConstants.defaultDelayInMilliseconds); + + return new HttpsCallableResult(true); +}; + +class Functions { + constructor(enforcer) { + if (enforcer !== singletonEnforcer) { + throw new Error('Cannot construct singleton'); + } + + this._uuid = uuidv4(); + + this._httpsCallables = {}; + this._httpsCallables[ + FunctionsConstants.deleteUserFunctionName + ] = deleteUser; + } + + static get instance() { + if (!this[singleton]) { + this[singleton] = new Functions(singletonEnforcer); + } + + return this[singleton]; + } + + get uuid() { + return this._uuid; + } + + httpsCallable(name) { + if (!name) { + throw new Error('name must be provided.'); + } else if (typeof name !== 'string') { + throw new Error('name should be a string.'); + } + + return this._httpsCallables[name]; + } +} + +export default Functions; diff --git a/__mocks__/gatsby-plugin-firebase/functions/httpsCallableResult.js b/__mocks__/gatsby-plugin-firebase/functions/httpsCallableResult.js new file mode 100644 index 000000000..d35ba4eee --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/functions/httpsCallableResult.js @@ -0,0 +1,19 @@ +/* eslint-disable no-underscore-dangle */ +import { v4 as uuidv4 } from 'uuid'; + +class HttpsCallableResult { + constructor(data) { + this._uuid = uuidv4(); + this._data = data; + } + + get data() { + return this._data; + } + + get uuid() { + return this._uuid; + } +} + +export default HttpsCallableResult; diff --git a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js new file mode 100644 index 000000000..4720dcac6 --- /dev/null +++ b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js @@ -0,0 +1,36 @@ +import { fireEvent, screen } from '@testing-library/react'; + +import FirebaseStub, { DatabaseConstants } from 'gatsby-plugin-firebase'; + +import { setupAndWait } from './helpers/builder'; + +const testTimeoutInMilliseconds = 20000; +jest.setTimeout(testTimeoutInMilliseconds); + +async function setup() { + const resumeId = DatabaseConstants.demoStateResume1Id; + await setupAndWait(resumeId, true, true); + + const button = screen.getByRole('button', { + name: /Delete Account/i, + }); + + const mockFirebaseFunctionsHttpsCallable = jest.spyOn( + FirebaseStub.functions(), + 'httpsCallable', + ); + + return { + button, + mockFirebaseFunctionsHttpsCallable, + }; +} + +test('prompts for confirmation', async () => { + const { button, mockFirebaseFunctionsHttpsCallable } = await setup(); + + fireEvent.click(button); + + expect(button).toHaveTextContent('Are you sure?'); + expect(mockFirebaseFunctionsHttpsCallable).not.toHaveBeenCalled(); +}); From e7943257875b2daeb641f57db0fa8cc37cfe6342 Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 11 May 2021 16:42:36 +0200 Subject: [PATCH 02/12] FirebaseStub: added auth User class --- .../gatsby-plugin-firebase/auth.test.js | 4 +- __mocks__/gatsby-plugin-firebase/auth/auth.js | 20 ++++++- __mocks__/gatsby-plugin-firebase/auth/user.js | 60 +++++++++++++++++++ .../gatsby-plugin-firebase/constants/auth.js | 2 - 4 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 __mocks__/gatsby-plugin-firebase/auth/user.js diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js index d70a5c1da..ca18050dd 100644 --- a/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js +++ b/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js @@ -13,7 +13,7 @@ test('returns anonymous user 1 when signing in anonymously', async () => { const user = await FirebaseStub.auth().signInAnonymously(); expect(user).toBeTruthy(); - expect(user).toEqual(AuthConstants.anonymousUser1); + expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid); }); test('calls onAuthStateChanged observer with anonymous user 1 when signing in anonymously', async () => { @@ -31,7 +31,7 @@ test('calls onAuthStateChanged observer with anonymous user 1 when signing in an await FirebaseStub.auth().signInAnonymously(); expect(user).toBeTruthy(); - expect(user).toEqual(AuthConstants.anonymousUser1); + expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid); expect(error).toBeNull(); }); diff --git a/__mocks__/gatsby-plugin-firebase/auth/auth.js b/__mocks__/gatsby-plugin-firebase/auth/auth.js index 116ea452c..cc5eeb0a5 100644 --- a/__mocks__/gatsby-plugin-firebase/auth/auth.js +++ b/__mocks__/gatsby-plugin-firebase/auth/auth.js @@ -2,6 +2,7 @@ import { v4 as uuidv4 } from 'uuid'; import Constants from '../constants/auth'; +import User from './user'; import { delay } from '../../../src/utils/index'; const singleton = Symbol(''); @@ -14,6 +15,7 @@ class Auth { } this._uuid = uuidv4(); + this._currentUser = null; this._onAuthStateChangedObservers = []; } @@ -25,6 +27,10 @@ class Auth { return this[singleton]; } + get currentUser() { + return this._currentUser; + } + get uuid() { return this._uuid; } @@ -46,11 +52,21 @@ class Auth { async signInAnonymously() { const user = Constants.anonymousUser1; + this._currentUser = new User( + user.displayName, + user.email, + user.isAnonymous, + user.uid, + async () => {}, + ); + await delay(Constants.defaultDelayInMilliseconds); - this.onAuthStateChangedObservers.forEach((observer) => observer(user)); + this.onAuthStateChangedObservers.forEach((observer) => + observer(this._currentUser), + ); - return user; + return this._currentUser; } } diff --git a/__mocks__/gatsby-plugin-firebase/auth/user.js b/__mocks__/gatsby-plugin-firebase/auth/user.js new file mode 100644 index 000000000..33f438459 --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/auth/user.js @@ -0,0 +1,60 @@ +/* eslint-disable no-underscore-dangle */ +import Constants from '../constants/auth'; +import { delay } from '../../../src/utils/index'; + +class User { + /** + * Creates a new user. + * + * @param {string|null} displayName Display name. + * @param {string|null} email Email. + * @param {boolean} isAnonymous Is anonymous. + * @param {string} uid The user's unique ID. + * @param {function():Promise} deleteUser Delete user callback. + */ + constructor(displayName, email, isAnonymous, uid, deleteUser) { + if (!uid) { + throw new Error('uid must be provided.'); + } else if (typeof uid !== 'string') { + throw new Error('uid should be a string.'); + } else { + this._uid = uid; + } + + if (!deleteUser) { + throw new Error('deleteUser must be provided.'); + } else if (typeof deleteUser !== 'function') { + throw new Error('deleteUser should be a function.'); + } else { + this._deleteUser = deleteUser; + } + + this._displayName = displayName; + this._email = email; + this._isAnonymous = isAnonymous; + } + + get displayName() { + return this._displayName; + } + + get email() { + return this._email; + } + + get isAnonymous() { + return this._isAnonymous; + } + + get uid() { + return this._uid; + } + + async delete() { + await delay(Constants.defaultDelayInMilliseconds); + + await this._deleteUser(); + } +} + +export default User; diff --git a/__mocks__/gatsby-plugin-firebase/constants/auth.js b/__mocks__/gatsby-plugin-firebase/constants/auth.js index 13f8fcc40..685fba3f3 100644 --- a/__mocks__/gatsby-plugin-firebase/constants/auth.js +++ b/__mocks__/gatsby-plugin-firebase/constants/auth.js @@ -2,7 +2,6 @@ const anonymousUser1 = { displayName: 'Anonymous User 1', email: 'anonymous1@noemail.com', isAnonymous: true, - name: 'Anonymous 1', uid: 'anonym123', }; @@ -10,7 +9,6 @@ const anonymousUser2 = { displayName: 'Anonymous User 2', email: 'anonymous2@noemail.com', isAnonymous: true, - name: 'Anonymous 2', uid: 'anonym456', }; From ffb8ae45e00ba7b3be4e95b02edb75f51a5bd47d Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 11 May 2021 17:25:30 +0200 Subject: [PATCH 03/12] Delete Account unit tests: first attempt to spy on Firebase auth User delete --- .../builder.settings.deleteAccount.test.js | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js index 4720dcac6..39302e869 100644 --- a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js +++ b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js @@ -15,22 +15,33 @@ async function setup() { name: /Delete Account/i, }); - const mockFirebaseFunctionsHttpsCallable = jest.spyOn( - FirebaseStub.functions(), - 'httpsCallable', + const mockFirebaseUserDelete = jest.spyOn( + FirebaseStub.auth().currentUser, + 'delete', ); return { button, - mockFirebaseFunctionsHttpsCallable, + mockFirebaseUserDelete, }; } test('prompts for confirmation', async () => { - const { button, mockFirebaseFunctionsHttpsCallable } = await setup(); + const { button, mockFirebaseUserDelete } = await setup(); fireEvent.click(button); expect(button).toHaveTextContent('Are you sure?'); - expect(mockFirebaseFunctionsHttpsCallable).not.toHaveBeenCalled(); + expect(mockFirebaseUserDelete).not.toHaveBeenCalled(); }); + +/* +test('calls Firebase user delete', async () => { + const { button, mockFirebaseUserDelete } = await setup(); + + fireEvent.click(button); + fireEvent.click(button); + + expect(mockFirebaseUserDelete).toHaveBeenCalledTimes(1); +}); +*/ From d11d414504428c6ee94e067b61fd606479092e15 Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Thu, 13 May 2021 16:53:04 +0200 Subject: [PATCH 04/12] Jest mocks: added @reach/router and stub for Firebase auth signOut method --- __mocks__/@reach/router.js | 17 +++++ __mocks__/gatsby-plugin-firebase/auth/auth.js | 16 ++++- .../constants/functions.js | 2 +- .../builder.settings.deleteAccount.test.js | 68 +++++++++++++++---- src/pages/app/__tests__/helpers/builder.js | 11 +++ 5 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 __mocks__/@reach/router.js diff --git a/__mocks__/@reach/router.js b/__mocks__/@reach/router.js new file mode 100644 index 000000000..75fb2af6e --- /dev/null +++ b/__mocks__/@reach/router.js @@ -0,0 +1,17 @@ +import { delay } from '../../src/utils/index'; + +const ReachRouter = jest.requireActual('@reach/router'); + +const defaultDelayInMilliseconds = 100; + +// eslint-disable-next-line no-unused-vars +const navigate = async (to, options) => { + await delay(defaultDelayInMilliseconds); + + return Promise.resolve(); +}; + +module.exports = { + ...ReachRouter, + navigate: jest.fn(navigate), +}; diff --git a/__mocks__/gatsby-plugin-firebase/auth/auth.js b/__mocks__/gatsby-plugin-firebase/auth/auth.js index cc5eeb0a5..edb718c9f 100644 --- a/__mocks__/gatsby-plugin-firebase/auth/auth.js +++ b/__mocks__/gatsby-plugin-firebase/auth/auth.js @@ -57,7 +57,7 @@ class Auth { user.email, user.isAnonymous, user.uid, - async () => {}, + this.signOut, ); await delay(Constants.defaultDelayInMilliseconds); @@ -68,6 +68,20 @@ class Auth { return this._currentUser; } + + async signOut() { + if (this._currentUser === null) { + return; + } + + this._currentUser = null; + + await delay(Constants.defaultDelayInMilliseconds); + + this.onAuthStateChangedObservers.forEach((observer) => + observer(this._currentUser), + ); + } } export default Auth; diff --git a/__mocks__/gatsby-plugin-firebase/constants/functions.js b/__mocks__/gatsby-plugin-firebase/constants/functions.js index 6ab0c54bd..6e238cdc5 100644 --- a/__mocks__/gatsby-plugin-firebase/constants/functions.js +++ b/__mocks__/gatsby-plugin-firebase/constants/functions.js @@ -1,6 +1,6 @@ const deleteUserFunctionName = 'deleteUser'; -const defaultDelayInMilliseconds = 2000; +const defaultDelayInMilliseconds = 100; class Functions { static get deleteUserFunctionName() { diff --git a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js index 39302e869..44706d814 100644 --- a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js +++ b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js @@ -1,8 +1,14 @@ -import { fireEvent, screen } from '@testing-library/react'; +import { navigate as mockNavigateFunction } from '@reach/router'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; -import FirebaseStub, { DatabaseConstants } from 'gatsby-plugin-firebase'; +import FirebaseStub, { + DatabaseConstants, + FunctionsConstants, +} from 'gatsby-plugin-firebase'; -import { setupAndWait } from './helpers/builder'; +import { delay } from '../../../utils/index'; + +import { setupAndWait, findAndDismissNotification } from './helpers/builder'; const testTimeoutInMilliseconds = 20000; jest.setTimeout(testTimeoutInMilliseconds); @@ -11,37 +17,75 @@ async function setup() { const resumeId = DatabaseConstants.demoStateResume1Id; await setupAndWait(resumeId, true, true); - const button = screen.getByRole('button', { - name: /Delete Account/i, + const mockFirebaseDeleteUserCloudFunction = jest.fn(async () => { + await delay(FunctionsConstants.defaultDelayInMilliseconds); }); + const mockFirebaseFunctionsHttpsCallable = jest.fn((name) => + name === FunctionsConstants.deleteUserFunctionName + ? mockFirebaseDeleteUserCloudFunction + : undefined, + ); + FirebaseStub.functions().httpsCallable = mockFirebaseFunctionsHttpsCallable; - const mockFirebaseUserDelete = jest.spyOn( + const mockFirebaseCurrentUserDelete = jest.spyOn( FirebaseStub.auth().currentUser, 'delete', ); + const button = screen.getByRole('button', { + name: /Delete Account/i, + }); + return { button, - mockFirebaseUserDelete, + mockFirebaseCurrentUserDelete, + mockFirebaseDeleteUserCloudFunction, }; } test('prompts for confirmation', async () => { - const { button, mockFirebaseUserDelete } = await setup(); + const { + button, + mockFirebaseDeleteUserCloudFunction, + mockFirebaseCurrentUserDelete, + } = await setup(); fireEvent.click(button); expect(button).toHaveTextContent('Are you sure?'); - expect(mockFirebaseUserDelete).not.toHaveBeenCalled(); + + await waitFor(() => + expect(mockFirebaseDeleteUserCloudFunction).not.toHaveBeenCalledTimes(1), + ); + await waitFor(() => + expect(mockFirebaseCurrentUserDelete).not.toHaveBeenCalled(), + ); }); /* -test('calls Firebase user delete', async () => { - const { button, mockFirebaseUserDelete } = await setup(); +test('calls Firebase delete user cloud function', async () => { + const { button, mockFirebaseDeleteUserCloudFunction } = await setup(); fireEvent.click(button); fireEvent.click(button); - expect(mockFirebaseUserDelete).toHaveBeenCalledTimes(1); + await waitFor(() => expect(mockNavigateFunction).toHaveBeenCalledTimes(1)); + await findAndDismissNotification(); + await waitFor(() => + expect(mockFirebaseDeleteUserCloudFunction).toHaveBeenCalledTimes(1), + ); +}); + +test('calls Firebase current user delete', async () => { + const { button, mockFirebaseCurrentUserDelete } = await setup(); + + fireEvent.click(button); + fireEvent.click(button); + + await waitFor(() => expect(mockNavigateFunction).toHaveBeenCalledTimes(1)); + await findAndDismissNotification(); + await waitFor(() => + expect(mockFirebaseCurrentUserDelete).toHaveBeenCalledTimes(1), + ); }); */ diff --git a/src/pages/app/__tests__/helpers/builder.js b/src/pages/app/__tests__/helpers/builder.js index e63c93468..8f5f30de7 100644 --- a/src/pages/app/__tests__/helpers/builder.js +++ b/src/pages/app/__tests__/helpers/builder.js @@ -47,6 +47,15 @@ const expectDatabaseUpdateToHaveCompleted = async ( ); }; +const dismissNotification = (notification) => { + fireEvent.click(notification); +}; + +const findAndDismissNotification = async () => { + const notification = await screen.findByRole('alert'); + dismissNotification(notification); +}; + const dragAndDropDirectionUp = 'DND_DIRECTION_UP'; const dragAndDropDirectionDown = 'DND_DIRECTION_DOWN'; @@ -158,4 +167,6 @@ export { dragAndDropDirectionUp, dragAndDropDirectionDown, dragAndDropListItem, + dismissNotification, + findAndDismissNotification, }; From a8cf553217d3b3ce0a2f74dc796dde17a85dbdf9 Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Fri, 14 May 2021 18:21:47 +0200 Subject: [PATCH 05/12] Delete Account: added more unit tests --- __mocks__/gatsby-plugin-firebase/auth/auth.js | 4 - src/contexts/UserContext.js | 6 +- .../builder.settings.deleteAccount.test.js | 124 ++++++++++++++---- src/pages/app/__tests__/helpers/builder.js | 11 -- 4 files changed, 98 insertions(+), 47 deletions(-) diff --git a/__mocks__/gatsby-plugin-firebase/auth/auth.js b/__mocks__/gatsby-plugin-firebase/auth/auth.js index edb718c9f..017dfbbcb 100644 --- a/__mocks__/gatsby-plugin-firebase/auth/auth.js +++ b/__mocks__/gatsby-plugin-firebase/auth/auth.js @@ -77,10 +77,6 @@ class Auth { this._currentUser = null; await delay(Constants.defaultDelayInMilliseconds); - - this.onAuthStateChangedObservers.forEach((observer) => - observer(this._currentUser), - ); } } diff --git a/src/contexts/UserContext.js b/src/contexts/UserContext.js index 5132927cc..d071a7e2e 100644 --- a/src/contexts/UserContext.js +++ b/src/contexts/UserContext.js @@ -66,8 +66,8 @@ const UserProvider = ({ children }) => { } }; - const logout = () => { - firebase.auth().signOut(); + const logout = async () => { + await firebase.auth().signOut(); localStorage.removeItem('user'); setUser(null); navigate('/'); @@ -123,7 +123,7 @@ const UserProvider = ({ children }) => { } catch (error) { toast.error(error.message); } finally { - logout(); + await logout(); toast( "It's sad to see you go, but we respect your privacy. All your data has been deleted successfully. Hope to see you again soon!", ); diff --git a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js index 44706d814..3c3044620 100644 --- a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js +++ b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js @@ -1,4 +1,5 @@ import { navigate as mockNavigateFunction } from '@reach/router'; +import { toast } from 'react-toastify'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import FirebaseStub, { @@ -6,9 +7,7 @@ import FirebaseStub, { FunctionsConstants, } from 'gatsby-plugin-firebase'; -import { delay } from '../../../utils/index'; - -import { setupAndWait, findAndDismissNotification } from './helpers/builder'; +import { setupAndWait } from './helpers/builder'; const testTimeoutInMilliseconds = 20000; jest.setTimeout(testTimeoutInMilliseconds); @@ -17,9 +16,7 @@ async function setup() { const resumeId = DatabaseConstants.demoStateResume1Id; await setupAndWait(resumeId, true, true); - const mockFirebaseDeleteUserCloudFunction = jest.fn(async () => { - await delay(FunctionsConstants.defaultDelayInMilliseconds); - }); + const mockFirebaseDeleteUserCloudFunction = jest.fn(async () => {}); const mockFirebaseFunctionsHttpsCallable = jest.fn((name) => name === FunctionsConstants.deleteUserFunctionName ? mockFirebaseDeleteUserCloudFunction @@ -27,19 +24,38 @@ async function setup() { ); FirebaseStub.functions().httpsCallable = mockFirebaseFunctionsHttpsCallable; - const mockFirebaseCurrentUserDelete = jest.spyOn( - FirebaseStub.auth().currentUser, - 'delete', - ); - - const button = screen.getByRole('button', { - name: /Delete Account/i, + const mockFirebaseCurrentUserDelete = jest.fn(async () => { + throw new Error('Error occurred while deleting user.'); }); + FirebaseStub.auth().currentUser.delete = mockFirebaseCurrentUserDelete; + + const mockFirebaseAuthSignOut = jest.fn(async () => { + throw new Error('Error occurred while signing out.'); + }); + FirebaseStub.auth().signOut = mockFirebaseAuthSignOut; + + // eslint-disable-next-line no-unused-vars + const mockToastError = jest.fn((content, options) => {}); + toast.error = mockToastError; + + const deleteAccountRegExp = /Delete Account/i; + const button = screen.getByRole('button', { + name: deleteAccountRegExp, + }); + + const waitForButtonNameToBeSetToDeleteAccount = async () => { + waitFor(() => { + expect(button).toHaveTextContent(deleteAccountRegExp); + }); + }; return { button, - mockFirebaseCurrentUserDelete, mockFirebaseDeleteUserCloudFunction, + mockFirebaseCurrentUserDelete, + mockFirebaseAuthSignOut, + mockToastError, + waitForButtonNameToBeSetToDeleteAccount, }; } @@ -48,44 +64,94 @@ test('prompts for confirmation', async () => { button, mockFirebaseDeleteUserCloudFunction, mockFirebaseCurrentUserDelete, + mockFirebaseAuthSignOut, + mockToastError, } = await setup(); fireEvent.click(button); - expect(button).toHaveTextContent('Are you sure?'); + expect(button).toHaveTextContent(/Are you sure?/i); - await waitFor(() => - expect(mockFirebaseDeleteUserCloudFunction).not.toHaveBeenCalledTimes(1), - ); - await waitFor(() => - expect(mockFirebaseCurrentUserDelete).not.toHaveBeenCalled(), - ); + expect(mockFirebaseDeleteUserCloudFunction).not.toHaveBeenCalled(); + expect(mockFirebaseCurrentUserDelete).not.toHaveBeenCalled(); + expect(mockFirebaseAuthSignOut).not.toHaveBeenCalled(); + expect(mockNavigateFunction).not.toHaveBeenCalled(); + expect(mockToastError).not.toHaveBeenCalled(); }); -/* test('calls Firebase delete user cloud function', async () => { - const { button, mockFirebaseDeleteUserCloudFunction } = await setup(); + const { + button, + mockFirebaseDeleteUserCloudFunction, + waitForButtonNameToBeSetToDeleteAccount, + } = await setup(); fireEvent.click(button); fireEvent.click(button); - await waitFor(() => expect(mockNavigateFunction).toHaveBeenCalledTimes(1)); - await findAndDismissNotification(); + await waitForButtonNameToBeSetToDeleteAccount(); await waitFor(() => expect(mockFirebaseDeleteUserCloudFunction).toHaveBeenCalledTimes(1), ); }); test('calls Firebase current user delete', async () => { - const { button, mockFirebaseCurrentUserDelete } = await setup(); + const { + button, + mockFirebaseCurrentUserDelete, + waitForButtonNameToBeSetToDeleteAccount, + } = await setup(); fireEvent.click(button); fireEvent.click(button); - await waitFor(() => expect(mockNavigateFunction).toHaveBeenCalledTimes(1)); - await findAndDismissNotification(); + await waitForButtonNameToBeSetToDeleteAccount(); await waitFor(() => expect(mockFirebaseCurrentUserDelete).toHaveBeenCalledTimes(1), ); }); -*/ + +test('calls Firebase auth sign out', async () => { + const { + button, + mockFirebaseAuthSignOut, + waitForButtonNameToBeSetToDeleteAccount, + } = await setup(); + + fireEvent.click(button); + fireEvent.click(button); + + await waitForButtonNameToBeSetToDeleteAccount(); + await waitFor(() => expect(mockFirebaseAuthSignOut).toHaveBeenCalledTimes(1)); +}); + +describe('if an error occurs while signing out the current user', () => { + test('does not navigate away', async () => { + const { button, waitForButtonNameToBeSetToDeleteAccount } = await setup(); + + fireEvent.click(button); + fireEvent.click(button); + + await waitForButtonNameToBeSetToDeleteAccount(); + expect(mockNavigateFunction).not.toHaveBeenCalled(); + }); + + /* + test('displays toast', async () => { + const { + button, + waitForButtonNameToBeSetToDeleteAccount, + mockToastError, + } = await setup(); + + fireEvent.click(button); + fireEvent.click(button); + + await waitForButtonNameToBeSetToDeleteAccount(); + expect(mockToastError).toHaveBeenCalledTimes(2); + expect(mockToastError).toHaveBeenLastCalledWith( + 'An error occurred deleting your account.', + ); + }); + */ +}); diff --git a/src/pages/app/__tests__/helpers/builder.js b/src/pages/app/__tests__/helpers/builder.js index 8f5f30de7..e63c93468 100644 --- a/src/pages/app/__tests__/helpers/builder.js +++ b/src/pages/app/__tests__/helpers/builder.js @@ -47,15 +47,6 @@ const expectDatabaseUpdateToHaveCompleted = async ( ); }; -const dismissNotification = (notification) => { - fireEvent.click(notification); -}; - -const findAndDismissNotification = async () => { - const notification = await screen.findByRole('alert'); - dismissNotification(notification); -}; - const dragAndDropDirectionUp = 'DND_DIRECTION_UP'; const dragAndDropDirectionDown = 'DND_DIRECTION_DOWN'; @@ -167,6 +158,4 @@ export { dragAndDropDirectionUp, dragAndDropDirectionDown, dragAndDropListItem, - dismissNotification, - findAndDismissNotification, }; From 53d0e178652b98e9d2a8d13949a943cc47d4068e Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 18 May 2021 13:41:38 +0200 Subject: [PATCH 06/12] Delete Account unit tests: fixed warnings, added test for error notifications --- .../builder.settings.deleteAccount.test.js | 126 +++++++++++------- src/utils/index.js | 5 +- 2 files changed, 80 insertions(+), 51 deletions(-) diff --git a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js index 3c3044620..7c5f8f1dd 100644 --- a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js +++ b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js @@ -3,20 +3,24 @@ import { toast } from 'react-toastify'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import FirebaseStub, { + AuthConstants, DatabaseConstants, FunctionsConstants, } from 'gatsby-plugin-firebase'; import { setupAndWait } from './helpers/builder'; +import { delay } from '../../../utils/index'; -const testTimeoutInMilliseconds = 20000; +const testTimeoutInMilliseconds = 60000; jest.setTimeout(testTimeoutInMilliseconds); async function setup() { const resumeId = DatabaseConstants.demoStateResume1Id; await setupAndWait(resumeId, true, true); - const mockFirebaseDeleteUserCloudFunction = jest.fn(async () => {}); + const mockFirebaseDeleteUserCloudFunction = jest.fn(async () => { + await delay(FunctionsConstants.defaultDelayInMilliseconds); + }); const mockFirebaseFunctionsHttpsCallable = jest.fn((name) => name === FunctionsConstants.deleteUserFunctionName ? mockFirebaseDeleteUserCloudFunction @@ -24,13 +28,16 @@ async function setup() { ); FirebaseStub.functions().httpsCallable = mockFirebaseFunctionsHttpsCallable; - const mockFirebaseCurrentUserDelete = jest.fn(async () => { - throw new Error('Error occurred while deleting user.'); - }); - FirebaseStub.auth().currentUser.delete = mockFirebaseCurrentUserDelete; + const mockFirebaseCurrentUserDelete = jest.spyOn( + FirebaseStub.auth().currentUser, + 'delete', + ); + const mockFirebaseAuthSignOutErrorMessage = + 'Error occurred while signing out.'; const mockFirebaseAuthSignOut = jest.fn(async () => { - throw new Error('Error occurred while signing out.'); + await delay(AuthConstants.defaultDelayInMilliseconds); + throw new Error(mockFirebaseAuthSignOutErrorMessage); }); FirebaseStub.auth().signOut = mockFirebaseAuthSignOut; @@ -38,40 +45,46 @@ async function setup() { const mockToastError = jest.fn((content, options) => {}); toast.error = mockToastError; - const deleteAccountRegExp = /Delete Account/i; - const button = screen.getByRole('button', { - name: deleteAccountRegExp, + const deleteAccountButtonText = /Delete Account/i; + const areYouSureButtonText = /Are you sure?/i; + const deleteAccountButton = screen.getByRole('button', { + name: deleteAccountButtonText, }); - - const waitForButtonNameToBeSetToDeleteAccount = async () => { - waitFor(() => { - expect(button).toHaveTextContent(deleteAccountRegExp); + const waitForDeleteAccountButtonTextToChangeTo = async (text) => { + await waitFor(() => expect(deleteAccountButton).toHaveTextContent(text), { + timeout: 30000, }); }; return { - button, + deleteAccountButton, + areYouSureButtonText, + deleteAccountButtonText, + waitForDeleteAccountButtonTextToChangeTo, mockFirebaseDeleteUserCloudFunction, mockFirebaseCurrentUserDelete, mockFirebaseAuthSignOut, + mockFirebaseAuthSignOutErrorMessage, mockToastError, - waitForButtonNameToBeSetToDeleteAccount, }; } test('prompts for confirmation', async () => { const { - button, + deleteAccountButton, + areYouSureButtonText, + waitForDeleteAccountButtonTextToChangeTo, mockFirebaseDeleteUserCloudFunction, mockFirebaseCurrentUserDelete, mockFirebaseAuthSignOut, mockToastError, } = await setup(); - fireEvent.click(button); - - expect(button).toHaveTextContent(/Are you sure?/i); + fireEvent.click(deleteAccountButton); + await expect( + waitForDeleteAccountButtonTextToChangeTo(areYouSureButtonText), + ).resolves.toBeUndefined(); expect(mockFirebaseDeleteUserCloudFunction).not.toHaveBeenCalled(); expect(mockFirebaseCurrentUserDelete).not.toHaveBeenCalled(); expect(mockFirebaseAuthSignOut).not.toHaveBeenCalled(); @@ -81,15 +94,17 @@ test('prompts for confirmation', async () => { test('calls Firebase delete user cloud function', async () => { const { - button, + deleteAccountButton, + deleteAccountButtonText, mockFirebaseDeleteUserCloudFunction, - waitForButtonNameToBeSetToDeleteAccount, + waitForDeleteAccountButtonTextToChangeTo, } = await setup(); - fireEvent.click(button); - fireEvent.click(button); + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); - await waitForButtonNameToBeSetToDeleteAccount(); await waitFor(() => expect(mockFirebaseDeleteUserCloudFunction).toHaveBeenCalledTimes(1), ); @@ -97,15 +112,17 @@ test('calls Firebase delete user cloud function', async () => { test('calls Firebase current user delete', async () => { const { - button, + deleteAccountButton, + deleteAccountButtonText, mockFirebaseCurrentUserDelete, - waitForButtonNameToBeSetToDeleteAccount, + waitForDeleteAccountButtonTextToChangeTo, } = await setup(); - fireEvent.click(button); - fireEvent.click(button); + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); - await waitForButtonNameToBeSetToDeleteAccount(); await waitFor(() => expect(mockFirebaseCurrentUserDelete).toHaveBeenCalledTimes(1), ); @@ -113,45 +130,58 @@ test('calls Firebase current user delete', async () => { test('calls Firebase auth sign out', async () => { const { - button, + deleteAccountButton, + deleteAccountButtonText, mockFirebaseAuthSignOut, - waitForButtonNameToBeSetToDeleteAccount, + waitForDeleteAccountButtonTextToChangeTo, } = await setup(); - fireEvent.click(button); - fireEvent.click(button); + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); - await waitForButtonNameToBeSetToDeleteAccount(); await waitFor(() => expect(mockFirebaseAuthSignOut).toHaveBeenCalledTimes(1)); }); describe('if an error occurs while signing out the current user', () => { test('does not navigate away', async () => { - const { button, waitForButtonNameToBeSetToDeleteAccount } = await setup(); + const { + deleteAccountButton, + deleteAccountButtonText, + waitForDeleteAccountButtonTextToChangeTo, + } = await setup(); - fireEvent.click(button); - fireEvent.click(button); + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); - await waitForButtonNameToBeSetToDeleteAccount(); expect(mockNavigateFunction).not.toHaveBeenCalled(); }); - /* - test('displays toast', async () => { + test('displays error notifications', async () => { const { - button, - waitForButtonNameToBeSetToDeleteAccount, + deleteAccountButton, + deleteAccountButtonText, + mockFirebaseAuthSignOutErrorMessage, + waitForDeleteAccountButtonTextToChangeTo, mockToastError, } = await setup(); - fireEvent.click(button); - fireEvent.click(button); + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); - await waitForButtonNameToBeSetToDeleteAccount(); expect(mockToastError).toHaveBeenCalledTimes(2); - expect(mockToastError).toHaveBeenLastCalledWith( + expect(mockToastError).toHaveBeenNthCalledWith( + 1, + mockFirebaseAuthSignOutErrorMessage, + ); + expect(mockToastError).toHaveBeenNthCalledWith( + 2, 'An error occurred deleting your account.', ); }); - */ }); diff --git a/src/utils/index.js b/src/utils/index.js index 843d14544..6e657583d 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -137,6 +137,5 @@ export const b64toBlob = (b64Data, contentType = '', sliceSize = 512) => { return blob; }; -export const delay = async (milliseconds) => { - await new Promise((resolve) => setTimeout(resolve, milliseconds)); -}; +export const delay = (milliseconds) => + new Promise((resolve) => setTimeout(resolve, milliseconds)); From 7cb469657d9d678125ed8d031a4535d05cbe8bb7 Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 18 May 2021 13:57:34 +0200 Subject: [PATCH 07/12] Firebase.auth Jest mock: minor fix in signOut() method --- __mocks__/gatsby-plugin-firebase/auth/auth.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/__mocks__/gatsby-plugin-firebase/auth/auth.js b/__mocks__/gatsby-plugin-firebase/auth/auth.js index 017dfbbcb..1dc0db674 100644 --- a/__mocks__/gatsby-plugin-firebase/auth/auth.js +++ b/__mocks__/gatsby-plugin-firebase/auth/auth.js @@ -77,6 +77,8 @@ class Auth { this._currentUser = null; await delay(Constants.defaultDelayInMilliseconds); + + this.onAuthStateChangedObservers.forEach((observer) => observer(null)); } } From 9e63d0b2c76990bd9748d3fde8dd44cbccc55a8b Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Thu, 20 May 2021 15:57:01 +0200 Subject: [PATCH 08/12] Delete Account unit tests: added tests related to Google reauthentication --- .../gatsby-plugin-firebase/database.test.js | 27 +++- __mocks__/gatsby-plugin-firebase.js | 3 + __mocks__/gatsby-plugin-firebase/auth/auth.js | 43 ++++++- .../auth/authProvider.js | 23 ++++ .../auth/googleAuthProvider.js | 10 ++ __mocks__/gatsby-plugin-firebase/auth/user.js | 49 ++++---- .../gatsby-plugin-firebase/auth/userInfo.js | 47 +++++++ .../gatsby-plugin-firebase/constants/auth.js | 24 +++- .../constants/database.js | 24 +++- .../database/database.js | 26 +++- .../__tests__/builder.dataPersistence.test.js | 3 +- .../app/__tests__/builder.rendering.test.js | 3 +- .../builder.settings.deleteAccount.test.js | 115 ++++++++++++++++-- .../app/__tests__/builder.settings.test.js | 1 + .../app/__tests__/builder.skills.test.js | 1 + .../__tests__/dashboard.deleteResume.test.js | 2 +- src/pages/app/__tests__/helpers/builder.js | 12 +- src/templates/__tests__/Castform.test.js | 6 +- 18 files changed, 363 insertions(+), 56 deletions(-) create mode 100644 __mocks__/gatsby-plugin-firebase/auth/authProvider.js create mode 100644 __mocks__/gatsby-plugin-firebase/auth/googleAuthProvider.js create mode 100644 __mocks__/gatsby-plugin-firebase/auth/userInfo.js diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/database.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/database.test.js index 8a97c0a94..e0f219af1 100644 --- a/__mocks__/__tests__/gatsby-plugin-firebase/database.test.js +++ b/__mocks__/__tests__/gatsby-plugin-firebase/database.test.js @@ -24,7 +24,7 @@ test('initializing data sets up resumes and users', async () => { const resumesDataSnapshot = await resumesRef.once('value'); const resumes = resumesDataSnapshot.val(); expect(resumes).toBeTruthy(); - expect(Object.keys(resumes)).toHaveLength(3); + expect(Object.keys(resumes)).toHaveLength(5); const demoStateResume1 = resumes[DatabaseConstants.demoStateResume1Id]; expect(demoStateResume1).toBeTruthy(); expect(demoStateResume1.id).toEqual(DatabaseConstants.demoStateResume1Id); @@ -33,20 +33,35 @@ test('initializing data sets up resumes and users', async () => { expect(demoStateResume2).toBeTruthy(); expect(demoStateResume2.id).toEqual(DatabaseConstants.demoStateResume2Id); expect(demoStateResume2.user).toEqual(DatabaseConstants.user2.uid); - const initialStateResume = resumes[DatabaseConstants.initialStateResumeId]; - expect(initialStateResume).toBeTruthy(); - expect(initialStateResume.id).toEqual(DatabaseConstants.initialStateResumeId); - expect(initialStateResume.user).toEqual(DatabaseConstants.user1.uid); + const initialStateResume1 = resumes[DatabaseConstants.initialStateResume1Id]; + expect(initialStateResume1).toBeTruthy(); + expect(initialStateResume1.id).toEqual( + DatabaseConstants.initialStateResume1Id, + ); + expect(initialStateResume1.user).toEqual(DatabaseConstants.user1.uid); + const demoStateResume3 = resumes[DatabaseConstants.demoStateResume3Id]; + expect(demoStateResume3).toBeTruthy(); + expect(demoStateResume3.id).toEqual(DatabaseConstants.demoStateResume3Id); + expect(demoStateResume3.user).toEqual(DatabaseConstants.user3.uid); + const initialStateResume2 = resumes[DatabaseConstants.initialStateResume2Id]; + expect(initialStateResume2).toBeTruthy(); + expect(initialStateResume2.id).toEqual( + DatabaseConstants.initialStateResume2Id, + ); + expect(initialStateResume2.user).toEqual(DatabaseConstants.user3.uid); const usersRef = FirebaseStub.database().ref(DatabaseConstants.usersPath); const usersDataSnapshot = await usersRef.once('value'); const users = usersDataSnapshot.val(); expect(users).toBeTruthy(); - expect(Object.keys(users)).toHaveLength(2); + expect(Object.keys(users)).toHaveLength(3); const anonymousUser1 = users[DatabaseConstants.user1.uid]; expect(anonymousUser1).toBeTruthy(); expect(anonymousUser1).toEqual(DatabaseConstants.user1); const anonymousUser2 = users[DatabaseConstants.user2.uid]; expect(anonymousUser2).toBeTruthy(); expect(anonymousUser2).toEqual(DatabaseConstants.user2); + const googleUser3 = users[DatabaseConstants.user3.uid]; + expect(googleUser3).toBeTruthy(); + expect(googleUser3).toEqual(DatabaseConstants.user3); }); diff --git a/__mocks__/gatsby-plugin-firebase.js b/__mocks__/gatsby-plugin-firebase.js index 556b9917d..84340d70d 100644 --- a/__mocks__/gatsby-plugin-firebase.js +++ b/__mocks__/gatsby-plugin-firebase.js @@ -1,4 +1,5 @@ import Auth from './gatsby-plugin-firebase/auth/auth'; +import GoogleAuthProvider from './gatsby-plugin-firebase/auth/googleAuthProvider'; import Database from './gatsby-plugin-firebase/database/database'; import Functions from './gatsby-plugin-firebase/functions/functions'; import AuthConstants from './gatsby-plugin-firebase/constants/auth'; @@ -19,6 +20,8 @@ class FirebaseStub { } } +FirebaseStub.auth.GoogleAuthProvider = GoogleAuthProvider; + FirebaseStub.database.ServerValue = {}; Object.defineProperty(FirebaseStub.database.ServerValue, 'TIMESTAMP', { get() { diff --git a/__mocks__/gatsby-plugin-firebase/auth/auth.js b/__mocks__/gatsby-plugin-firebase/auth/auth.js index 1dc0db674..b8217a012 100644 --- a/__mocks__/gatsby-plugin-firebase/auth/auth.js +++ b/__mocks__/gatsby-plugin-firebase/auth/auth.js @@ -2,6 +2,8 @@ import { v4 as uuidv4 } from 'uuid'; import Constants from '../constants/auth'; +import AuthProvider from './authProvider'; +import GoogleAuthProvider from './googleAuthProvider'; import User from './user'; import { delay } from '../../../src/utils/index'; @@ -55,8 +57,47 @@ class Auth { this._currentUser = new User( user.displayName, user.email, - user.isAnonymous, + user.providerId, user.uid, + user.isAnonymous, + this.signOut, + ); + + await delay(Constants.defaultDelayInMilliseconds); + + this.onAuthStateChangedObservers.forEach((observer) => + observer(this._currentUser), + ); + + return this._currentUser; + } + + /** + * Authenticates with popup. + * + * @param {AuthProvider} provider The provider to authenticate. + */ + async signInWithPopup(provider) { + if (!provider) { + throw new Error('provider must be provided.'); + } else if (!(provider instanceof AuthProvider)) { + throw new Error('provider should be an AuthProvider.'); + } + + if (!(provider instanceof GoogleAuthProvider)) { + throw new Error( + `${provider.constructor.name} is currently not supported.`, + ); + } + + const user = Constants.googleUser3; + + this._currentUser = new User( + user.displayName, + user.email, + user.providerId, + user.uid, + user.isAnonymous, this.signOut, ); diff --git a/__mocks__/gatsby-plugin-firebase/auth/authProvider.js b/__mocks__/gatsby-plugin-firebase/auth/authProvider.js new file mode 100644 index 000000000..e9b77a6cf --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/auth/authProvider.js @@ -0,0 +1,23 @@ +/* eslint-disable no-underscore-dangle */ +class AuthProvider { + /** + * Creates a new auth provider. + * + * @param {string} providerId Provider ID. + */ + constructor(providerId) { + if (!providerId) { + throw new Error('providerId must be provided.'); + } else if (typeof providerId !== 'string') { + throw new Error('providerId should be a string.'); + } else { + this._providerId = providerId; + } + } + + get providerId() { + return this._providerId; + } +} + +export default AuthProvider; diff --git a/__mocks__/gatsby-plugin-firebase/auth/googleAuthProvider.js b/__mocks__/gatsby-plugin-firebase/auth/googleAuthProvider.js new file mode 100644 index 000000000..f98ea3c66 --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/auth/googleAuthProvider.js @@ -0,0 +1,10 @@ +import AuthProvider from './authProvider'; +import Constants from '../constants/auth'; + +class GoogleAuthProvider extends AuthProvider { + constructor() { + super(Constants.googleAuthProviderId); + } +} + +export default GoogleAuthProvider; diff --git a/__mocks__/gatsby-plugin-firebase/auth/user.js b/__mocks__/gatsby-plugin-firebase/auth/user.js index 33f438459..05843cb47 100644 --- a/__mocks__/gatsby-plugin-firebase/auth/user.js +++ b/__mocks__/gatsby-plugin-firebase/auth/user.js @@ -1,25 +1,23 @@ /* eslint-disable no-underscore-dangle */ import Constants from '../constants/auth'; +// eslint-disable-next-line no-unused-vars +import AuthProvider from './authProvider'; +import UserInfo from './userInfo'; import { delay } from '../../../src/utils/index'; -class User { +class User extends UserInfo { /** * Creates a new user. * * @param {string|null} displayName Display name. * @param {string|null} email Email. - * @param {boolean} isAnonymous Is anonymous. + * @param {string} providerId Auth provider ID. * @param {string} uid The user's unique ID. + * @param {boolean} isAnonymous Is anonymous. * @param {function():Promise} deleteUser Delete user callback. */ - constructor(displayName, email, isAnonymous, uid, deleteUser) { - if (!uid) { - throw new Error('uid must be provided.'); - } else if (typeof uid !== 'string') { - throw new Error('uid should be a string.'); - } else { - this._uid = uid; - } + constructor(displayName, email, providerId, uid, isAnonymous, deleteUser) { + super(displayName, email, providerId, uid); if (!deleteUser) { throw new Error('deleteUser must be provided.'); @@ -29,25 +27,22 @@ class User { this._deleteUser = deleteUser; } - this._displayName = displayName; - this._email = email; this._isAnonymous = isAnonymous; - } - get displayName() { - return this._displayName; - } - - get email() { - return this._email; + this._providerData = []; + if (!isAnonymous) { + this._providerData.push( + new UserInfo(displayName, email, providerId, uid), + ); + } } get isAnonymous() { return this._isAnonymous; } - get uid() { - return this._uid; + get providerData() { + return this._providerData; } async delete() { @@ -55,6 +50,18 @@ class User { await this._deleteUser(); } + + /** + * Reauthenticates the user with popup. + * + * @param {AuthProvider} provider The provider to authenticate. + */ + // eslint-disable-next-line no-unused-vars + async reauthenticateWithPopup(provider) { + await delay(Constants.defaultDelayInMilliseconds); + + return this; + } } export default User; diff --git a/__mocks__/gatsby-plugin-firebase/auth/userInfo.js b/__mocks__/gatsby-plugin-firebase/auth/userInfo.js new file mode 100644 index 000000000..c648698de --- /dev/null +++ b/__mocks__/gatsby-plugin-firebase/auth/userInfo.js @@ -0,0 +1,47 @@ +/* eslint-disable no-underscore-dangle */ +class UserInfo { + /** + * Creates a new user profile information. + * + * @param {string|null} displayName Display name. + * @param {string|null} email Email. + * @param {string} providerId Auth provider ID. + * @param {string} uid The user's unique ID. + */ + constructor(displayName, email, providerId, uid) { + if (!uid) { + throw new Error('uid must be provided.'); + } else if (typeof uid !== 'string') { + throw new Error('uid should be a string.'); + } else { + this._uid = uid; + } + + if (typeof providerId !== 'string') { + throw new Error('providerId should be a string.'); + } else { + this._providerId = providerId; + } + + this._displayName = displayName; + this._email = email; + } + + get displayName() { + return this._displayName; + } + + get email() { + return this._email; + } + + get providerId() { + return this._providerId; + } + + get uid() { + return this._uid; + } +} + +export default UserInfo; diff --git a/__mocks__/gatsby-plugin-firebase/constants/auth.js b/__mocks__/gatsby-plugin-firebase/constants/auth.js index 685fba3f3..072d04a7d 100644 --- a/__mocks__/gatsby-plugin-firebase/constants/auth.js +++ b/__mocks__/gatsby-plugin-firebase/constants/auth.js @@ -1,20 +1,36 @@ +const googleAuthProviderId = 'google.com'; + const anonymousUser1 = { displayName: 'Anonymous User 1', email: 'anonymous1@noemail.com', isAnonymous: true, - uid: 'anonym123', + providerId: '', + uid: 'anonym1', }; const anonymousUser2 = { displayName: 'Anonymous User 2', email: 'anonymous2@noemail.com', isAnonymous: true, - uid: 'anonym456', + providerId: '', + uid: 'anonym2', +}; + +const googleUser3 = { + displayName: 'Google User 3', + email: 'google3@noemail.com', + isAnonymous: false, + providerId: googleAuthProviderId, + uid: 'google3', }; const defaultDelayInMilliseconds = 100; class Auth { + static get googleAuthProviderId() { + return googleAuthProviderId; + } + static get anonymousUser1() { return anonymousUser1; } @@ -23,6 +39,10 @@ class Auth { return anonymousUser2; } + static get googleUser3() { + return googleUser3; + } + static get defaultDelayInMilliseconds() { return defaultDelayInMilliseconds; } diff --git a/__mocks__/gatsby-plugin-firebase/constants/database.js b/__mocks__/gatsby-plugin-firebase/constants/database.js index 62a5459e0..e4417175b 100644 --- a/__mocks__/gatsby-plugin-firebase/constants/database.js +++ b/__mocks__/gatsby-plugin-firebase/constants/database.js @@ -9,7 +9,9 @@ const connectedPath = '.info/connected'; const demoStateResume1Id = 'demo_1'; const demoStateResume2Id = 'demo_2'; -const initialStateResumeId = 'initst'; +const demoStateResume3Id = 'demo_3'; +const initialStateResume1Id = 'init_1'; +const initialStateResume2Id = 'init_2'; const user1 = { uid: AuthConstants.anonymousUser1.uid, @@ -19,6 +21,10 @@ const user2 = { uid: AuthConstants.anonymousUser2.uid, isAnonymous: AuthConstants.anonymousUser2.isAnonymous, }; +const user3 = { + uid: AuthConstants.googleUser3.uid, + isAnonymous: AuthConstants.googleUser3.isAnonymous, +}; const defaultDelayInMilliseconds = 100; @@ -51,8 +57,16 @@ class Database { return demoStateResume2Id; } - static get initialStateResumeId() { - return initialStateResumeId; + static get demoStateResume3Id() { + return demoStateResume3Id; + } + + static get initialStateResume1Id() { + return initialStateResume1Id; + } + + static get initialStateResume2Id() { + return initialStateResume2Id; } static get user1() { @@ -63,6 +77,10 @@ class Database { return user2; } + static get user3() { + return user3; + } + static get defaultDelayInMilliseconds() { return defaultDelayInMilliseconds; } diff --git a/__mocks__/gatsby-plugin-firebase/database/database.js b/__mocks__/gatsby-plugin-firebase/database/database.js index 69125d2d0..6df9ea5ab 100644 --- a/__mocks__/gatsby-plugin-firebase/database/database.js +++ b/__mocks__/gatsby-plugin-firebase/database/database.js @@ -101,9 +101,9 @@ class Database { initializeData() { const resumes = {}; + const date = new Date('December 15, 2020 11:20:25'); const demoStateResume1 = readFile('../../../src/data/demoState.json'); - const date = new Date('December 15, 2020 11:20:25'); demoStateResume1.updatedAt = date.valueOf(); date.setMonth(date.getMonth() - 2); demoStateResume1.createdAt = date.valueOf(); @@ -114,11 +114,24 @@ class Database { demoStateResume2.user = DatabaseConstants.user2.uid; resumes[DatabaseConstants.demoStateResume2Id] = demoStateResume2; - const initialStateResume = readFile('../../../src/data/initialState.json'); - initialStateResume.updatedAt = date.valueOf(); - initialStateResume.createdAt = date.valueOf(); - initialStateResume.user = DatabaseConstants.user1.uid; - resumes[DatabaseConstants.initialStateResumeId] = initialStateResume; + const initialStateResume1 = readFile('../../../src/data/initialState.json'); + initialStateResume1.updatedAt = date.valueOf(); + initialStateResume1.createdAt = date.valueOf(); + initialStateResume1.user = DatabaseConstants.user1.uid; + resumes[DatabaseConstants.initialStateResume1Id] = initialStateResume1; + + const demoStateResume3 = readFile('../../../src/data/demoState.json'); + demoStateResume3.updatedAt = date.valueOf(); + date.setMonth(date.getMonth() - 2); + demoStateResume3.createdAt = date.valueOf(); + demoStateResume3.user = DatabaseConstants.user3.uid; + resumes[DatabaseConstants.demoStateResume3Id] = demoStateResume3; + + const initialStateResume2 = readFile('../../../src/data/initialState.json'); + initialStateResume2.updatedAt = date.valueOf(); + initialStateResume2.createdAt = date.valueOf(); + initialStateResume2.user = DatabaseConstants.user3.uid; + resumes[DatabaseConstants.initialStateResume2Id] = initialStateResume2; Object.keys(resumes).forEach((key) => { const resume = resumes[key]; @@ -131,6 +144,7 @@ class Database { const users = {}; users[DatabaseConstants.user1.uid] = DatabaseConstants.user1; users[DatabaseConstants.user2.uid] = DatabaseConstants.user2; + users[DatabaseConstants.user3.uid] = DatabaseConstants.user3; this._data[DatabaseConstants.usersPath] = users; } diff --git a/src/pages/app/__tests__/builder.dataPersistence.test.js b/src/pages/app/__tests__/builder.dataPersistence.test.js index 9f1fef644..26748c590 100644 --- a/src/pages/app/__tests__/builder.dataPersistence.test.js +++ b/src/pages/app/__tests__/builder.dataPersistence.test.js @@ -7,13 +7,14 @@ import { expectDatabaseUpdateToHaveCompleted, } from './helpers/builder'; -const testTimeoutInMilliseconds = 20000; +const testTimeoutInMilliseconds = 30000; jest.setTimeout(testTimeoutInMilliseconds); test('when input value is changed, updates database', async () => { const resumeId = DatabaseConstants.demoStateResume1Id; const { mockDatabaseUpdateFunction } = await setupAndWait( resumeId, + false, true, true, ); diff --git a/src/pages/app/__tests__/builder.rendering.test.js b/src/pages/app/__tests__/builder.rendering.test.js index 950296522..3a5e82bdb 100644 --- a/src/pages/app/__tests__/builder.rendering.test.js +++ b/src/pages/app/__tests__/builder.rendering.test.js @@ -20,6 +20,7 @@ jest.setTimeout(testTimeoutInMilliseconds); test('renders first and last name', async () => { const { resume } = await setupAndWait( DatabaseConstants.demoStateResume1Id, + false, true, true, ); @@ -53,7 +54,7 @@ test('renders loading screen', async () => { }); test('if resume is in initial state, renders load demo data notification', async () => { - await setup(DatabaseConstants.initialStateResumeId); + await setup(DatabaseConstants.initialStateResume1Id); const notification = await screen.findByRole('alert'); expect( diff --git a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js index 7c5f8f1dd..72bc54fe9 100644 --- a/src/pages/app/__tests__/builder.settings.deleteAccount.test.js +++ b/src/pages/app/__tests__/builder.settings.deleteAccount.test.js @@ -14,9 +14,32 @@ import { delay } from '../../../utils/index'; const testTimeoutInMilliseconds = 60000; jest.setTimeout(testTimeoutInMilliseconds); -async function setup() { - const resumeId = DatabaseConstants.demoStateResume1Id; - await setupAndWait(resumeId, true, true); +async function setup(signInWithGoogle, failReauthentication) { + const resumeId = signInWithGoogle + ? DatabaseConstants.demoStateResume3Id + : DatabaseConstants.demoStateResume1Id; + + await setupAndWait(resumeId, signInWithGoogle, true, true); + + let mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage; + let mockFirebaseCurrentUserReauthenticateWithPopup; + if (failReauthentication) { + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage = + 'Error occurred while reauthenticating.'; + mockFirebaseCurrentUserReauthenticateWithPopup = jest.fn(async () => { + await delay(AuthConstants.defaultDelayInMilliseconds); + throw new Error( + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage, + ); + }); + FirebaseStub.auth().currentUser.reauthenticateWithPopup = mockFirebaseCurrentUserReauthenticateWithPopup; + } else { + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage = null; + mockFirebaseCurrentUserReauthenticateWithPopup = jest.spyOn( + FirebaseStub.auth().currentUser, + 'reauthenticateWithPopup', + ); + } const mockFirebaseDeleteUserCloudFunction = jest.fn(async () => { await delay(FunctionsConstants.defaultDelayInMilliseconds); @@ -61,6 +84,8 @@ async function setup() { areYouSureButtonText, deleteAccountButtonText, waitForDeleteAccountButtonTextToChangeTo, + mockFirebaseCurrentUserReauthenticateWithPopup, + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage, mockFirebaseDeleteUserCloudFunction, mockFirebaseCurrentUserDelete, mockFirebaseAuthSignOut, @@ -78,7 +103,7 @@ test('prompts for confirmation', async () => { mockFirebaseCurrentUserDelete, mockFirebaseAuthSignOut, mockToastError, - } = await setup(); + } = await setup(false, false); fireEvent.click(deleteAccountButton); @@ -98,7 +123,7 @@ test('calls Firebase delete user cloud function', async () => { deleteAccountButtonText, mockFirebaseDeleteUserCloudFunction, waitForDeleteAccountButtonTextToChangeTo, - } = await setup(); + } = await setup(false, false); fireEvent.click(deleteAccountButton); fireEvent.click(deleteAccountButton); @@ -116,7 +141,7 @@ test('calls Firebase current user delete', async () => { deleteAccountButtonText, mockFirebaseCurrentUserDelete, waitForDeleteAccountButtonTextToChangeTo, - } = await setup(); + } = await setup(false, false); fireEvent.click(deleteAccountButton); fireEvent.click(deleteAccountButton); @@ -134,7 +159,7 @@ test('calls Firebase auth sign out', async () => { deleteAccountButtonText, mockFirebaseAuthSignOut, waitForDeleteAccountButtonTextToChangeTo, - } = await setup(); + } = await setup(false, false); fireEvent.click(deleteAccountButton); fireEvent.click(deleteAccountButton); @@ -150,7 +175,7 @@ describe('if an error occurs while signing out the current user', () => { deleteAccountButton, deleteAccountButtonText, waitForDeleteAccountButtonTextToChangeTo, - } = await setup(); + } = await setup(false, false); fireEvent.click(deleteAccountButton); fireEvent.click(deleteAccountButton); @@ -167,7 +192,7 @@ describe('if an error occurs while signing out the current user', () => { mockFirebaseAuthSignOutErrorMessage, waitForDeleteAccountButtonTextToChangeTo, mockToastError, - } = await setup(); + } = await setup(false, false); fireEvent.click(deleteAccountButton); fireEvent.click(deleteAccountButton); @@ -185,3 +210,75 @@ describe('if an error occurs while signing out the current user', () => { ); }); }); + +describe('if the current user is signed in with Google', () => { + test('reauthenticates the user', async () => { + const googleAuthProvider = new FirebaseStub.auth.GoogleAuthProvider(); + const { + deleteAccountButton, + deleteAccountButtonText, + mockFirebaseCurrentUserReauthenticateWithPopup, + waitForDeleteAccountButtonTextToChangeTo, + } = await setup(true, false); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + expect( + mockFirebaseCurrentUserReauthenticateWithPopup, + ).toHaveBeenCalledTimes(1); + expect(mockFirebaseCurrentUserReauthenticateWithPopup).toHaveBeenCalledWith( + googleAuthProvider, + ); + }); + + describe('and reauthentication fails', () => { + test('does not proceed further', async () => { + const { + deleteAccountButton, + deleteAccountButtonText, + waitForDeleteAccountButtonTextToChangeTo, + mockFirebaseDeleteUserCloudFunction, + mockFirebaseCurrentUserDelete, + mockFirebaseAuthSignOut, + } = await setup(true, true); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + expect(mockFirebaseDeleteUserCloudFunction).not.toHaveBeenCalled(); + expect(mockFirebaseCurrentUserDelete).not.toHaveBeenCalled(); + expect(mockFirebaseAuthSignOut).not.toHaveBeenCalled(); + expect(mockNavigateFunction).not.toHaveBeenCalled(); + }); + + test('displays error notifications', async () => { + const { + deleteAccountButton, + deleteAccountButtonText, + waitForDeleteAccountButtonTextToChangeTo, + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage, + mockToastError, + } = await setup(true, true); + + fireEvent.click(deleteAccountButton); + fireEvent.click(deleteAccountButton); + + await waitForDeleteAccountButtonTextToChangeTo(deleteAccountButtonText); + + expect(mockToastError).toHaveBeenCalledTimes(2); + expect(mockToastError).toHaveBeenNthCalledWith( + 1, + mockFirebaseCurrentUserReauthenticateWithPopupErrorMessage, + ); + expect(mockToastError).toHaveBeenNthCalledWith( + 2, + 'An error occurred deleting your account.', + ); + }); + }); +}); diff --git a/src/pages/app/__tests__/builder.settings.test.js b/src/pages/app/__tests__/builder.settings.test.js index 7db8a1c1a..6bb4e7a1d 100644 --- a/src/pages/app/__tests__/builder.settings.test.js +++ b/src/pages/app/__tests__/builder.settings.test.js @@ -16,6 +16,7 @@ test('allows to change the language', async () => { const resumeId = DatabaseConstants.demoStateResume1Id; const { mockDatabaseUpdateFunction } = await setupAndWait( resumeId, + false, true, true, ); diff --git a/src/pages/app/__tests__/builder.skills.test.js b/src/pages/app/__tests__/builder.skills.test.js index 1677ac366..535b5284b 100644 --- a/src/pages/app/__tests__/builder.skills.test.js +++ b/src/pages/app/__tests__/builder.skills.test.js @@ -19,6 +19,7 @@ test('allows to drag & drop', async () => { const resumeId = DatabaseConstants.demoStateResume1Id; const { resume, mockDatabaseUpdateFunction } = await setupAndWait( resumeId, + false, true, true, ); diff --git a/src/pages/app/__tests__/dashboard.deleteResume.test.js b/src/pages/app/__tests__/dashboard.deleteResume.test.js index 1b84ac1d5..d4561f3d3 100644 --- a/src/pages/app/__tests__/dashboard.deleteResume.test.js +++ b/src/pages/app/__tests__/dashboard.deleteResume.test.js @@ -44,7 +44,7 @@ async function setup() { ); const resumeToDeleteId = resumeToDelete.id; const [undeletedResume] = Object.values(userResumes).filter( - (resume) => resume.id === DatabaseConstants.initialStateResumeId, + (resume) => resume.id === DatabaseConstants.initialStateResume1Id, ); const mockDatabaseRemoveFunction = jest.spyOn( diff --git a/src/pages/app/__tests__/helpers/builder.js b/src/pages/app/__tests__/helpers/builder.js index e63c93468..e9cdf4db9 100644 --- a/src/pages/app/__tests__/helpers/builder.js +++ b/src/pages/app/__tests__/helpers/builder.js @@ -80,6 +80,7 @@ const dragAndDropListItem = (listItemElement, direction) => { // eslint-disable-next-line no-underscore-dangle async function _setup( resumeId, + signInWithGoogle, waitForLoadingScreenToDisappear, waitForDatabaseUpdateToHaveCompleted, ) { @@ -96,7 +97,12 @@ async function _setup( 'update', ); - FirebaseStub.auth().signInAnonymously(); + if (signInWithGoogle) { + const provider = new FirebaseStub.auth.GoogleAuthProvider(); + FirebaseStub.auth().signInWithPopup(provider); + } else { + FirebaseStub.auth().signInAnonymously(); + } render( @@ -132,17 +138,19 @@ async function _setup( } async function setup(resumeId) { - const returnValue = await _setup(resumeId, false, false); + const returnValue = await _setup(resumeId, false, false, false); return returnValue; } async function setupAndWait( resumeId, + signInWithGoogle, waitForLoadingScreenToDisappear, waitForDatabaseUpdateToHaveCompleted, ) { const returnValue = await _setup( resumeId, + signInWithGoogle, waitForLoadingScreenToDisappear, waitForDatabaseUpdateToHaveCompleted, ); diff --git a/src/templates/__tests__/Castform.test.js b/src/templates/__tests__/Castform.test.js index 51cc84c0e..026636c88 100644 --- a/src/templates/__tests__/Castform.test.js +++ b/src/templates/__tests__/Castform.test.js @@ -21,7 +21,7 @@ async function setup(resumeId) { } test('renders correctly', async () => { - const resume = await setup(DatabaseConstants.initialStateResumeId); + const resume = await setup(DatabaseConstants.initialStateResume1Id); const { container } = render(); @@ -30,7 +30,7 @@ test('renders correctly', async () => { }); test('date of birth is not shown if not provided', async () => { - const resume = await setup(DatabaseConstants.initialStateResumeId); + const resume = await setup(DatabaseConstants.initialStateResume1Id); render(); @@ -38,7 +38,7 @@ test('date of birth is not shown if not provided', async () => { }); test('date of birth is shown if provided', async () => { - const resume = await setup(DatabaseConstants.initialStateResumeId); + const resume = await setup(DatabaseConstants.initialStateResume1Id); const birthDate = new Date(1990, 0, 20); const birthDateFormatted = '20 January 1990'; From a4d474b8ecedaa5d1c6515eeb6e80563dd51bf22 Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 25 May 2021 12:16:49 +0200 Subject: [PATCH 09/12] FirebaseStub: added more unit tests related to auth --- .../auth.signInAnonymously.test.js | 35 ++++++++++++ .../auth.signInWithPopup.test.js | 54 +++++++++++++++++++ .../auth.signOut.test.js | 29 ++++++++++ .../gatsby-plugin-firebase/auth.test.js | 38 ++++--------- __mocks__/gatsby-plugin-firebase/auth/auth.js | 4 +- 5 files changed, 131 insertions(+), 29 deletions(-) create mode 100644 __mocks__/__tests__/gatsby-plugin-firebase/auth.signInAnonymously.test.js create mode 100644 __mocks__/__tests__/gatsby-plugin-firebase/auth.signInWithPopup.test.js create mode 100644 __mocks__/__tests__/gatsby-plugin-firebase/auth.signOut.test.js diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInAnonymously.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInAnonymously.test.js new file mode 100644 index 000000000..d159d44ea --- /dev/null +++ b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInAnonymously.test.js @@ -0,0 +1,35 @@ +import FirebaseStub, { AuthConstants } from '../../gatsby-plugin-firebase'; + +test('sets current user to anonymous user 1', async () => { + await FirebaseStub.auth().signInAnonymously(); + + const { currentUser } = FirebaseStub.auth(); + expect(currentUser).toBeTruthy(); + expect(currentUser.uid).toEqual(AuthConstants.anonymousUser1.uid); +}); + +test('calls onAuthStateChanged observer with anonymous user 1', async () => { + let user = null; + let error = null; + FirebaseStub.auth().onAuthStateChanged( + (_user) => { + user = _user; + }, + (_error) => { + error = _error; + }, + ); + + await FirebaseStub.auth().signInAnonymously(); + + expect(user).toBeTruthy(); + expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid); + expect(error).toBeNull(); +}); + +test('returns anonymous user 1', async () => { + const user = await FirebaseStub.auth().signInAnonymously(); + + expect(user).toBeTruthy(); + expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid); +}); diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInWithPopup.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInWithPopup.test.js new file mode 100644 index 000000000..e20cd1cab --- /dev/null +++ b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signInWithPopup.test.js @@ -0,0 +1,54 @@ +import FirebaseStub, { AuthConstants } from '../../gatsby-plugin-firebase'; + +describe('with Google auth provider', () => { + test('sets current user to Google user 3', async () => { + await FirebaseStub.auth().signInWithPopup( + new FirebaseStub.auth.GoogleAuthProvider(), + ); + + const { currentUser } = FirebaseStub.auth(); + expect(currentUser).toBeTruthy(); + expect(currentUser.uid).toEqual(AuthConstants.googleUser3.uid); + }); + + test('sets current user provider data', async () => { + const provider = new FirebaseStub.auth.GoogleAuthProvider(); + await FirebaseStub.auth().signInWithPopup(provider); + + const { currentUser } = FirebaseStub.auth(); + expect(currentUser).toBeTruthy(); + expect(currentUser.providerData).toBeTruthy(); + expect(currentUser.providerData).toHaveLength(1); + expect(currentUser.providerData[0].providerId).toEqual(provider.providerId); + }); + + test('calls onAuthStateChanged observer with Google user 3', async () => { + let user = null; + let error = null; + FirebaseStub.auth().onAuthStateChanged( + (_user) => { + user = _user; + }, + (_error) => { + error = _error; + }, + ); + + await FirebaseStub.auth().signInWithPopup( + new FirebaseStub.auth.GoogleAuthProvider(), + ); + + expect(user).toBeTruthy(); + expect(user.uid).toEqual(AuthConstants.googleUser3.uid); + expect(error).toBeNull(); + }); + + test('returns Google user 3', async () => { + const user = await FirebaseStub.auth().signInWithPopup( + new FirebaseStub.auth.GoogleAuthProvider(), + ); + + expect(user).toBeTruthy(); + expect(user.uid).toEqual(AuthConstants.googleUser3.uid); + }); +}); diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/auth.signOut.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signOut.test.js new file mode 100644 index 000000000..06bd830bc --- /dev/null +++ b/__mocks__/__tests__/gatsby-plugin-firebase/auth.signOut.test.js @@ -0,0 +1,29 @@ +import FirebaseStub from '../../gatsby-plugin-firebase'; + +test('sets current user to null', async () => { + await FirebaseStub.auth().signInAnonymously(); + + await FirebaseStub.auth().signOut(); + + const { currentUser } = FirebaseStub.auth(); + expect(currentUser).toBeNull(); +}); + +test('calls onAuthStateChanged observer with null', async () => { + let user = null; + let error = null; + FirebaseStub.auth().onAuthStateChanged( + (_user) => { + user = _user; + }, + (_error) => { + error = _error; + }, + ); + await FirebaseStub.auth().signInAnonymously(); + + await FirebaseStub.auth().signOut(); + + expect(user).toBeNull(); + expect(error).toBeNull(); +}); diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js index ca18050dd..2ffd0aa3b 100644 --- a/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js +++ b/__mocks__/__tests__/gatsby-plugin-firebase/auth.test.js @@ -1,4 +1,4 @@ -import FirebaseStub, { AuthConstants } from '../../gatsby-plugin-firebase'; +import FirebaseStub from '../../gatsby-plugin-firebase'; test('reuses existing Auth instance', () => { const auth1 = FirebaseStub.auth(); @@ -9,32 +9,6 @@ test('reuses existing Auth instance', () => { expect(auth1.uuid).toEqual(auth2.uuid); }); -test('returns anonymous user 1 when signing in anonymously', async () => { - const user = await FirebaseStub.auth().signInAnonymously(); - - expect(user).toBeTruthy(); - expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid); -}); - -test('calls onAuthStateChanged observer with anonymous user 1 when signing in anonymously', async () => { - let user = null; - let error = null; - FirebaseStub.auth().onAuthStateChanged( - (_user) => { - user = _user; - }, - (_error) => { - error = _error; - }, - ); - - await FirebaseStub.auth().signInAnonymously(); - - expect(user).toBeTruthy(); - expect(user.uid).toEqual(AuthConstants.anonymousUser1.uid); - expect(error).toBeNull(); -}); - test('onAuthStateChanged unsubscribe removes observer', () => { const observer = () => {}; const unsubscribe = FirebaseStub.auth().onAuthStateChanged(observer); @@ -49,3 +23,13 @@ test('onAuthStateChanged unsubscribe removes observer', () => { FirebaseStub.auth().onAuthStateChangedObservers.indexOf(observer), ).not.toBeGreaterThanOrEqual(0); }); + +test('current user delete calls signOut', async () => { + const mockSignOut = jest.spyOn(FirebaseStub.auth(), 'signOut'); + await FirebaseStub.auth().signInAnonymously(); + const { currentUser } = FirebaseStub.auth(); + + await currentUser.delete(); + + expect(mockSignOut).toHaveBeenCalledTimes(1); +}); diff --git a/__mocks__/gatsby-plugin-firebase/auth/auth.js b/__mocks__/gatsby-plugin-firebase/auth/auth.js index b8217a012..b0c123ea5 100644 --- a/__mocks__/gatsby-plugin-firebase/auth/auth.js +++ b/__mocks__/gatsby-plugin-firebase/auth/auth.js @@ -60,7 +60,7 @@ class Auth { user.providerId, user.uid, user.isAnonymous, - this.signOut, + this.signOut.bind(this), ); await delay(Constants.defaultDelayInMilliseconds); @@ -98,7 +98,7 @@ class Auth { user.providerId, user.uid, user.isAnonymous, - this.signOut, + this.signOut.bind(this), ); await delay(Constants.defaultDelayInMilliseconds); From fb18b5c94cc219e0401d3b4304f3eaf5496938bf Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 25 May 2021 12:48:38 +0200 Subject: [PATCH 10/12] FirebaseStub: added more unit tests related to functions --- .../gatsby-plugin-firebase/functions.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 __mocks__/__tests__/gatsby-plugin-firebase/functions.test.js diff --git a/__mocks__/__tests__/gatsby-plugin-firebase/functions.test.js b/__mocks__/__tests__/gatsby-plugin-firebase/functions.test.js new file mode 100644 index 000000000..6cf12ab4c --- /dev/null +++ b/__mocks__/__tests__/gatsby-plugin-firebase/functions.test.js @@ -0,0 +1,19 @@ +import FirebaseStub from '../../gatsby-plugin-firebase'; + +test('reuses existing Functions instance', () => { + const functions1 = FirebaseStub.functions(); + const functions2 = FirebaseStub.functions(); + + expect(functions1.uuid).toBeTruthy(); + expect(functions2.uuid).toBeTruthy(); + expect(functions1.uuid).toEqual(functions2.uuid); +}); + +test('deleteUser function returns true', async () => { + const deleteUser = FirebaseStub.functions().httpsCallable('deleteUser'); + + const result = await deleteUser(); + + expect(result).toBeTruthy(); + expect(result.data).toEqual(true); +}); From 99f87ebb3e5840372b6584d55168759adf455698 Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Tue, 1 Jun 2021 12:14:12 +0200 Subject: [PATCH 11/12] Fixed Jest errors occurring after latest merge from upstream --- jest-preprocess.js | 2 +- jest.config.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/jest-preprocess.js b/jest-preprocess.js index c5af69317..52a941801 100644 --- a/jest-preprocess.js +++ b/jest-preprocess.js @@ -2,4 +2,4 @@ const babelOptions = { presets: ['babel-preset-gatsby'], }; -module.exports = require('babel-jest').createTransformer(babelOptions); +module.exports = require('babel-jest').default.createTransformer(babelOptions); diff --git a/jest.config.js b/jest.config.js index 2d83acea7..11a69f511 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,5 @@ +const esModules = ['gatsby', 'nanoevents'].join('|'); + module.exports = { testRegex: '/*.test.js$', collectCoverage: true, @@ -26,11 +28,12 @@ module.exports = { '.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `/__mocks__/file-mock.js`, }, testPathIgnorePatterns: [`node_modules`, `\\.cache`], - transformIgnorePatterns: [`node_modules/(?!(gatsby)/)`], + transformIgnorePatterns: [`node_modules/(?!${esModules})`], globals: { __PATH_PREFIX__: ``, }, testURL: `http://localhost`, setupFiles: [`/loadershim.js`], setupFilesAfterEnv: [`/jest.setup.js`], + testEnvironment: 'jsdom', }; From d71d378aa1075c1995dad67e0d51da9858b23c57 Mon Sep 17 00:00:00 2001 From: gianantoniopini <63844628+gianantoniopini@users.noreply.github.com> Date: Wed, 2 Jun 2021 15:02:36 +0200 Subject: [PATCH 12/12] Fixed Jest warnings occurring after latest merge from upstream --- __mocks__/gatsby.js | 56 +++++++++++++++------------------------------ 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/__mocks__/gatsby.js b/__mocks__/gatsby.js index bac68ffb9..996f2e576 100644 --- a/__mocks__/gatsby.js +++ b/__mocks__/gatsby.js @@ -4,24 +4,18 @@ import { delay } from '../src/utils/index'; const Gatsby = jest.requireActual('gatsby'); -const fluidImageShapes = [ - { - aspectRatio: 2, - src: 'test_image.jpg', - srcSet: 'some srcSet', - srcSetWebp: 'some srcSetWebp', - sizes: '(max-width: 600px) 100vw, 600px', - base64: 'string_of_base64', +const imageData = { + images: { + fallback: { + src: `image_src.jpg`, + srcSet: `image_src_set.jpg 1x`, + }, }, - { - aspectRatio: 3, - src: 'test_image_2.jpg', - srcSet: 'some other srcSet', - srcSetWebp: 'some other srcSetWebp', - sizes: '(max-width: 400px) 100vw, 400px', - base64: 'string_of_base64', - }, -]; + layout: `fixed`, + width: 1, + height: 2, +}; +const childImageSharp = { gatsbyImageData: imageData }; const useStaticQuery = () => ({ site: { @@ -33,39 +27,25 @@ const useStaticQuery = () => ({ }, }, file: { - childImageSharp: { - fluid: fluidImageShapes[0], - }, + childImageSharp, }, onyx: { - childImageSharp: { - fluid: fluidImageShapes[0], - }, + childImageSharp, }, pikachu: { - childImageSharp: { - fluid: fluidImageShapes[1], - }, + childImageSharp, }, gengar: { - childImageSharp: { - fluid: fluidImageShapes[0], - }, + childImageSharp, }, castform: { - childImageSharp: { - fluid: fluidImageShapes[1], - }, + childImageSharp, }, glalie: { - childImageSharp: { - fluid: fluidImageShapes[0], - }, + childImageSharp, }, celebi: { - childImageSharp: { - fluid: fluidImageShapes[1], - }, + childImageSharp, }, });