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

有些话、适合烂在心里 提交于 2019-12-05 21:51:59

问题


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?

  1. Authenticate with the UI of my application, and instruct Cybress not to touch the local storage
  2. Keep experimenting with a way of sending the correct POST requests, and save the values to local storage.
  3. Make Cypress run custom JS code, and then use the Firebase SDK to login.

I am really looking for some advice here :)


回答1:


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.

  1. Obtain Firebase SDK localStorage entry via pupeteer
  2. Store the contents in the tmp file (problems passing it via env var to Cypress)
  3. 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)
...



回答2:


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.




回答3:


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.




回答4:


Ok after much trial and error, I tried solution path 2 and it worked.

So my auth flow looks like this:

  1. 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

  2. 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.




回答5:


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



来源:https://stackoverflow.com/questions/48735570/is-it-possible-to-use-cypress-e2e-testing-with-a-firebase-auth-project

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!