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

后端 未结 6 1420
再見小時候
再見小時候 2021-02-19 03:16

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

相关标签:
6条回答
  • 2021-02-19 03:39

    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.

    0 讨论(0)
  • 2021-02-19 03:41

    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)
    ...
    
    0 讨论(0)
  • 2021-02-19 03:42

    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

    0 讨论(0)
  • 2021-02-19 03:53

    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.

    0 讨论(0)
  • 2021-02-19 03:58

    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.

    0 讨论(0)
  • 2021-02-19 04:04

    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.

    0 讨论(0)
提交回复
热议问题