Is it possible to use Cypress e2e testing with a f

2019-04-23 18:47发布

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 :)

4条回答
做个烂人
2楼-- · 2019-04-23 19:01

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.

查看更多
Melony?
3楼-- · 2019-04-23 19:11

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.

查看更多
贪生不怕死
4楼-- · 2019-04-23 19:14

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.

查看更多
放我归山
5楼-- · 2019-04-23 19:17

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)
...
查看更多
登录 后发表回答