Creating a Modern Firebase Powered Application with TDD
Use reactive programming and tests to build a professional app
[Sprint One] Circumventing Firebase Authentication for E2E Tests
Setting up cypress-firebase
PROModule Outline
- Source Code & Resources PRO
- Lesson 1: Introduction PUBLIC
- Lesson 2: The Structure of this Module PUBLIC
- Lesson 3: [Sprint One] Setting up Firebase PUBLIC
- Lesson 4: [Sprint One] Creating Security Rules with TDD PRO
- Lesson 5: [Sprint One] Testing Authentication PRO
- Lesson 6: [Sprint One] Component Store PRO
- Lesson 7: [Sprint One] Circumventing Firebase Authentication for E2E Tests PRO
- Lesson 8: [Sprint Two] Displaying Client List from Firestore PRO
- Lesson 9: [Sprint Two] - Adding Clients PRO
- Lesson 10: [Sprint Two] - Editing Clients PRO
- Lesson 11: [Sprint Two] - Client Details PRO
- Lesson 12: Preparing for Delivery PRO
- Lesson 13: Configuring for Production PRO
- Lesson 14: [Sprint Three] - Refactoring PRO
- Lesson 15: [Sprint Three] Setting up PWA PRO
- Lesson 16: [Sprint Three] Logout PRO
- Lesson 17: [Sprint Three] Delete a Client PRO
- Lesson 18: [Sprint Three] - Feedback Mechanism PRO
- Lesson 19: [Sprint Three] View Feedback PRO
- Lesson 20: More Styling PRO
- Lesson 21: [Sprint Four] - Refactoring Feedback PRO
- Lesson 22: [Sprint Four] - Feedback Dates PRO
- Lesson 23: [Sprint Four] - Client Survey PRO
- Lesson 24: [Sprint Four] - View Survey PRO
- Lesson 25: Final Touches PRO
- Lesson 26: Conclusion PRO
Lesson Outline
Circumventing Authentication
You might have been hoping to make some good progress on this E2E test, but we immediately hit another big hurdle. You will likely notice that when the login button is clicked we are greeted with our signInWithPopup
window:
This is a problem.
Now we get into the tricky part, and the tricky part can be separated into two distinct non equally tricky parts:
- We need to be authenticated before most tests
- We need to authenticate using an external service
Since most of the tests we write are going to be for the admin user, it will require that we have admin access to whatever it is we are testing. We could go through the entire authentication flow at the beginning of every test to authenticate as the admin, but this is going to slow our tests down.
The other issue is that if we were using a more standard authentication process, like just entering in a username/password into a text field, then handling the authentication with Cypress would be easy. We could just log in a demo account by sending the username/password to the appropriate fields using Cypress (although this isn't necessarily the best solution).
However, we are going to be using Firebase/Google to authenticate. That means there are parts of the authentication process that need to happen outside of Cypress. The user clicks the login button, then a separate Google authentication window pops up and they use that to sign in. We have no control over that in our Cypress test.
To deal with this issue we are going to implement two solutions:
- We will make sure that our application can utilise the Firebase emulators whilst in development mode
- We will use the
cypress-firebase
package to create a custom authentication token for us, which will allow us to fake a login in our tests (as well as providing other helper methods for interacting with our Firestore database in tests)
First, we are going to set up the emulators for our application so that when we are running in development mode will be connecting to an emulated Firestore and Auth instance.
Setting up Emulators for Development
To get our application to use the emulators in development mode we are going to need to make a few changes. First, we will need to modify our app.module.ts
file:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { initializeApp, provideFirebaseApp, getApp } from '@angular/fire/app';
import { environment } from '../environments/environment';
import { provideAuth, getAuth, connectAuthEmulator } from '@angular/fire/auth';
import {
initializeFirestore,
provideFirestore,
connectFirestoreEmulator,
getFirestore,
Firestore,
} from '@angular/fire/firestore';
@NgModule({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule,
IonicModule.forRoot(),
AppRoutingModule,
provideFirebaseApp(() => initializeApp(environment.firebase)),
provideFirestore(() => {
let firestore: Firestore;
if (environment.useEmulators) {
// Long polling required for Cypress
firestore = initializeFirestore(getApp(), {
experimentalForceLongPolling: true,
});
connectFirestoreEmulator(firestore, 'localhost', 8080);
} else {
firestore = getFirestore();
}
return firestore;
}),
provideAuth(() => {
const auth = getAuth();
if (environment.useEmulators) {
connectAuthEmulator(auth, 'http://localhost:9099', {
disableWarnings: true,
});
}
return auth;
}),
],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
bootstrap: [AppComponent],
})
export class AppModule {}
Notice that in the provideAuth
and provideFirestore
methods we are now checking for a useEmulators
option from our environment config files. If that value is set to true
then we call a method to connect to the emulator. For Cypress to work properly with Firebase we also need to set the experimentalForceLongPolling
option - so if we are using emulators we also manually configure Firestore to use this setting (otherwise we don't need to manually configure any settings)
We also need to make sure we set that value in our environment files.
Modify
environments/environment.prod.ts
to reflect the following:
export const environment = {
firebase: {
// your firebase config
},
production: true,
useEmulators: false,
};
Modify
environments/environment.ts
to reflect the following:
export const environment = {
firebase: {
projectId: 'demo-project',
// your firebase config
},
production: false,
useEmulators: true,
};
NOTE: Make sure to change your projectId
for environment.ts
(i.e. your dev environment) to demo-project
. It doesn't have to specifically be this name but it does have to be in the demo-*
format. This is a special naming convention for the Firebase emulator suite that we are using, which will indicate that we are not connecting to a real project - just emulators. If you use projectId
like this you should notice the following message when we start up the emulators:
emulators: Detected demo project ID "demo-project", emulated services will use a demo configuration and attempts to access non-emulated services for this project will fail.
Finally, so that we don't always have to remember to manually start the emulators, let's modify our start
script in package.json
to start the emulators before running ionic serve
:
"start": "firebase emulators:exec --project=demo-project --ui 'ionic serve'",
NOTE: We've also added the --ui
flag so that we can access the Emulator UI on http://localhost:4000
, and --project
to make sure we are connecting to a demo project.
Now we should be able to run:
npm start
and our application should now be using the emulators since we are in development mode. When we do a production build with --prod
it will connect to the real Firebase project (because it will use environment.prod.ts
which has useEmulators
set to false
).
That's convenient for development, but we also need to do the same thing for our Cypress tests:
"e2e": "firebase emulators:exec --project=demo-project --ui 'ng e2e'",
Now when we run npm run e2e
the emulators will be started and then our tests will run.
Setting up cypress-firebase
Getting the emulators to run in development is only half the story, now we need to set up cypress-firebase
. To do this, we are going to need to install a few packages:
npm i --save-dev cypress-firebase firebase-admin@9 cross-env
NOTE: Version 9 of firebase-admin
is required specifically here as cypress-firebase
has not yet been updated for v10
The cypress-firebase
package can connect to your actual Firebase project by using a serviceAccount.json
file (which is a way to provide admin access to your project), but we are only going to be using it with the emulator so we don't need to create that file.
Now we need to configure Cypress so that we can use the custom cypress-firebase
commands like login()
and callFirestore
.
Add the following to
cypress/support/commands.ts
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import 'firebase/compat/database';
import 'firebase/compat/firestore';
import { attachCustomCommands } from 'cypress-firebase';
import { environment } from '../../src/environments/environment';
firebase.initializeApp(environment.firebase);
const firestoreEmulatorHost = Cypress.env('FIRESTORE_EMULATOR_HOST');
if (firestoreEmulatorHost) {
firebase.firestore().settings({
host: firestoreEmulatorHost,
experimentalForceLongPolling: true,
ssl: false,
});
}
const authEmulatorHost = Cypress.env('FIREBASE_AUTH_EMULATOR_HOST');
if (authEmulatorHost) {
firebase.auth().useEmulator(`http://${authEmulatorHost}/`);
}
attachCustomCommands({ Cypress, cy, firebase });
We use the same Firebase config here that we have already set up in our environment file. Also note how we are enabling the usage of the emulators here - this relies on us defining the FIRESTORE_EMULATOR_HOST
and FIREBASE_AUTH_EMULATOR_HOST
environment variables. We will get to that in a moment.
Uncomment the following line in
cypress/support/e2e.ts
:
import './commands';
Modify
cypress/plugins/index.ts
to reflect the following:
Thanks for checking out the preview of this lesson!
You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).