是否可以将赛普拉斯e2e测试与firebase auth项目一起使用?

时间:2018-02-11 19:32:43

标签: firebase firebase-authentication cypress

我正在探索赛普拉斯的e2​​e测试,看起来很棒的软件。 问题是身份验证,赛普拉斯文档解释了为什么使用UI非常糟糕here

所以我尝试查看我的应用程序的网络点击,看看我是否可以创建对firebase API的POST请求,并在不使用GUI的情况下进行身份验证。但我可以看到至少有2个请求被触发,并且令牌已保存到应用程序存储中。

那么我应该采用什么方法?

  1. 使用我的应用程序的UI进行身份验证,并指示Cybress不要触摸本地存储
  2. 继续尝试发送正确的POST请求,并将值保存到本地存储。
  3. 让赛普拉斯运行自定义JS代码,然后使用Firebase SDK登录。
  4. 我在这里寻找一些建议:)

6 个答案:

答案 0 :(得分:2)

我采用了使用自动UI的方法来获取Firebase JS SDK使用的localStorage的内容。我也想在整个赛普拉斯运行中只执行一次,所以我在赛普拉斯开始之前就已经这样做了。

  1. 通过 pupeteer
  2. 获取Firebase SDK localStorage条目
  3. 将内容存储在tmp文件中(通过env var传递给赛普拉斯的问题)
  4. 通过env var将文件位置传递给赛普拉斯,让它读取内容并设置localStorage以设置会话
  5. 获取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
    })
    

    由于将JSON作为环境变量发送到赛普拉斯有问题,我使用tmp文件在脚本和赛普拉斯进程之间传递数据。

    node test/getFbAuthEntry ${USER} ${PASSWORD} > test/tmp/fbAuth.json
    cypress open --env FB_AUTH_FILE=test/tmp/fbAuth.json
    

    在赛普拉斯,我从文件系统中读取它并将其设置为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)
    ...
    

答案 1 :(得分:2)

这肯定是一个黑客,但为了解决我正在处理的应用程序的登录部分,我使用beforeEach挂钩登录到应用程序。

beforeEach(() => {
  cy.resetTestDatabase().then(() => {
    cy.setupTestDatabase();
  });
});

这是从我的帮助函数派生的。

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);
      });
  });
});

我想知道的是,从firebase返回的信息最终会如何保存到localStorage。我真的没有答案,但它有效。此外,该应用使用.signInWithPopup(new firebase.auth.GoogleAuthProvider()),而在其上方则使用电子邮件和密码登录。所以我只是因为cypress具有CORS限制而使签名过程变得简单。

答案 2 :(得分:1)

当我自己做此操作时,我做了一些自定义命令(例如cy.login进行身份验证,然后cy.callRtdbcy.callFirestore进行数据验证)。在厌倦了重复构建它们所花费的逻辑之后,我将其包装到一个名为cypress-firebase的库中。它包括自定义命令和一个用于生成自定义身份验证令牌的cli。

设置主要只是在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 })

并将插件添加到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)
}

但是有关设置are available in the setup docs的完整详细信息。

披露,我是cypress-firebase的作者,这是完整的答案。

答案 3 :(得分:1)

即将推出的Auth emulator变得越来越容易。 Firebase Auth Emulatorfirebase-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);
    })() )
  })
})

用作:

cy.signAs('joe', { displayName: 'Joe D.', photoURL: 'http://some' });

如果您需要设置.email.password,则可以设置类似的功能,但这对我的测试就足够了。作为测试的一部分,我现在可以临时模拟任何用户。该方法不需要在仿真器中创建用户。您可以使用特定的uid声称自己是一个。对我来说很好。

注意:

Firebase身份验证位于IndexedDB中(如其他答案所述),并且在两次测试之间,赛普拉斯不会清除它。 cypress #1208中对此进行了讨论。

答案 4 :(得分:0)

经过多次试验和错误后,我尝试了解决方案路径2并且运行良好。

所以我的身份验证流程如下:

  1. 发送POST请求(使用cybress.request) https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword, 并解析响应。创建一个对象:response1 = response.body

  2. 发送POST请求(使用cybress.request) https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo, 使用prev请求中的idToken。创建一个对象:user = response2.body.users[0];

  3. 将对象中的响应与以下属性组合在一起:

    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
        };
    

    然后在cybress中,我只是将这个对象保存在本地storag中,在前钩子中:localStorage.setItem( firebase:authUser:$ {apiKey}:[DEFAULT] , authObject);

    可能不完美,但它解决了这个问题。如果您对代码感兴趣,并且您对如何构建&#34; authObject&#34;或者以其他方式解决此问题有任何了解,请告诉我。

答案 5 :(得分:0)

在撰写本文时,我已经研究了这些方法

  • 中断Firebase网络请求-确实很困难。一堆firebase请求被连续发送。请求参数太多,有效载荷很大,而且它们不可读。
  • localStorage注入-与请求存根相同。它需要从内部全面了解Firebase SDK和数据结构。
  • cypress-firebase插件-它还不够成熟并且缺少文档。我跳过了此选项,因为它需要一个服务帐户(管理员密钥)。我正在研究的项目是开源的,并且有很多贡献者。如果不将密钥包含在源代码管理中,则很难共享它。

最终,我自己实现了它,这很简单。最重要的是,它不需要任何机密的Firebase凭证。基本上是由

完成的
  • 在Cypress中初始化另一个Firebase实例
  • 使用该Firebase实例构建赛普拉斯自定义命令以登录

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)

这是具有所有集成代码https://github.com/ONEARMY/community-platform/commit/b441699c856c6aeedb8b73464c05fce542e9ead1

的提交