How to test Firebase login action (React/Jest) - javascript

I'm trying to create a test that will see if signIn has been called, then proceed to the success and error function testing.
I'm using the firebase-mock package here:
https://github.com/soumak77/firebase-mock/blob/master/tutorials/auth/authentication.md
Below is my Login action
// Sign in action
export const signIn = (email, password, redirectUrl = ROUTEPATH_DEFAULT_PAGE) => (dispatch) => {
dispatch({ type: USER_LOGIN_PENDING });
firebase
.then(auth => auth.signInWithEmailAndPassword(email, password))
.catch((e) => {
console.error('actions/Login/signIn', e);
// Register a new user
if (e.code === LOGIN_USER_NOT_FOUND) {
dispatch(push(ROUTEPATH_FORBIDDEN));
dispatch(toggleNotification(true, e.message, 'error'));
} else {
dispatch(displayError(true, e.message));
setTimeout(() => {
dispatch(displayError(false, ''));
}, 5000);
throw e;
}
})
.then(res => res.getIdToken())
.then((idToken) => {
if (!idToken) {
dispatch(displayError(true, 'Sorry, there was an issue with getting your token.'));
}
dispatch(onCheckAuth(email));
dispatch(push(redirectUrl));
});
};
My test:
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { MockFirebase } from 'firebase-mock';
// Login Actions
import { onCheckAuth, signIn } from 'actions';
// String Constants
import { LOGIN_USER_NOT_FOUND } from 'copy';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
let mockProps;
describe('login actions', () => {
// console.log('MockFirebase', MockFirebase);
// console.log('onCheckAuth', onCheckAuth);
let mockAuth;
beforeEach(() => {
mockAuth = new MockFirebase();
console.log('mockAuth: ==>', mockAuth);
mockProps = {
signIn: jest.fn(),
signOut: jest.fn(),
checkAuth: jest.fn(),
createUser: jest.fn(),
resetPassword: jest.fn(),
verifyEmail: jest.fn()
};
});
it('signIn should be called', () => {
const user = {
email: 'first.last#yum.com',
password: 'abd123'
};
signIn(user.email, user.password);
console.log('signIn', signIn);
expect(signIn).toHaveBeenCalled();
});
});
Error message
FAIL client/actions/Login/index.test.js
● login actions β€Ί signIn should be called
expect(jest.fn())[.not].toHaveBeenCalled()
jest.fn() value must be a mock function or spy.
Received:
function: [Function signIn]
at Object.<anonymous> (client/actions/Login/index.test.js:71:29)

I was incorrectly mocking the firebase services function, below is code I got working, however running into a new issue posted here: How to test is code inside of thenable in jest test is getting called?
The following test passes, however not sure that the code inside of the store.dispatch is thenable...
// Mock all the exports in the module.
function mockFirebaseService() {
return new Promise(resolve => resolve(true));
}
// Since "services/firebase" is a dependency on this file that we are testing,
// we need to mock the child dependency.
jest.mock('services/firebase', () => new Promise(resolve => resolve(true)));
describe('login actions', () => {
let store;
beforeEach(() => {
store = mockStore({});
});
it('signIn should call firebase', () => {
const user = {
email: 'first.last#yum.com',
password: 'abd123'
};
store.dispatch(signIn(user.email, user.password)).then(() => {
expect(mockFirebaseService).toHaveBeenCalled();
});
});
});

Related

Jest mocked promise function return undefined

This is my helper function in src/utils/calls/aws.js
export const loginCognito = (cognitoUser, authenticationDetails) => {
return new Promise((resolve, reject) => {
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: resolve,
onFailure: reject,
newPasswordRequired: resolve,
});
});
};
This is my test file:
import { fireEvent, screen, waitFor } from "#testing-library/react";
import { LoginPage } from "../../../views/LoginPage/LoginPage";
import { renderWithProviders } from "../../__test-utils__/test-utils";
// eslint-disable-next-line jest/no-mocks-import
import { LOCALSTORAGE_ACCESS_TOKEN } from "../../../utils/constants/constant";
const accessToken = "an.access.token";
const promisedResult = {
"idToken": {
"jwtToken": "an.access.token",
"payload": {
"sub": "354548-5454-c59637523bfd",
"email_verified": true,
"iss": "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_tomahwaf",
"cognito:username": "john-doe",
"preferred_username": "john-doe",
"aud": "client.id.aws",
"custom:dataInizioContratto": "01/01/2020",
"event_id": "ee5626d5-ffc2-4ecf-89df-2e1a83c0c174",
"token_use": "id",
"auth_time": 1670256322,
"custom:azienda": "acme",
"exp": 1670259922,
"iat": 1670256322,
"email": "info#example.com"
}
},
"refreshToken": {
"token": "a.refresh.token"
},
"accessToken": {
"jwtToken": "an.access.token",
"payload": {
"sub": "354548-5454-c59637523bfd",
"event_id": "ee5626d5-ffc2-4ecf-89df-2e1a83c0c174",
"token_use": "access",
"scope": "aws.cognito.signin.user.admin",
"auth_time": 1670256322,
"iss": "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_tomahwaf",
"exp": 1670259922,
"iat": 1670256322,
"jti": "083d4b9a-5042-485b-bb65-c66e8ae5b921",
"client_id": "client.id.aws",
"username": "john-doe"
}
},
"clockDrift": 0
};
jest.mock("../../../utils/calls/aws.js", () => ({
//loginCognito: jest.fn().mockReturnValue(Promise.resolve({accessToken: "an.access.token"}))
//loginCognito: jest.fn(() => Promise.resolve(promisedResult))
//loginCognito: jest.fn(() => promisedResult)
loginCognito: jest.fn().mockResolvedValue(promisedResult)
}));
describe("Test LoginPage", () => {
it("Can press button without userEvent", async () => {
renderWithProviders(<LoginPage />);
const inputUsername = screen.getByLabelText(/username/i);
fireEvent.change(inputUsername, {
target: {
value: "info#example.com",
},
});
const inputPassword = screen.getByLabelText(/password/i);
fireEvent.change(inputPassword, {
target: {
value: "thisIsASecretPassword",
},
});
const loginButton = screen.getByRole("button", { name: /login/i });
fireEvent.click(loginButton);
await waitFor(() => {
expect(localStorage.getItem(LOCALSTORAGE_ACCESS_TOKEN)).toEqual(
accessToken
);
});
});
});
In the real file (LoginPage) I have
const handleClick = async () => {
// Fetch the data from cognito
const {cognitoUser, authenticationDetails} = loginCognitoUser(currentItem);
// Call AWS
try {
const isLogged = await loginCognito(cognitoUser, authenticationDetails);
console.log(">>>>>>>>>>>>>>>>>>>", JSON.stringify(isLogged))
} catch (e) {
console.log(e)
}
};
For real implementation it works, but when I call from the test I get
console.log
>>>>>>>>>>>>>>>>>>> undefined
at handleClick (src/views/LoginPage/LoginPage.js:52:15)
Doesn't mock of a function "bypasses" it returning what I want?
I have made 4 tests, I don't know anymore what to do:
//loginCognito: jest.fn().mockReturnValue(Promise.resolve({accessToken: "an.access.token"}))
//loginCognito: jest.fn(() => Promise.resolve(promisedResult))
//loginCognito: jest.fn(() => promisedResult)
loginCognito: jest.fn().mockResolvedValue(promisedResult)
I tried add a console.log inside my aws function and... If I remove the mocked I have the console.log, so I think that function is correctly mocked but cannot have the return.
Edit nr. 2
I tried another thing:
export const loginCognito = () => {
return "ciao"
}
And in test:
jest.mock("../../../utils/calls/aws.js", () => ({
loginCognito: jest.fn().mockReturnValue("addio")
}));
The page under test has console.log undefined!
try {
//const isLogged = await loginCognito(cognitoUser, authenticationDetails);
const isLogged = loginCognito();
console.log("==========", isLogged)
} catch (e) {
console.log(e)
}
Am I that I don't know how mock it works?
Console
console.log
========== undefined
at handleClick (src/views/LoginPage/LoginPage.js:55:15)
I need to bypass totally the AWS funciton...
I can't really speak for why your code is not working, but this is how I would go about writing your test:
// import the function you want to mock
import { loginCognito } from "../../../utils/calls/aws.js";
// tell jest to mock your module
jest.mock("../../../utils/calls/aws.js");
// before each test, mock the implementation
beforeEach(() => {
loginCognito.mockResolvedValue(...);
});
// cleanup after each test
afterEach(() => {
loginCognito.mockClear(...);
});
If you're using typescript, you will need to make typescript aware that the method is mocked. This is done by casting the method, which is mostly a tedium required for typescript. As a matter of convention, I usually put the work "mock" in front of the name:
const mockLoginCongnito = loginCognito as jest.MockedFunction<typeof loginCognito>;

The test with JestJS that closes the application does not work

Friends, I have a problem!
I'm testing my API with NestJS.
I'm using Jest for the test. Unfortunately, I am encountering the following error:
TypeError: Cannot read properties of undefined (reading 'close')
This error is very explicit but I don't see where it could come from.
Would you have an idea?
My current code :
import * as pactum from 'pactum';
import { Test } from '#nestjs/testing';
import { AppModule } from '../src/app.module';
import { INestApplication, ValidationPipe } from '#nestjs/common';
import { PrismaService } from '../src/prisma/prisma.service';
import { AuthDto } from '../src/auth/dto';
describe('App e2e', () => {
let app: INestApplication;
let prisma: PrismaService;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
const app = moduleRef.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
await app.init();
await app.listen(3334);
prisma = app.get(PrismaService);
await prisma.cleanDatabase();
});
afterAll(async () => {
console.log('Closing server');
await app.close(); // <------------- THE PROBLEM ARISES HERE.
});
describe('Auth', () => {
describe('Signup', () => {
it('should signup a user', () => {
const dto: AuthDto = {
email: 'darrel.doe#mail.com',
password: '1234',
};
return pactum
.spec()
.post('http://localhost:3333/auth/signup')
.withBody(dto)
.expectStatus(201);
});
});
describe('Signin', () => {
it.todo('should signin a user');
});
});
});
You are mixing two variables in different scopes.
let app: INestApplication; is upper scope one which you are actually using, since it does not have any value assigned it is undefined. The inner one is different because you are defining inside another scope.
A solution is very simple, just remove const from const app = moduleRef.createNestApplication();

In Jest, mock the constructor of a class instantiated in the tested function

I want to test getSessionStorage().
Inside getSessionStorage() I'm calling new RedisStore(process.env.REDIS_URL).
This throws an error because process.env.REDIS_URL is not accessible outside a vpn.
How can I mock RedisStore.constructor to avoid calling this.client.connect(); and thus avoid the error?
RedisStore.js
import { createClient } from "redis";
class RedisStore {
/**
* #param {string} url
*/
constructor(url) {
this.client = createClient({ url });
this.client.on("error", (err) => console.log("Redis Client Error", err));
this.client.connect();
}
async storeCallback(session) {}
async loadCallback(id) {}
async deleteCallback(id) {}
}
export default RedisStore;
getSessionStorage.js
import RedisStore from "./RedisStore";
const getSessionStorage = ()=> {
return new RedisStore(process.env.REDIS_URL);
}
export default getSessionStorage;
getSessionStorage.test.js
import getSessionStorage from "./getSessionStorage.js";
describe("getSessionStorage", () => {
it("should pass", () => {
expect(getSessionStorage()).toMatchObject({
storeCallback: expect.any(Function),
loadCallback: expect.any(Function),
deleteCallback: expect.any(Function)
});
});
});
You can mock redis:
jest.mock('redis', () => ({
createClient : jest.fn().mockReturnValue({
on: jest.fn(),
connect: jest.fn()
}),
}));

Testing firebase functions in vuejs

I want to unit test my vue components. Since I'm working with firebase this is a little bit difficult.
Fir of all, I created a __mocks__ folder to contain all my mocked functions. Inside that folder, I've created firebase.js:
import * as firebase from 'firebase';
const onAuthStateChanged = jest.fn();
const getRedirectResult = jest.fn(() => Promise.resolve({
user: {
displayName: 'redirectResultTestDisplayName',
email: 'redirectTest#test.com',
emailVerified: true,
},
}));
const sendEmailVerification = jest.fn(() => Promise.resolve('result of sendEmailVerification'));
const sendPasswordResetEmail = jest.fn(() => Promise.resolve());
const createUserWithEmailAndPassword = jest.fn(() => {
console.log('heeeeelllo');
Promise.resolve({
user: {
displayName: 'redirectResultTestDisplayName',
email: 'redirectTest#test.com',
emailVerified: true,
},
});
});
const signInWithEmailAndPassword = jest.fn(() => Promise.resolve('result of signInWithEmailAndPassword'));
const signInWithRedirect = jest.fn(() => Promise.resolve('result of signInWithRedirect'));
const initializeApp = jest // eslint-disable-line no-unused-vars
.spyOn(firebase, 'initializeApp')
.mockImplementation(() => ({
auth: () => ({
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
currentUser: {
sendEmailVerification,
},
signInWithRedirect,
}),
}));
jest.spyOn(firebase, 'auth').mockImplementation(() => ({
onAuthStateChanged,
currentUser: {
displayName: 'testDisplayName',
email: 'test#test.com',
emailVerified: true,
},
getRedirectResult,
sendPasswordResetEmail,
}));
firebase.auth.FacebookAuthProvider = jest.fn(() => {});
firebase.auth.GoogleAuthProvider = jest.fn(() => {});
This file, I took from: https://github.com/mrbenhowl/mocking-firebase-initializeApp-and-firebase-auth-using-jest
The component I want to test is called EmailSignupLogin. In this particular case, I want to test the registerViaEmail-method:
methods: {
registerViaEmail() {
if (this.password.length > 0 && this.password === this.passwordReenter) {
firebase.auth().createUserWithEmailAndPassword(this.emailAdress, this.password).then((result) => {
const { user } = result;
console.log(result);
this.setUser(user);
this.$router.push('/stocks');
}).catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
this.error = errorMessage;
console.error(errorCode, errorMessage);
});
} else {
this.error = 'passwords not matching';
}
},
},
Now to my test file(email-signup-login.spec.js):
import { mount } from '#vue/test-utils';
import Vue from 'vue';
import EmailSignupLogin from '#/components/email-signup-login';
jest.mock('../../__mocks__/firebase');
describe('EmailSignupLogin', () => {
let wrapper;
const mockFunction = jest.fn();
beforeEach(() => {
wrapper = mount(EmailSignupLogin, {
data() {
return {
password: '123456',
passwordReenter: '123456',
emailAdress: 'test#test.com',
};
},
store: {
actions: {
setUser: mockFunction,
},
},
});
});
describe('methods', () => {
describe('#registerViaEmail', () => {
it('calls mockFunction', async () => {
await wrapper.vm.registerViaEmail();
expect(mockFunction).toHaveBeenCalled();
});
});
});
});
Inside the registerViaEmail-method I call the setUser-action, which is a vuex-action.
The problem is that it doesn't seem to call my mocked functions from __mocks__/firebase.js. Can somebody please tell me why?
Several issues turned up in your code:
registerViaEmail() is not async (not returning a Promise), so the await call returns prematurely, at which point your test tries to assert something that hasn't occurred yet. To resolve this, just wrap the function body with a Promise:
registerViaEmail() {
return new Promise((resolve, reject) => {
if (this.password.length > 0 && this.password === this.passwordReenter) {
firebase.auth().createUserWithEmailAndPassword(this.emailAdress, this.password).then((result) => {
//...
resolve()
}).catch((error) => {
//...
reject()
});
} else {
//...
reject()
}
})
},
The script you referred to is not intended to be used with Jest __mocks__. The script itself directly modifies the firebase object, replacing its methods/properties with mocks. To use the script, you just need to import it before importing the test module that uses firebase:
import './firebase-mock' // <-- order important
import EmailSignupLogin from '#/components/EmailSignupLogin'
createUserWithEmailAndPassword does not return anything. It looks like it originally returned the Promise, but you modified it with a console.log, and forgot to continue returning the Promise, which prevented this method from being awaited (same issue as #1). The solution there is to return the Promise:
const createUserWithEmailAndPassword = jest.fn(() => {
console.log('heeeeelllo')
return /*πŸ‘ˆ*/ Promise.resolve(/*...*/)
})
createUserWithEmailAndPassword is the method to be tested in EmailSignupLogin, but it's currently not mocked in your auth mock object. It's only mocked in the return of initializeApp.auth, but that's not what it being used in EmailSignupLogin. To resolve the issue, copy createUserWithEmailAndPassword to your auth mock object:
jest.spyOn(firebase, 'auth').mockImplementation(() => ({
onAuthStateChanged,
currentUser: {
displayName: 'testDisplayName',
email: 'test#test.com',
emailVerified: true,
},
getRedirectResult,
sendPasswordResetEmail,
createUserWithEmailAndPassword, //πŸ‘ˆ
}));
In your test setup, you mocked the store with a plain object, but it actually needs to be an instance of Vuex.Store:
mount({
//store: { /*...*/ }, //❌DON'T DO THIS
store: new Vuex.Store({ /*...*/ }) //βœ…
})
Github demo

Vuex: Testing actions with API calls

I have been following these testing guidelines to test my vuex store.
But when I touched upon the actions part, I felt there is a lot going on that I couldn't understand.
The first part goes like:
// actions.js
import shop from '../api/shop'
export const getAllProducts = ({ commit }) => {
commit('REQUEST_PRODUCTS')
shop.getProducts(products => {
commit('RECEIVE_PRODUCTS', products)
})
}
// actions.spec.js
// use require syntax for inline loaders.
// with inject-loader, this returns a module factory
// that allows us to inject mocked dependencies.
import { expect } from 'chai'
const actionsInjector = require('inject!./actions')
// create the module with our mocks
const actions = actionsInjector({
'../api/shop': {
getProducts (cb) {
setTimeout(() => {
cb([ /* mocked response */ ])
}, 100)
}
}
})
I infer that this is to mock the service inside the action.
The part which follows is:
// helper for testing action with expected mutations
const testAction = (action, payload, state, expectedMutations, done) => {
let count = 0
// mock commit
const commit = (type, payload) => {
const mutation = expectedMutations[count]
expect(mutation.type).to.equal(type)
if (payload) {
expect(mutation.payload).to.deep.equal(payload)
}
count++
if (count >= expectedMutations.length) {
done()
}
}
// call the action with mocked store and arguments
action({ commit, state }, payload)
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
expect(count).to.equal(0)
done()
}
}
describe('actions', () => {
it('getAllProducts', done => {
testAction(actions.getAllProducts, null, {}, [
{ type: 'REQUEST_PRODUCTS' },
{ type: 'RECEIVE_PRODUCTS', payload: { /* mocked response */ } }
], done)
})
})
This is where it I find it difficult to follow.
My store looks like:
import * as NameSpace from '../NameSpace'
import { ParseService } from '../../Services/parse'
const state = {
[NameSpace.AUTH_STATE]: {
auth: {},
error: null
}
}
const getters = {
[NameSpace.AUTH_GETTER]: state => {
return state[NameSpace.AUTH_STATE]
}
}
const mutations = {
[NameSpace.AUTH_MUTATION]: (state, payload) => {
state[NameSpace.AUTH_STATE] = payload
}
}
const actions = {
[NameSpace.ASYNC_AUTH_ACTION]: ({ commit }, payload) => {
ParseService.login(payload.username, payload.password)
.then((user) => {
commit(NameSpace.AUTH_MUTATION, {auth: user, error: null})
})
.catch((error) => {
commit(NameSpace.AUTH_MUTATION, {auth: [], error: error})
})
}
}
export default {
state,
getters,
mutations,
actions
}
And This is how I am trying to test:
import * as NameSpace from 'src/store/NameSpace'
import AuthStore from 'src/store/modules/authorization'
const actionsInjector = require('inject!../../../../../src/store/modules/authorization')
// This file is present at: test/unit/specs/store/modules/authorization.spec.js
// src and test are siblings
describe('AuthStore Actions', () => {
const injectedAction = actionsInjector({
'../../Services/parse': {
login (username, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve({})
} else {
reject({})
}
}, 300)
})
}
}
})
it('Gets the user profile if the username and password matches', () => {
const testAction = (action, payload, state, mutations, done) => {
const commit = (payload) => {
if (payload) {
expect(mutations.payload).to.deep.equal(payload)
}
}
action({ commit, state }, payload)
.then(result => {
expect(state).to.deep.equal({auth: result, error: null})
})
.catch(error => {
expect(state).to.deep.equal({auth: [], error: error})
})
}
testAction(injectedAction.login, null, {}, [])
})
})
If I try to do this, I get:
"Gets the user profile if the username and password matches"
undefined is not a constructor (evaluating 'action({ commit: commit, state: state }, payload)')
"testAction#webpack:///test/unit/specs/store/modules/authorization.spec.js:96:13 <- index.js:26198:14
webpack:///test/unit/specs/store/modules/authorization.spec.js:104:15 <- index.js:26204:16"
I need help understanding what am I supposed to do to test such actions.
I know it's been awhile but I came across this question because I was having a similar problem. If you were to console.log injectedActions right before you make the testAction call you'd see that the injectedAction object actually looks like:
Object{default: Object{FUNC_NAME: function FUNC_NAME(_ref) { ... }}}
So the main solution here would be changing the testAction call to:
testAction(injectedAction.default.login, null, {}, [], done)
because you are exporting your action as defaults in your store.
A few other issues that are unrelated to your particular error... You do not need to manipulate the testAction boilerplate code. It will work as expected so long as you pass in the proper parameters. Also, be sure to pass done to testAction or your test will timeout. Hope this helps somebody else who comes across this!

Categories