21

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

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

那么我应该使用什么方法呢?

  1. 使用我的应用程序的 UI 进行身份验证,并指示赛普拉斯不要接触本地存储
  2. 继续尝试发送正确 POST 请求的方法,并将值保存到本地存储。
  3. 让 Cypress 运行自定义 JS 代码,然后使用 Firebase SDK 登录。

我真的在这里寻找一些建议:)

4

6 回答 6

5

当我自己这样做时,我制作了自定义命令(例如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)
}

但是设置文档中提供了有关设置的完整详细信息。

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

于 2018-11-30T07:13:32.560 回答
2

使用即将推出的Auth 模拟器,这变得更加容易。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);
    })() )
  })
})

将其用作:

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

如果您需要设置.emailor .password,它们有类似的功能,但这对于我的测试来说已经足够了。作为测试的一部分,我现在可以临时模拟任何用户。该方法不需要在模拟器中创建用户;您可以使用特定的 uid 声称自己是其中之一。对我来说效果很好。

笔记:

Firebase 身份验证在 IndexedDB 中(如其他答案中所述),赛普拉斯在测试之间没有清除它。在cypress #1208中有关于此的讨论。

于 2020-10-11T11:00:06.790 回答
2

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

  1. 通过pupeteer获取 Firebase SDK localStorage 条目
  2. 将内容存储在 tmp 文件中(通过 env var 将其传递给赛普拉斯的问题)
  3. 通过 env var 将文件位置传递给 Cypress,让它读取内容并设置 localStorage 以设置会话

获取 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)
...
于 2018-02-20T13:35:31.697 回答
2

这当然是一个 hack,但为了绕过我正在开发的应用程序的登录部分,我使用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 限制而简化了登录过程。

于 2018-05-02T18:44:42.500 回答
1

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

  • 存根 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

于 2019-10-11T03:21:43.453 回答
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];

将对象中的响应与以下属性相结合:

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 中,我只是将这个对象保存在本地存储中,在 before 钩子中:localStorage.setItem(firebase:authUser:${apiKey}:[DEFAULT], authObject);

也许不完美,但它解决了问题。如果您对代码感兴趣,请告诉我,如果您对如何构建“authObject”有任何了解,或者以其他方式解决此问题。

于 2018-02-13T09:42:51.140 回答