connecting to firestore emulator with #firebase/testing - firebase

I am trying to test a firebase app locally.
I am running the test with firebase emulators:exec --only firestore 'mocha -r ts-node/register src/**/*.spec.ts
In my spec, I import #firebase/testing and setup my app and followed the directions from
https://firebase.google.com/docs/rules/unit-tests
I have a FirebaseService which is a singleton wrapper for my methods into which I inject my firebase app.
In production, I'll inject the firebase, and it gets initialized in the FirebaseService in testing, I initialize outside of the service.
The wrapper is fairly simple
export const FirebaseService = (function(): FirebaseSrvc {
let firebase;
const fbServiceObj: FirebaseSrvc = {
getInstance: (firebaseConfig, firebaseCore, initialize) => {
firebase = firebaseCore;
if (initialize && firebase.apps.length === 0) {
firebase.initializeApp(firebaseConfig);
}
return fbServiceObj;
},
createActivity: async (title: string) => {
try {
const firebaseUid = firebase.auth().currentuser.uid;
const newActivity: ActivityProps = {
title,
created_at: 123445,
created_by: firebaseUid,
public: false,
available_to: [firebaseUid],
};
console.log(' before create', newActivity);
const createResponse = await firebase
.firestore()
.collection('activities')
.doc(stringToSafeId(title))
.set(newActivity);
console.log('create response', createResponse);
return true;
} catch (e) {
console.log('error creating activity', e);
}
},
getActivity: async (title: string): Promise<ActivityProps> => {
try {
const actResponse: DocumentReferenceTo<ActivityProps> = await firebase
.firestore()
.collection('activities')
.doc(stringToSafeId(title))
.get();
return actResponse as ActivityProps;
} catch (e) {
console.log('error getting activity from firebase', e);
}
},
};
return fbServiceObj;
})();
The test I am attempting to run is
import * as firebase from '#firebase/testing';
import { assert } from 'chai';
import 'mocha';
import * as appConfig from '../../app-dev.json';
import { FirebaseService } from '../services/FirebaseService';
firebase.initializeTestApp({ ...appConfig.expo.extra.firebase, auth: { uid: 'random', email: 'test#test.com' } });
describe('Activity', async () => {
const fb = FirebaseService.getInstance(appConfig.expo.extra.firebase, testApp, false);
const activityData = new Activity(fb);
beforeEach(async () => await firebase.clearFirestoreData({ projectId }));
it('should create a new activity', async () => {
await activityData.set('test-activity'); // this runs FirebaseService.createActivity
const findActivity = await activityData.get('test-activity'); // this run FirebaseService.getActivity
assert(findActivity.title === 'test-activity');
});
});
When I run the test I get an error
Your API key is invalid, please check you have copied it correctly.] {
code: 'auth/invalid-api-key',
message: 'Your API key is invalid, please check you have copied it correctly.'
}
I can confirm that the API key which is passed into firebase.initializeTestApp matches the Web API Key in my firebase console.
I have also downloaded the google-services.json from my firebase console and lists
{
"api_key": [
{ "current_key": different_from_web_key}
]
}
And I have replaced my existing key with this new key, I still get the same error.
I have also tried setting up initializeTestApp({ projectId }) which is how the example from firebase docs sets it up, and I receive the same result.
I am using the same project details to run a project locally in android studio, and I am able to authenticate and write to firestore, so the API key I am using does work, but it appears to have issues being used in the test app.

This usually doesn't have a specific way to solve it. It might be that even a new copy and paste of the API key to the parameters, might make it work and the error to disappear.
I would recommend you to take a look at the following posts from the Community, that have some possible fixes for the error that you are facing.
Firebase Error: auth/invalid-api-key, Your API key is invalid, please check you have copied it correctly
Invalid API Key supplied using Firebase
In addition to that, since Firebase has free support offers, I think you reaching out to the Firebase support would help you fix this quickly. You should be able to contact directly for free.
Let me know if the information helped you!

Related

RESTful API hosting on GCP

I have this multi layered application entirely hosted on GCP. At the moment, we only have the back-end part. Front-end and API are to be developed. For the front-end, the decision has been made - it will be a React.js app hosted on Firebase Hosting and the authentication method will be Email/password and users will be provisioned manually through the Firebase Hosting UI.
As we'd like to have a re-usable middle layer (API) we're in a process of making a decision what type of a solution to be used for our middle layer. The main request here is only logged in users to be able to call the API endpoints. Eventually, there will be also a native/mobile application which will have to also be able to make authenticated requests to the API.
My question here is, what type of GCP service is advised to pick here? I want it to be light, scalable and price optimized. Preferred programming language would be C# but Node.js would be also acceptable.
Firebase Functions would work well for this authenticated API. With a function, you can simply check for the existence of context.auth.uid before proceeding with the API call.
https://firebase.google.com/docs/functions/callable
You'll want to use the .onCall() method to access this context.auth object.
Here's an example I took from one of my active Firebase projects which uses this concept:
Inside your functions>src folder, create a new function doAuthenticatedThing.ts
/**
* A Firebase Function that can be called from your React Firebase client UI
*/
import * as functions from 'firebase-functions';
import { initializeApp } from 'firebase/app';
import { connectFirestoreEmulator, getFirestore, getDocs, query, where, collection } from 'firebase/firestore';
import firebaseConfig from './firebase-config.json';
let isEmulator = false;
const doAuthenticatedThing = functions
.region('us-west1')
.runWith({
enforceAppCheck: true,
memory: '256MB',
})
.https.onCall(async (_data, context) => {
// disable if you don't use app-check verify (you probably should)
if (context.app == undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called from an App Check verified app.',
);
}
// checks for a firebase authenticated frontend user
if (context.auth == undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The user must be authenticated.',
);
}
// establish firestore db for queries
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
// start the emulator
if (process.env.MODE === 'development' && !isEmulator) {
connectFirestoreEmulator(db, '127.0.0.1', 6060);
isEmulator = true;
}
// obtain the user's firebase auth UID
const uuid = context?.auth?.uid as string;
// do some database stuff
const ref = collection(db, 'collection-name');
const q = query(ref, where(uuid, '==', uuid));
const results = await getDocs(q);
if (results.empty) {
throw new functions.https.HttpsError(
'internal',
'There were no results found!',
);
}
// prepare document data
const data: Array<any> = [];
// gather chats, and an array of all chat uids
results.forEach((d) => {
data.push({ id: d.id, data: d.data() });
});
return data;
});
export default doAuthenticatedThing;
Make sure to reference this new Firebase Function in the functions/src/index.ts file.
import doAuthenticatedThingFn from './doAuthenticatedThing';
export const doAuthenticatedThing = doAuthenticatedThingFn;
Create a frontend React Hook so any component can use any function you make. Call it useGetFunction.ts
import { getApp } from 'firebase/app';
import { getFunctions, HttpsCallable, httpsCallable } from '#firebase/functions';
const useGetFunction = (functionName: string): HttpsCallable<unknown, unknown> => {
const app = getApp();
const region = 'us-west1';
const functions = getFunctions(app, region);
return httpsCallable(functions, functionName);
};
export default useGetFunction;
Now you can simply get this function and use it in any React component:
const SomeComponent = () => {
const doAuthenticatedThing = useGetFunction('doAuthenticatedThing');
useEffect(() => {
(async () => {
const results = await doAuthenticatedThing();
})();
}, []);
};

Error when setting up Firebase automated backup with Google Cloud Functions

I'm trying to setup automatic backup of my Firestore using instructions here: https://firebase.google.com/docs/firestore/solutions/schedule-export
I get error:
firestoreExpert
g2o6pmdwatdp
TypeError: Cannot read properties of undefined (reading 'charCodeAt')
at peg$parsetemplate (/workspace/node_modules/google-gax/build/src/pathTemplateParser.js:304:17)
at Object.peg$parse [as parse] (/workspace/node_modules/google-gax/build/src/pathTemplateParser.js:633:18)
at new PathTemplate (/workspace/node_modules/google-gax/build/src/pathTemplate.js:55:54)
Any suggestions to debug this?
I've tried looking for errors in my permissions. E.g. I don't know how to check if the service has access to the specific bucket, although the GCL ran OK.
I've also tried looking for errors in the script.
index.js
const firestore = require('#google-cloud/firestore');
const client = new firestore.v1.FirestoreAdminClient();
// Replace BUCKET_NAME
const bucket = 'gs://EDITEDHERE.appspot.com'
exports.scheduledFirestoreExport = (event, context) => {
const databaseName = client.databasePath(
process.env.GCLOUD_PROJECT,
'(default)'
);
return client
.exportDocuments({
name: databaseName,
outputUriPrefix: bucket,
// Leave collectionIds empty to export all collections
// or define a list of collection IDs:
// collectionIds: ['users', 'posts']
collectionIds: [],
})
.then(responses => {
const response = responses[0];
console.log(`Operation Name: ${response['name']}`);
return response;
})
.catch(err => {
console.error(err);
});
};
and package.json
{
"dependencies": {
"#google-cloud/firestore": "^1.3.0"
}
}
I found these great video tutorials
How to schedule firestorm backups and
How To Transfer Firestore Data From One Project To Another

Create a user programatically using Firebase Auth emulator

I am trying to write jest tests using the Firebase Auth emulator and continue to receive the following CORS error.
console.error
Error: Headers X-Client-Version forbidden
at dispatchError (/Users/me/my-project/node_modules/jsdom/lib/jsdom/living/xhr/xhr-utils.js:62:19)
at validCORSPreflightHeaders (/Users/me/my-project/node_modules/jsdom/lib/jsdom/living/xhr/xhr-utils.js:99:5)
at Request.<anonymous> (/Users/me/my-project/node_modules/jsdom/lib/jsdom/living/xhr/xhr-utils.js:367:12)
at Request.emit (events.js:315:20)
at Request.onRequestResponse (/Users/me/my-project/node_modules/request/request.js:1059:10)
at ClientRequest.emit (events.js:315:20)
at HTTPParser.parserOnIncomingClient [as onIncoming] (_http_client.js:641:27)
at HTTPParser.parserOnHeadersComplete (_http_common.js:126:17)
at Socket.socketOnData (_http_client.js:509:22)
at Socket.emit (events.js:315:20) undefined
The test is very simple:
import { renderHook, act } from "#testing-library/react-hooks"
import faker from "faker"
import { useAuth, FirebaseProvider, firebase } from "./index"
const wrapper = ({ firebase, children }) => {
return <FirebaseProvider firebase={firebase}>{children}</FirebaseProvider>
}
const createUser = ({ email = faker.internet.email(), password = faker.internet.password({ length: 6 }) } = {}) => {
return firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(user => user)
}
const signUserIn = ({ email, password } = {}) => {
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(user => user)
}
describe("useAuth", () => {
it("will return the user", async () => {
const { result } = renderHook(() => useAuth(), { wrapper, initialProps: { firebase } })
const email = faker.internet.email()
const password = faker.internet.password()
await act(async () => {
const user = await createUser({ email, password }) // this fails
await signUserIn({ email, password }) //and so does this
})
expect(result.user).toEqual({ email, password })
})
})
And for reference, the index file:
const FirebaseProvider = ({ children, firebase }) => {
const firestore = firebase.firestore()
const auth = firebase.auth()
if (useEmulator()) {
firestore.useEmulator("localhost", 8080)
auth.useEmulator("http://localhost:9099/")
}
const value = { firestore, auth }
return <FirebaseContext.Provider value={value}>{children}</FirebaseContext.Provider>
}
const throwError = hook => {
throw new Error(`${hook} must be used within a FirebaseProvider`)
}
const useAuth = () => {
const context = useContext(FirebaseContext)
if (context === undefined) throwError("useAuth")
const [user, setUser] = useState()
useEffect(() => {
const cleanup = context.auth.onAuthStateChanged(authUser => {
authUser ? setUser(authUser) : setUser(null)
})
return () => cleanup()
})
return { ...context.auth, user }
}
I have tried using the REST endpoint that the actual emulator uses (below) and it errors in the same way.
http://localhost:9099/identitytoolkit.googleapis.com/v1/projects/<my-project>/accounts
Is there anyway to get this to run when using jest? Or do I need to create the accounts using the emulator UI, export them and re-import when I am running tests?
I have found I can use the REST endpoint below to make a user in the test, however it bypasses the emulator and makes a real user.
https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=<api-key>
Update jsdom version 16.5.2
This new version now supports wildcards for access-control-allow-headers, so updating to this version or using it as resolution, for projects created with Create React App, solves the problem.
Solution for jsdom prior to version 16.5.2
The error is thrown by jsdom because it doesn't support wildcard for access-control-allow-headers, but firebase uses the wildcard (see this issue for jsdom and this pull request related to firebase). There are two open pull requests to fix this issue: https://github.com/jsdom/jsdom/pull/3073 and https://github.com/jsdom/jsdom/pull/2867.
The issue can be fixed by either changing the relevant code manually in the node_modules folder or by using the fork as dependency in the package.json:
"jsdom": "silviot/jsdom#fix/allow-headers"
If jsdom isn't a direct dependency, then you can add the following to the package.json at the top level:
"resolutions": {
"jsdom": "silviot/jsdom#fix/allow-headers"
}
If the fork is used there are some auto-generated files missing in the jsdom folder. These can be generated by running npm install or yarn install in the folder. To automate this you can add a prepare script to the package.json:
"scripts": {
"prepare": "cd node_modules/jsdom && yarn"
},
I also had problems making users programaticly in the firebase auth emulator.
Instead of using
https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY]
You have to use the following format:
http://localhost:9099/identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY]
Then giving a JSON body like this, and hit post
{
"email": "test#test.com",
"password": "test12"
}
And voila! You have a user in your emulator. Combine this with fetch or axios and you seed your emulator with users. If you need to add custom claims or other info, create function in the functions emulator that triggers on user creation.
functions.auth.user().onCreate

How to test `functions.https.onCall` firebase cloud functions locally?

I can't seem to find the solution for this in the Firebase Documentation.
I want to test my functions.https.onCall functions locally. Is it possible using the shell or somehow connect my client (firebase SDK enabled) to the local server?
I want to avoid having to deploy every time just to test a change to my onCall functions.
My code
Function :
exports.myFunction = functions.https.onCall((data, context) => {
// Do something
});
Client:
const message = { message: 'Hello.' };
firebase.functions().httpsCallable('myFunction')(message)
.then(result => {
// Do something //
})
.catch(error => {
// Error handler //
});
For locally you must call (after firebase.initializeApp)
firebase.functions().useFunctionsEmulator('http://localhost:5000')
Although the official Firebase Cloud Function docs have not yet been updated, you can now use firebase-functions-test with onCall functions.
You can see an example in their repository.
I have managed to test my TypeScript functions using jest, here is a brief example. There are some peculiarities here, like import order, so make sure to read the docs :-)
/* functions/src/test/index.test.js */
/* dependencies: Jest and jest-ts */
const admin = require("firebase-admin");
jest.mock("firebase-admin");
admin.initializeApp = jest.fn(); // stub the init (see docs)
const fft = require("firebase-functions-test")();
import * as funcs from "../index";
// myFunc is an https.onCall function
describe("test myFunc", () => {
// helper function so I can easily test different context/auth scenarios
const getContext = (uid = "test-uid", email_verified = true) => ({
auth: {
uid,
token: {
firebase: {
email_verified
}
}
}
});
const wrapped = fft.wrap(funcs.myFunc);
test("returns data on success", async () => {
const result = await wrapped(null, getContext());
expect(result).toBeTruthy();
});
test("throws when no Auth context", async () => {
await expect(wrapped(null, { auth: null })).rejects.toThrow(
"No authentication context."
);
});
});
There is a simple trick, how you can simplify onCall -function testing. Just declare the onCall function callback as a local function and test that instead:
export const _myFunction = (data, context) => { // <= call this on your unit tests
// Do something
}
exports.myFunction = functions.https.onCall(_myFunction);
Now you can variate all cases with a normal function with the input you define on your function call.
Callables are just HTTPS functions with a specific format. You can test just like a HTTPS function, except you have to write code to deliver it the protocol as defined in the documentation.
you should first check for dev environment and then point your functions to local emulator.
For JS:
//after firebase init
if (window.location.host.includes("localhost") ||
window.location.host.includes("127.0.0.1")
) {
firebase
.app()
.functions() //add location here also if you're mentioning location while invoking function()
.useFunctionsEmulator("http://localhost:5001");
}
or if you don't create instance of firebase then
//after firebase init
if (window.location.host.includes("localhost") ||
window.location.host.includes("127.0.0.1")
) {
firebase
.functions()
.useFunctionsEmulator("http://localhost:5001");
}
or when serving pages from backend (node.js):
//after firebase init
if (process.env.NODE_ENV === 'development') {
firebase.functions().useFunctionsEmulator('http://localhost:5001');
}
if you are using angularfire, add this to you app.module
{
provide: FirestoreSettingsToken,
useValue: environment.production
? undefined
: {
host: "localhost:5002",
ssl: false
}
}

Is it possible to use Cypress e2e testing with a firebase auth project?

I am exploring Cypress for e2e testing, looks like great software.
The problem is Authentication, the Cypress documentation explains why using the UI is very bad here.
So I tried looking at the network tap of my application, to see if I could create a POST request to the firebase API, and authenticate without using the GUI. But I can see that there at least 2 request fired, and token saved to application storage.
So what approach should I use?
Authenticate with the UI of my application, and instruct Cypress not to touch the local storage
Keep experimenting with a way of sending the correct POST requests, and save the values to local storage.
Make Cypress run custom JS code, and then use the Firebase SDK to login.
I am really looking for some advice here :)
When doing this myself I made custom commands (like cy.login for auth then cy.callRtdb and cy.callFirestore for verifying data). After getting tired of repeating the logic it took to build them, I wrapped it up into a library called cypress-firebase. It includes custom commands and a cli to generate a custom auth token.
Setup mostly just consists of adding the custom commands in cypress/support/commands.js:
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/database';
import 'firebase/firestore';
import { attachCustomCommands } from 'cypress-firebase';
const fbConfig = {
// Your config from Firebase Console
};
window.fbInstance = firebase.initializeApp(fbConfig);
attachCustomCommands({ Cypress, cy, firebase })
And adding the plugin to cypress/plugins/index.js:
const cypressFirebasePlugin = require('cypress-firebase').plugin
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
// Return extended config (with settings from .firebaserc)
return cypressFirebasePlugin(config)
}
But there full details on setup are available in the setup docs.
Disclosure, I am the author of cypress-firebase, which is the whole answer.
I took the approach of using automated UI to obtain the contents of localStorage used by Firebase JS SDK. I also wanted to do this only once per whole Cypress run so I did it before the Cypress start.
Obtain Firebase SDK localStorage entry via pupeteer
Store the contents in the tmp file (problems passing it via env var to Cypress)
Pass the file location to Cypress via env var and let it read the contents and set the localStorage to setup the session
Helper script which obtains contents of localStorage:
const puppeteer = require('puppeteer')
const invokeLogin = async page => {
await page.goto('http://localhost:3000/login')
await page.waitForSelector('.btn-googleplus')
await page.evaluate(() =>
document.querySelector('.btn-googleplus').click())
}
const doLogin = async (page, {username, password}) => {
// Username step
await page.waitForSelector('#identifierId')
await page.evaluate((username) => {
document.querySelector('#identifierId').value = username
document.querySelector('#identifierNext').click()
}, username)
// Password step
await page.waitForSelector('#passwordNext')
await page.evaluate(password =>
setTimeout(() => {
document.querySelector('input[type=password]').value = password
document.querySelector('#passwordNext').click()
}, 3000) // Wait 3 second to next phase to init (couldn't find better way)
, password)
}
const extractStorageEntry = async page =>
page.evaluate(() => {
for (let key in localStorage) {
if (key.startsWith('firebase'))
return {key, value: localStorage[key]}
}
})
const waitForApp = async page => {
await page.waitForSelector('#app')
}
const main = async (credentials, cfg) => {
const browser = await puppeteer.launch(cfg)
const page = await browser.newPage()
await invokeLogin(page)
await doLogin(page, credentials)
await waitForApp(page)
const entry = await extractStorageEntry(page)
console.log(JSON.stringify(entry))
await browser.close()
}
const username = process.argv[2]
const password = process.argv[3]
main({username, password}, {
headless: true // Set to false for debugging
})
Since there were problem with sending JSON as environment variables to Cypress I use tmp file to pass the data between the script and the Cypress process.
node test/getFbAuthEntry ${USER} ${PASSWORD} > test/tmp/fbAuth.json
cypress open --env FB_AUTH_FILE=test/tmp/fbAuth.json
In Cypress I read it from the file system and set it to the localStorage
const setFbAuth = () =>
cy.readFile(Cypress.env('FB_AUTH_FILE'))
.then(fbAuth => {
const {key, value} = fbAuth
localStorage[key] = value
})
describe('an app something', () => {
it('does stuff', () => {
setFbAuth()
cy.viewport(1300, 800)
...
This is certainly a hack but to get around the login part for the app I am working on I use the beforeEach hook to login to the application.
beforeEach(() => {
cy.resetTestDatabase().then(() => {
cy.setupTestDatabase();
});
});
Which is derived from my helper functions.
Cypress.Commands.add('login', () => {
return firebase
.auth()
.signInWithEmailAndPassword(Cypress.env('USER_EMAIL'), Cypress.env('USER_PASSWORD'));
});
Cypress.Commands.add('resetTestDatabase', () => {
return cy.login().then(() => {
firebase
.database()
.ref(DEFAULT_CATEGORIES_PATH)
.once('value')
.then(snapshot => {
const defaultCategories = snapshot.val();
const updates = {};
updates[TEST_CATEGORIES_PATH] = defaultCategories;
updates[TEST_EVENTS_PATH] = null;
updates[TEST_STATE_PATH] = null;
updates[TEST_EXPORT_PATH] = null;
return firebase
.database()
.ref()
.update(updates);
});
});
});
What I would like to know is how the information coming back from firebase ultimately gets saved to localStorage. I don't really have an answer to this but it works. Also, the app uses .signInWithPopup(new firebase.auth.GoogleAuthProvider()) whereas above it signs in with email and password. So I am kind of shortcutting the signin process only because cypress has the CORS limitation.
This is becoming way easier with the upcoming Auth emulator. This has become easier with the Firebase Auth Emulator (firebase-tools >= 8.1.4).
cypress/support/signAs.js:
Cypress.Commands.add('signAs', (uid, opt) => {
cy.visit('/')
cy.window().its('firebase').then( fb => {
cy.wrap( (async _ => {
// Create a user based on the provided token (only '.uid' is used by Firebase)
await fb.auth().signInWithCustomToken( JSON.stringify({ uid }) );
// Set '.displayName', '.photoURL'; for email and password, other functions exist (not implemented)
await fb.auth().currentUser.updateProfile(opt);
})() )
})
})
Use it as:
cy.signAs('joe', { displayName: 'Joe D.', photoURL: 'http://some' });
If you need to set .email or .password, there are similar functions for those, but this was sufficient for my tests. I can now impersonate any user ad-hoc, as part of the test. The approach does not need users to be created in the emulator; you can just claim to be one, with the particular uid. Works well for me.
Note:
Firebase authentication is in IndexedDB (as mentioned in other answers) and Cypress does not clear it, between the tests. There is discussion about this in cypress #1208.
At the time writing, I've examined these approaches
stubbing firebase network requests - really difficult. A bunch of firebase requests is sent continuously. There are so many request params & large payload and they're unreadable.
localStorage injection - as same as request stubbing. It requires an internally thorough understanding of both firebase SDK and data structure.
cypress-firebase plugin - it's not matured enough and lack of documentation. I skipped this option because it needs a service account (admin key). The project I'm working on is opensource and there are many contributors. It's hard to share the key without including it in the source control.
Eventually, I implemented it on my own which is quite simple. Most importantly, it doesn't require any confidential firebase credentials. Basically, it's done by
initialize another firebase instance within Cypress
use that firebase instance to build a Cypress custom command to login
const fbConfig = {
apiKey: `your api key`, // AIzaSyDAxS_7M780mI3_tlwnAvpbaqRsQPlmp64
authDomain: `your auth domain`, // onearmy-test-ci.firebaseapp.com
projectId: `your project id`, // onearmy-test-ci
}
firebase.initializeApp(fbConfig)
const attachCustomCommands = (
Cypress,
{ auth, firestore }: typeof firebase,
) => {
let currentUser: null | firebase.User = null
auth().onAuthStateChanged(user => {
currentUser = user
})
Cypress.Commands.add('login', (email, password) => {
Cypress.log({
displayName: 'login',
consoleProps: () => {
return { email, password }
},
})
return auth().signInWithEmailAndPassword(email, password)
})
Cypress.Commands.add('logout', () => {
const userInfo = currentUser ? currentUser.email : 'Not login yet - Skipped'
Cypress.log({
displayName: 'logout',
consoleProps: () => {
return { currentUser: userInfo }
},
})
return auth().signOut()
})
}
attachCustomCommands(Cypress, firebase)
Here is the commit that has all integration code https://github.com/ONEARMY/community-platform/commit/b441699c856c6aeedb8b73464c05fce542e9ead1
Ok after much trial and error, I tried solution path 2 and it worked.
So my auth flow looks like this:
Send POST request (using cybress.request) to
https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword,
and parse the response. Create an object: response1 = response.body
Send POST request (using cybress.request) to
https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo,
use the idToken from the prev request. Create an object: user = response2.body.users[0];
Combine the response in an object, with the following properties:
const authObject = {
uid: response1.localId,
displayName: response1.displayName,
photoURL: null,
email: response1.email,
phoneNumber: null,
isAnonymous: false,
providerData: [
{
uid: response1.email,
displayName: response1.displayName,
photoURL: null,
email: body.email,
phoneNumber: null,
providerId: 'password'
}
],
'apiKey': apiKey,
'appName': '[DEFAULT]',
'authDomain': '<name of firebase domain>',
'stsTokenManager': {
'apiKey': apiKey,
'refreshToken': response1.refreshToken,
'accessToken': response1.idToken,
'expirationTime': user.lastLoginAt + Number(response1.expiresIn)
},
'redirectEventId': null,
'lastLoginAt': user.lastLoginAt,
'createdAt': user.createdAt
};
Then in cybress, I simply save this object in local storag, in the before hook: localStorage.setItem(firebase:authUser:${apiKey}:[DEFAULT], authObject);
Maybe not perfect, but it solves the problem. Let me know if you interested in the code, and if you have any knowledge about how to build the "authObject", or solve this problem in another way.

Resources