From 6c9add1f13102e30cfc345f211d3b0dd56de9317 Mon Sep 17 00:00:00 2001 From: Kanti Kuijk Date: Wed, 12 Jun 2024 00:45:23 +0200 Subject: [PATCH] feat: more auth functionality (#1230) - @KantiKuijk --- README.md | 286 ++++-- src/attachCustomCommands.ts | 1231 +++++++++++++++++++++--- src/plugin.ts | 54 +- src/tasks.ts | 602 +++++++++++- test/unit/attachCustomCommands.spec.ts | 559 +++++++++-- test/unit/plugin.spec.ts | 4 +- 6 files changed, 2446 insertions(+), 290 deletions(-) diff --git a/README.md b/README.md index ac0b7219..01a42b90 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,14 @@ 0 dependency plugin which adds [custom cypress commands](https://docs.cypress.io/api/cypress-api/custom-commands.html#Syntax) for interactions with Firebase: -- [cy.login][1] -- [cy.logout][4] -- [cy.callRtdb][6] -- [cy.callFirestore][9] +- [cy.createUserWithClaims][createUserWClaims] +- [cy.login][login] +- [cy.loginWithEmailAndPassword][loginEmail] +- [cy.logout][logout] +- [cy.deleteAllAuthUsers][delAllUsers] +- [cy.callRtdb][callRtdb] +- [cy.callFirestore][callFirestore] +- [cy.\[authFunction\]][authFunctions] If you are interested in what drove the need for this checkout [the why section](#why) @@ -181,16 +185,48 @@ attachCustomCommands({ Cypress, cy, firebase, app: namedApp }); #### Table of Contents -- [cy.login][1] - - [Examples][2] -- [cy.logout][4] - - [Examples][5] -- [cy.callRtdb][6] - - [Parameters][7] - - [Examples][8] -- [cy.callFirestore][9] - - [Parameters][10] - - [Examples][11] +- [cy.createUserWithClaims][createUserWClaims] + - [Parameters][createUserWClaims-params] + - [Examples][createUserWClaims-ex] +- [cy.login][login] + - [Parameters][login-params] + - [Examples][login-ex] +- [cy.loginWithEmailAndPassword][loginEmail] + - [Parameters][loginEmail-params] + - [Examples][loginEmail-ex] +- [cy.logout][logout] + - [Parameters][logout-params] + - [Examples][logout-ex] +- [cy.deleteAllAuthUsers][delAllUsers] + - [Parameters][delAllUsers-params] + - [Examples][delAllUsers-ex] +- [cy.callRtdb][callRtdb] + - [Parameters][callRtdb-params] + - [Examples][callRtdb-ex] +- [cy.callFirestore][callFirestore] + - [Parameters][callFirestore-params] + - [Examples][callFirestore-ex] +- [cy.\[authFunction\]][authFunctions] + - [Parameters][authFunctions-params] + - [Examples][authFunctions-ex] + +#### cy.createUserWithClaims + +Command to create a user and give the user custom claims in one command. + +##### Parameters + +- `properties` **[CreateRequest][firebase-createrequest]** The properties of the new user +- `customClaims` **[object][mdn-object] | [null][mdn-null]** Optional custom claims to be set, or null to remove custom claims +- `tenantId` **[string][mdn-string]** Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID + +##### Examples + +```javascript +const uid = '123SomeUid'; +const claims = { role: 'Admin' }; +cy.createUserWithClaims(uid, claims); +``` #### cy.login @@ -198,22 +234,28 @@ Login to Firebase using custom auth token. To specify a tenant ID, either pass the ID as a parameter to `cy.login`, or set it as environment variable `TEST_TENANT_ID`. Read more about [Firebase multi-tenancy](https://cloud.google.com/identity-platform/docs/multi-tenancy-authentication). +##### Parameters + +- `uid` **[string][mdn-string]** UID of user to login as. Can also be set with environment variable TEST_UID +- `customClaims` **[string][mdn-string]** Optional custom claims to attach to the custom token +- `tenantId` **[string][mdn-string]** Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID + ##### Examples -Loading `TEST_UID` automatically from Cypress env: +_Loading `TEST_UID` automatically from Cypress env_ ```javascript cy.login(); ``` -Passing a UID +_Passing a UID_ ```javascript const uid = '123SomeUid'; cy.login(uid); ``` -Passing a tenant ID +_Passing a tenant ID_ ```javascript const uid = '123SomeUid'; @@ -221,33 +263,101 @@ const tenantId = '123SomeTenantId'; cy.login(uid, undefined, tenantId); ``` +#### cy.loginWithEmailAndPassword + +Login to Firebase using an email and password account. + +##### Parameters + +- `email` **[string][mdn-string]** Email of user to login as. Can also be set with environment variable TEST_EMAIL +- `password` **[string][mdn-string]** Password of user to login as. Can also be set with environment variable TEST_PASSWORD +- `extraInfo` **[CreateRequest][firebase-createrequest]** Optional additional CreateRequest parameters except for email and password +- `tenantId` **[string][mdn-string]** Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID + +##### Examples + +_Loading `TEST_EMAIL` and `TEST_PASSWORD` automatically from Cypress env_ + +```javascript +cy.loginWithEmailAndPassword(); +``` + +_Passing an email and password_ + +```javascript +const email = 'some@user.com'; +const psswd = 'password123'; +cy.loginWithEmailAndPassword(email, psswd); +``` + +_Passing a tenant ID_ + +```javascript +const email = 'some@user.com'; +const psswd = 'password123'; +const tenantId = '123SomeTenantId'; +cy.loginWithEmailAndPassword(email, psswd, undefined, tenantId); +``` + #### cy.logout Log out of Firebase instance +##### Parameters + +- `tenantId` **[string][mdn-string]** Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID + ##### Examples ```javascript cy.logout(); ``` +_Passing a tenant ID_ + +```javascript +const tenantId = '123SomeTenantId'; +cy.logout(tenantId); +``` + +#### cy.deleteAllAuthUsers + +Command to recursively delete all firebase auth users. The firebase deleteUsers function (cy.authDeleteUsers) can only remove a maximum amount of users at a time. This command calls the deleteUsers function recursively for every pageToken returned by the previous iteration. + +##### Parameters + +- `tenantId` **[string][mdn-string]** Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID + +##### Examples + +```javascript +cy.deleteAllAuthUsers(); +``` + +_Passing a tenant ID_ + +```javascript +const tenantId = '123SomeTenantId'; +cy.deleteAllAuthUsers(tenantId); +``` + #### cy.callRtdb Call Real Time Database path with some specified action such as `set`, `update` and `remove` ##### Parameters -- `action` **[String][11]** The action type to call with (set, push, update, remove) -- `actionPath` **[String][11]** Path within RTDB that action should be applied -- `options` **[object][12]** Options - - `options.limitToFirst` **[number|boolean][13]** Limit to the first `` results. If true is passed than query is limited to last 1 item. - - `options.limitToLast` **[number|boolean][13]** Limit to the last `` results. If true is passed than query is limited to last 1 item. - - `options.orderByKey` **[boolean][13]** Order by key name - - `options.orderByValue` **[boolean][13]** Order by primitive value - - `options.orderByChild` **[string][11]** Select a child key by which to order results - - `options.equalTo` **[string][11]** Restrict results to `` (based on specified ordering) - - `options.startAt` **[string][11]** Start results at `` (based on specified ordering) - - `options.endAt` **[string][11]** End results at `` (based on specified ordering) +- `action` **[string][mdn-string]** The action type to call with (set, push, update, remove) +- `actionPath` **[string][mdn-string]** Path within RTDB that action should be applied +- `options` **[object][mdn-object]** Options + - `options.limitToFirst` **[number][mdn-number]|[boolean][mdn-boolean]** Limit to the first `` results. If true is passed than query is limited to last 1 item. + - `options.limitToLast` **[number][mdn-number]|[boolean][mdn-boolean]** Limit to the last `` results. If true is passed than query is limited to last 1 item. + - `options.orderByKey` **[boolean][mdn-boolean]** Order by key name + - `options.orderByValue` **[boolean][mdn-boolean]** Order by primitive value + - `options.orderByChild` **[string][mdn-string]** Select a child key by which to order results + - `options.equalTo` **[string][mdn-string]** Restrict results to `` (based on specified ordering) + - `options.startAt` **[string][mdn-string]** Start results at `` (based on specified ordering) + - `options.endAt` **[string][mdn-string]** End results at `` (based on specified ordering) ##### Examples @@ -327,19 +437,19 @@ level. ##### Parameters -- `action` **[String][11]** The action type to call with (set, push, update, delete) -- `actionPath` **[String][11]** Path within Firestore that action should be applied -- `dataOrOptions` **[String|Object][11]** Data for write actions or options for get action -- `options` **[Object][12]** Options - - `options.withMeta` **[boolean][13]** Whether or not to include `createdAt` and `createdBy` - - `options.merge` **[boolean][13]** Merge data during set - - `options.batchSize` **[number][13]** Size of batch to use while deleting - - `options.where` **[Array][13]** Filter documents by the specified field and the value should satisfy +- `action` **[string][mdn-string]** The action type to call with (set, push, update, delete) +- `actionPath` **[string][mdn-string]** Path within Firestore that action should be applied +- `dataOrOptions` **[string][mdn-string]|[object][mdn-object]** Data for write actions or options for get action +- `options` **[object][mdn-object]** Options + - `options.withMeta` **[boolean][mdn-boolean]** Whether or not to include `createdAt` and `createdBy` + - `options.merge` **[boolean][mdn-boolean]** Merge data during set + - `options.batchSize` **[number][mdn-number]** Size of batch to use while deleting + - `options.where` **[array][mdn-array]** Filter documents by the specified field and the value should satisfy * the relation constraint provided - - `options.orderBy` **[string|Array][13]** Order documents - - `options.limit` **[number][13]** Limit to n number of documents - - `options.limitToLast` **[number][13]** Limit to last n number of documents - - `options.statics` **[admin.firestore][13]** Firestore statics (i.e. `admin.firestore`). This should only be needed during testing due to @firebase/testing not containing statics + - `options.orderBy` **[string][mdn-string]|[array][mdn-array]** Order documents + - `options.limit` **[number][mdn-number]** Limit to n number of documents + - `options.limitToLast` **[number][mdn-number]** Limit to last n number of documents + - `options.statics` **admin.firestore** Firestore statics (i.e. `admin.firestore`). This should only be needed during testing due to @firebase/testing not containing statics ##### Examples @@ -409,6 +519,56 @@ describe('Test firestore', () => { }); ``` +#### cy.[authFunction] + +Use any of the [firebase admin auth methods](https://firebase.google.com/docs/reference/admin/node/firebase-admin.auth.baseauth#methods): + +- `authCreateAuthUser` +- `authImportAuthUsers` +- `authListAuthUsers` +- `authGetAuthUser` +- `authGetAuthUserByEmail` +- `authGetAuthUserByPhoneNumber` +- `authGetAuthUserByProviderUid` +- `authGetAuthUsers` +- `authUpdateAuthUser` +- `authSetAuthUserCustomClaims` +- `authDeleteAuthUser` +- `authDeleteAuthUsers` +- `authCreateCustomToken` +- `authCreateSessionCookie` +- `authVerifyIdToken` +- `authRevokeRefreshTokens` +- `authGenerateEmailVerificationLink` +- `authGeneratePasswordResetLink` +- `authGenerateSignInWithEmailLink` +- `authGenerateVerifyAndChangeEmailLink` +- `authGreateProviderConfig` +- `authGetProviderConfig` +- `authListProviderConfigs` +- `authUpdateProviderCondig` +- `authDeleteProviderConfig` + +##### Parameters + +The parameters (and return type) depend on the function used. They are the same as for the firebase api function it refers to, with addition of an optional `tenantId` parameter as last parameter: + +- `tenantId` **[string][mdn-string]** Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID + +##### Examples + +```javascript +const uid = '123SomeUid'; +const email = 'some@user.com'; +cy.authCreateUser({ uid }); +cy.authUpdateUser(uid, { displayName: 'Test User', email }); +cy.authSetCustomUserClaims(uid, { role: 'admin' }); +cy.authGetUserByEmail(email).then((user) => { + console.log(user?.displayName); // Test User + console.log(user?.customClaims?.['role']); // admin +}); +``` + ### Plugin Plugin attaches cypress tasks, which are called by custom commands, and initializes firebase-admin instance. By default cypress-firebase internally initializes firebase-admin using `GCLOUD_PROJECT` environment variable for project identification and application-default credentials (set by providing path to service account in `GOOGLE_APPLICATION_CREDENTIALS` environment variable) [matching Google documentation](https://firebase.google.com/docs/admin/setup#initialize-sdk). This default functionality can be overriden by passing a forth argument to the plugin - this argument is passed directly into the firebase-admin instance as [AppOptions](https://firebase.google.com/docs/reference/admin/dotnet/class/firebase-admin/app-options#constructors-and-destructors) on init which means any other config such as `databaseURL`, `credential`, or `databaseAuthVariableOverride` can be included. @@ -814,20 +974,38 @@ If you experience this with an SDK version newer than v7 please create a new iss - Drop support for service account file in favor of application default credentails env variable (path to file set in `GOOGLE_APPLICATION_CREDENTIALS`) - Support for Auth emulators (this will become the suggested method instead of needing a service account) -[1]: #cylogin -[2]: #examples -[3]: #currentuser -[4]: #cylogout -[5]: #examples-1 -[6]: #cycallrtdb -[7]: #parameters -[8]: #examples-2 -[9]: #cycallfirestore -[10]: #parameters-1 -[11]: #examples-3 -[12]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object -[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array +[createUserWClaims]: #cycreateuserwithclaims +[createUserWClaims-params]: #parameters +[createUserWClaims-ex]: #examples +[login]: #cylogin +[login-params]: #parameters-1 +[login-ex]: #examples-1 +[loginEmail]: #cyloginwithemailandpassword +[loginEmail-params]: #parameters-2 +[loginEmail-ex]: #examples-2 +[currentUser]: #currentuser +[logout]: #cylogout +[logout-params]: #parameters-3 +[logout-ex]: #examples-3 +[delAllUsers]: #cydeleteallauthusers +[delAllUsers-params]: #examples-4 +[delAllUsers-ex]: #examples-4 +[callRtdb]: #cycallrtdb +[callRtdb-params]: #parameters-5 +[callRtdb-ex]: #examples-5 +[callFirestore]: #cycallfirestore +[callFirestore-params]: #parameters-6 +[callFirestore-ex]: #examples-6 +[authFunctions]: #cyauthfunction +[authFunctions-params]: #parameters-7 +[authFunctions-ex]: #examples-7 +[mdn-string]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String +[mdn-number]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[mdn-boolean]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[mdn-object]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object +[mdn-array]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array +[mdn-null]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/null +[firebase-createrequest]: https://firebase.google.com/docs/reference/admin/node/firebase-admin.auth.createrequest.md#createrequest_interface [fireadmin-url]: https://fireadmin.io [fireadmin-source]: https://github.com/prescottprue/fireadmin [npm-image]: https://img.shields.io/npm/v/cypress-firebase.svg?style=flat-square diff --git a/src/attachCustomCommands.ts b/src/attachCustomCommands.ts index 7f5373cb..0990d76d 100644 --- a/src/attachCustomCommands.ts +++ b/src/attachCustomCommands.ts @@ -1,4 +1,6 @@ -import type { firestore } from 'firebase-admin'; +import type { auth, firestore } from 'firebase-admin'; +import type { authCreateUser } from './tasks'; +import { typedTask, TaskNameToParams } from './tasks'; /** * Params for attachCustomCommand function for @@ -167,29 +169,6 @@ declare global { namespace Cypress { /* eslint-enable @typescript-eslint/no-namespace,@typescript-eslint/no-unused-vars */ interface Chainable { - /** - * Login to Firebase auth as a user with either a passed uid or the TEST_UID - * environment variable. A custom auth token is generated using firebase-admin - * authenticated with serviceAccount.json or SERVICE_ACCOUNT env var. - * @see https://github.com/prescottprue/cypress-firebase#cylogin - * @param uid - UID of user to login as - * @param customClaims - Custom claims to attach to the custom token - * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also be set with environment variable TEST_TENANT_ID - * @example Env Based Login (TEST_UID) - * cy.login() - * @example Passed UID - * cy.login('123SOMEUID') - */ - login: (uid?: string, customClaims?: any, tenantId?: string) => Chainable; - - /** - * Log current user out of Firebase Auth - * @see https://github.com/prescottprue/cypress-firebase#cylogout - * @example - * cy.logout() - */ - logout: () => Chainable; - /** * Call Real Time Database path with some specified action. Authentication is through * `FIREBASE_TOKEN` (CI token) since firebase-tools is used under the hood, allowing @@ -231,7 +210,6 @@ declare global { deletePath: string, options?: CallFirestoreOptions, ): Chainable; - /** * Set, or add a document to Firestore. Authentication is through serviceAccount.json or SERVICE_ACCOUNT * environment variable. @@ -256,7 +234,6 @@ declare global { data: firestore.PartialWithFieldValue, options?: CallFirestoreOptions, ): Chainable; - /** * Update an existing document in Firestore. Authentication is through serviceAccount.json or SERVICE_ACCOUNT * environment variable. @@ -271,7 +248,6 @@ declare global { data: firestore.UpdateData, options?: CallFirestoreOptions, ): Chainable; - /** * Get an existing document from Firestore. Authentication is through serviceAccount.json or SERVICE_ACCOUNT * environment variable. @@ -289,6 +265,431 @@ declare global { getPath: string, options?: CallFirestoreOptions, ): Chainable; + + /** + * Create a Firebase Auth user + * @param properties - The properties to set on the new user record to be created + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authCreateUser + * @see https://github.com/prescottprue/cypress-firebase#cyauthcreateuser + * cy.authCreateUser() + */ + authCreateUser: ( + properties: auth.CreateRequest, + tenantId?: string, + ) => Chainable; + + /** + * Create a Firebase Auth user with custom claims + * @param properties - The properties to set on the new user record to be created + * @param customClaims - Custom claims to attach to the new user + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.createUserWithClaims + * @see https://github.com/prescottprue/cypress-firebase#cycreateuserwithclaims + * cy.createUserWithClaims() + */ + createUserWithClaims: ( + properties: auth.CreateRequest, + customClaims?: object | null, + tenantId?: string, + ) => Chainable; + + /** + * Import list of Firebase Auth users + * @param usersImport - The list of user records to import to Firebase Auth + * @param importOptions - Optional options for the user import + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authImportUsers + * @see https://github.com/prescottprue/cypress-firebase#cyauthimportusers + * @example + * cy.authImportUsers() + */ + authImportUsers: ( + ...args: TaskNameToParams<'authImportUsers'> + ) => Chainable; + + /** + * List Firebase Auth users + * @param maxResults - The page size, 1000 if undefined + * @param pageToken - The next page token + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authListUsers + * @see https://github.com/prescottprue/cypress-firebase#cyauthlistusers + * @example + * cy.authListUsers() + */ + authListUsers: ( + ...args: TaskNameToParams<'authListUsers'> + ) => Cypress.Chainable; + + /** + * Login to Firebase auth as a user with either a passed uid or the TEST_UID + * environment variable. A custom auth token is generated using firebase-admin + * authenticated with serviceAccount.json or SERVICE_ACCOUNT env var. + * @see https://github.com/prescottprue/cypress-firebase#cylogin + * @param uid - UID of user to login as + * @param customClaims - Custom claims to attach to the custom token + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @example Env Based Login (TEST_UID) + * cy.login() + * @example Passed UID + * cy.login('123SOMEUID') + */ + login: (uid?: string, customClaims?: any, tenantId?: string) => Chainable; + + /** + * Login to Firebase auth using email and password user with either a passed + * email and password or the TEST_EMAIL and TEST_PASSWORD environment variables. + * Authentication happens with serviceAccount.json or SERVICE_ACCOUNT env var. + * @see https://github.com/prescottprue/cypress-firebase#cyloginwithemailandpassword + * @param email - Email of user to login as + * @param password - Password of user to login as + * @param customClaims - Custom claims to attach to the custom token + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @example Env Based Login (TEST_EMAIL, TEST_PASSWORD) + * cy.loginWithEmailAndPassword() + * @example Passed email and password + * cy.login('some@user.com', 'password123') + */ + loginWithEmailAndPassword: ( + email?: string, + password?: string, + customClaims?: any, + tenantId?: string, + ) => Chainable; + + /** + * Log current user out of Firebase Auth + * @see https://github.com/prescottprue/cypress-firebase#cylogout + * @example + * cy.logout() + */ + logout: () => Chainable; + + /** + * Get Firebase auth user by UID + * @param uid - UID of user to get + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authGetUser + * @see https://github.com/prescottprue/cypress-firebase#cyauthgetuser + * @example + * cy.authGetUser('1234') + */ + authGetUser: ( + ...args: TaskNameToParams<'authGetUser'> + ) => Chainable; + /** + * Get Firebase auth user by email + * @param email - Email of user to get + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authGetUserByEmail + * @see https://github.com/prescottprue/cypress-firebase#cyauthgetuserbyemail + * @example + * cy.authGetUserByEmail('foobar@mail.com') + */ + authGetUserByEmail: ( + ...args: TaskNameToParams<'authGetUserByEmail'> + ) => Chainable; + /** + * Get Firebase auth user by phone number + * @param phoneNumber - Phone number of user to get + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authGetUserByPhoneNumber + * @see https://github.com/prescottprue/cypress-firebase#cyauthgetuserbyphonenumber + * @example + * cy.authGetUserByPhoneNumber('1234567890') + */ + authGetUserByPhoneNumber: ( + ...args: TaskNameToParams<'authGetUserByPhoneNumber'> + ) => Chainable; + /** + * Get Firebase auth user by providerID and UID + * @param providerId - Provider ID of user to get + * @param uid - UID of user to get + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authGetUserByProviderUid + * @see https://github.com/prescottprue/cypress-firebase#cyauthgetuserbyprovideruid + * @example + * cy.authGetUserByProviderUid(providerId, uid) + */ + authGetUserByProviderUid: ( + ...args: TaskNameToParams<'authGetUserByProviderUid'> + ) => Chainable; + + /** + * Get Firebase Auth users based on identifiers + * @param adminInstance - Admin SDK instance + * @param identifiers - The identifiers used to indicate which user records should be returned. + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with a GetUsersResult object + * @see https://github.com/prescottprue/cypress-firebase#cyauthgetusers + * @example + * cy.authGetUsers([{email: 'foobar@mail.com'}, {uid: "1234"}]) + */ + authGetUsers: ( + ...args: TaskNameToParams<'authGetUsers'> + ) => Chainable; + + /** + * Update an existing Firebase Auth user + * @param uid - UID of the user to edit + * @param properties - The properties to update on the user + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authUpdateUser + * @see https://github.com/prescottprue/cypress-firebase#cyauthupdateuser + * @example + * cy.authUpdateUser(uid, {displayName: "New Name"}) + */ + authUpdateUser: ( + ...args: TaskNameToParams<'authUpdateUser'> + ) => Chainable; + /** + * Set custom claims of an existing Firebase Auth user + * @param uid - UID of the user to edit + * @param customClaims - The custom claims to set, null deletes the custom claims + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authSetCustomUserClaims + * @see https://github.com/prescottprue/cypress-firebase#cyauthsetcustomuserclaims + * @example + * cy.authSetCustomUserClaims(uid, {some: 'claim'}) + */ + authSetCustomUserClaims: ( + ...args: TaskNameToParams<'authSetCustomUserClaims'> + ) => Chainable; + + /** + * Delete a user from Firebase Auth with either a passed uid or the TEST_UID + * environment variable + * @param uid = UID of user to delete + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authDeleteUser + * @see https://github.com/prescottprue/cypress-firebase#cyauthdeleteuser + * @example + * cy.authDeleteUser(uid) + */ + authDeleteUser: ( + ...args: TaskNameToParams<'authDeleteUser'> + ) => Chainable; + /** + * Delete a user from Firebase Auth with either a passed uid or the TEST_UID + * environment variable + * @param uid = UID of user to delete + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authDeleteUsers + * @see https://github.com/prescottprue/cypress-firebase#cyauthdeleteusers + * @example + * cy.authDeleteUsers([uid1, uid2]) + */ + authDeleteUsers: ( + ...args: TaskNameToParams<'authDeleteUsers'> + ) => Chainable; + /** + * Delete all users from Firebase Auth + * Resolves when all users have been deleted + * Rejects if too many deletes fail or all deletes failed + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.deleteAllAuthUsers + * @see https://github.com/prescottprue/cypress-firebase#cydeleteallauthusers + * @example + * cy.deleteAllAuthUsers() + */ + deleteAllAuthUsers: (tenantId?: string) => Chainable; + + /** + * Create a custom token for a user + * @param uid - UID of the user to create a custom token for + * @param customClaims - Custom claims to attach to the custom token + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authCreateCustomToken + * @see https://github.com/prescottprue/cypress-firebase#cyauthcreatecustomtoken + * @example + * cy.authCreateCustomToken(uid) + */ + authCreateCustomToken: ( + ...args: TaskNameToParams<'authCreateCustomToken'> + ) => Chainable; + /** + * Create a session cookie for a user + * @param idToken - ID token to create session cookie for + * @param sessionCookieOptions - Options for the session cookie + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authCreateSessionCookie + * @see https://github.com/prescottprue/cypress-firebase#cyauthcreatesessioncookie + * @example + * cy.authCreateSessionCookie(idToken) + */ + authCreateSessionCookie: ( + ...args: TaskNameToParams<'authCreateSessionCookie'> + ) => Chainable; + /** + * Verify an ID token + * @param idToken - ID token to verify + * @param checkRevoked - Whether to check if the token has been revoked + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authVerifyIdToken + * @see https://github.com/prescottprue/cypress-firebase#cyauthverifyidtoken + * @example + * cy.authVerifyIdToken(idToken) + */ + authVerifyIdToken: ( + ...args: TaskNameToParams<'authVerifyIdToken'> + ) => Chainable; + /** + * Revoke all refresh tokens for a user + * @param uid - UID of user to revoke refresh tokens for + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authRevokeRefreshTokens + * @see https://github.com/prescottprue/cypress-firebase#cyauthrevokerefreshtokens + * @example + * cy.authRevokeRefreshTokens(uid) + */ + authRevokeRefreshTokens: ( + ...args: TaskNameToParams<'authRevokeRefreshTokens'> + ) => Chainable; + /** + * Generate an email verification link + * @param email - Email to generate verification link for + * @param actionCodeSettings - Action code settings for the email + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authGenerateEmailVerificationLink + * @see https://github.com/prescottprue/cypress-firebase#cyauthgenerateemailverificationlink + * @example + * cy.authGenerateEmailVerificationLink(email) + */ + authGenerateEmailVerificationLink: ( + ...args: TaskNameToParams<'authGenerateEmailVerificationLink'> + ) => Chainable; + /** + * Generate a password reset link + * @param email - Email to generate password reset link for + * @param actionCodeSettings - Action code settings for the email + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authGeneratePasswordResetLink + * @see https://github.com/prescottprue/cypress-firebase#cyauthgeneratepasswordresetlink + * @example + * cy.authGeneratePasswordResetLink(email) + */ + authGeneratePasswordResetLink: ( + ...args: TaskNameToParams<'authGeneratePasswordResetLink'> + ) => Chainable; + /** + * Generate a sign in with email link + * @param email - Email to generate sign in link for + * @param actionCodeSettings - Action code settings for the email + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authGenerateSignInWithEmailLink + * @see https://github.com/prescottprue/cypress-firebase#cyauthgeneratesigninwithemaillink + * @example + * cy.authGenerateSignInWithEmailLink(email, actionCodeSettings) + */ + authGenerateSignInWithEmailLink: ( + ...args: TaskNameToParams<'authGenerateSignInWithEmailLink'> + ) => Chainable; + + /** + * Generate a verify and change email link + * @param email - Email to generate verify and change email link for + * @param newEmail - New email to change to + * @param actionCodeSettings - Action code settings for the email + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authGenerateVerifyAndChangeEmailLink + * @see https://github.com/prescottprue/cypress-firebase#cyauthgenerateverifyandchangeemaillink + * @example + * cy.authGenerateVerifyAndChangeEmailLink(oldEmail, newEmail) + */ + authGenerateVerifyAndChangeEmailLink: ( + ...args: TaskNameToParams<'authGenerateVerifyAndChangeEmailLink'> + ) => Chainable; + + /** + * Create a provider config + * @param providerConfig - The provider config to create + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authCreateProviderConfig + * @see https://github.com/prescottprue/cypress-firebase#cyauthcreateproviderconfig + * @example + * cy.authCreateProviderConfig(providerConfig) + */ + authCreateProviderConfig: ( + ...args: TaskNameToParams<'authCreateProviderConfig'> + ) => Chainable; + /** + * Get a provider config + * @param providerId - The provider ID to get the config for + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authGetProviderConfig + * @see https://github.com/prescottprue/cypress-firebase#cyauthgetproviderconfig + * @example + * cy.authGetProviderConfig(providerId) + */ + authGetProviderConfig: ( + ...args: TaskNameToParams<'authGetProviderConfig'> + ) => Chainable; + /** + * List provider configs + * @param providerFilter - The provider ID to filter by, or null to list all + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authListProviderConfigs + * @see https://github.com/prescottprue/cypress-firebase#cyauthlistproviderconfigs + * @example + * cy.authListProviderConfigs(providerFilter) + */ + authListProviderConfigs: ( + ...args: TaskNameToParams<'authListProviderConfigs'> + ) => Chainable; + /** + * Update a provider config + * @param providerId - The provider ID to update the config for + * @param providerConfig - The provider config to update to + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authUpdateProviderConfig + * @see https://github.com/prescottprue/cypress-firebase#cyauthupdateproviderconfig + * @example + * cy.authUpdateProviderConfig(providerId, providerConfig) + */ + authUpdateProviderConfig: ( + ...args: TaskNameToParams<'authUpdateProviderConfig'> + ) => Chainable; + /** + * Delete a provider config + * @param providerId - The provider ID to delete the config for + * @param tenantId - Optional ID of tenant used for multi-tenancy. Can also + * be set with environment variable TEST_TENANT_ID + * @name cy.authDeleteProviderConfig + * @see https://github.com/prescottprue/cypress-firebase#cyauthdeleteproviderconfig + * @example + * cy.authDeleteProviderConfig(providerId) + */ + authDeleteProviderConfig: ( + ...args: TaskNameToParams<'authDeleteProviderConfig'> + ) => Chainable; } } } @@ -336,14 +737,103 @@ function loginWithCustomToken(auth: any, customToken: string): Promise { }); } -interface CommandNamespacesConfig { - login?: string; - logout?: string; - callRtdb?: string; - callFirestore?: string; - getAuthUser?: string; +/** + * @param auth - firebase auth instance + * @param email - email to use for login + * @param password - password to use for login + * @returns Promise which resolves with the user auth object + */ +function loginWithEmailAndPassword( + auth: any, + email: string, + password: string, +): Promise { + return new Promise((resolve, reject): any => { + auth.onAuthStateChanged((auth: any) => { + if (auth) { + resolve(auth); + } + }); + auth.signInWithEmailAndPassword(email, password).catch(reject); + }); } +/** + * Delete all users from Firebase Auth, recursively because of batch + * size limitations + * Resolves when all users have been deleted + * Rejects if too many deletes fail or all deletes failed + * @param cy - Cypress object + * @param tenantId - Tenant ID to use for user deletion + * @param pageToken - Page token used for recursion + * @returns Promise which resolves when all users have been deleted + */ +function authDeleteAllUsers( + cy: AttachCustomCommandParams['cy'], + tenantId?: string | undefined, + pageToken?: string, +): Promise { + return new Promise((resolve, reject): any => { + typedTask(cy, 'authListUsers', { tenantId, pageToken }).then( + ({ users, pageToken: nextPageToken }) => { + if (users.length === 0) resolve(); + else { + const uids = users.map((user) => user.uid); + typedTask(cy, 'authDeleteUsers', { uids, tenantId }).then( + ({ successCount, failureCount }) => { + if (failureCount > successCount || successCount === 0) + reject( + new Error( + `Too many deletes failed. ${successCount} users were deleted, ${failureCount} failed.`, + ), + ); + authDeleteAllUsers(cy, tenantId, nextPageToken).then(resolve); + }, + ); + } + }, + ); + }); +} + +type CommandNames = + | 'callRtdb' + | 'callFirestore' + | 'authCreateUser' + | 'createUserWithClaims' + | 'authImportUsers' + | 'authListUsers' + | 'login' + | 'loginWithEmailAndPassword' + | 'logout' + | 'authGetUser' + | 'authGetUserByEmail' + | 'authGetUserByPhoneNumber' + | 'authGetUserByProviderUid' + | 'authGetUsers' + | 'authUpdateUser' + | 'authSetCustomUserClaims' + | 'authDeleteUser' + | 'authDeleteUsers' + | 'deleteAllAuthUsers' + | 'authCreateCustomToken' + | 'authCreateSessionCookie' + | 'authVerifyIdToken' + | 'authRevokeRefreshTokens' + | 'authGenerateEmailVerificationLink' + | 'authGeneratePasswordResetLink' + | 'authGenerateSignInWithEmailLink' + | 'authGenerateVerifyAndChangeEmailLink' + | 'authCreateProviderConfig' + | 'authGetProviderConfig' + | 'authListProviderConfigs' + | 'authUpdateProviderConfig' + | 'authDeleteProviderConfig'; + +type CommandNamespacesConfig = { + [N in CommandNames]?: string | boolean; +}; + interface CustomCommandOptions { commandNames?: CommandNamespacesConfig; tenantId?: string; @@ -376,83 +866,6 @@ export default function attachCustomCommands( return auth; } - /** - * Login to Firebase auth as a user with either a passed uid or the TEST_UID - * environment variable. A custom auth token is generated using firebase-admin - * authenticated with serviceAccount.json or SERVICE_ACCOUNT env var. - * @name cy.login - */ - Cypress.Commands.add( - (options && options.commandNames && options.commandNames.login) || 'login', - ( - uid?: string, - customClaims?: any, - tenantId: string | undefined = Cypress.env('TEST_TENANT_ID'), - ): any => { - const userUid = uid || Cypress.env('TEST_UID'); - // Handle UID which is passed in - if (!userUid) { - throw new Error( - 'uid must be passed or TEST_UID set within environment to login', - ); - } - const auth = getAuth(tenantId); - // Resolve with current user if they already exist - if (auth.currentUser && userUid === auth.currentUser.uid) { - cy.log('Authed user already exists, login complete.'); - return undefined; - } - - cy.log('Creating custom token for login...'); - - // Generate a custom token using createCustomToken task (if tasks are enabled) then login - return cy - .task('createCustomToken', { - uid: userUid, - customClaims, - tenantId, - }) - .then((customToken: string) => loginWithCustomToken(auth, customToken)); - }, - ); - - /** - * Log out of Firebase instance - * @name cy.logout - * @see https://github.com/prescottprue/cypress-firebase#cylogout - * @example - * cy.logout() - */ - Cypress.Commands.add( - (options && options.commandNames && options.commandNames.logout) || - 'logout', - ( - tenantId: string | undefined = Cypress.env('TEST_TENANT_ID'), - ): Promise => - new Promise( - ( - resolve: (value?: any) => void, - reject: (reason?: any) => void, - ): any => { - const auth = getAuth(tenantId); - auth.onAuthStateChanged((auth: any) => { - if (!auth) { - resolve(); - } - }); - auth.signOut().catch(reject); - }, - ), - ); - - /** - * Call Real Time Database path with some specified action. Leverages callRtdb - * Cypress task which calls through firebase-admin. - * @param action - The action type to call with (set, push, update, remove) - * @param actionPath - Path within RTDB that action should be applied - * @param options - Options - * @name cy.callRtdb - */ Cypress.Commands.add( (options && options.commandNames && options.commandNames.callRtdb) || 'callRtdb', @@ -494,14 +907,6 @@ export default function attachCustomCommands( }, ); - /** - * Call Firestore instance with some specified action. Leverages callFirestore - * Cypress task which calls through firebase-admin. - * @param action - The action type to call with (set, push, update, remove) - * @param actionPath - Path within RTDB that action should be applied - * @param options - Options - * @name cy.callFirestore - */ Cypress.Commands.add( (options && options.commandNames && options.commandNames.callFirestore) || 'callFirestore', @@ -543,16 +948,586 @@ export default function attachCustomCommands( }, ); - /** - * Get Firebase auth user by UID - * @name cy.getAuthUser - * @see https://github.com/prescottprue/cypress-firebase#cygetauthuser - * @example - * cy.getAuthUser() - */ Cypress.Commands.add( - (options && options.commandNames && options.commandNames.getAuthUser) || - 'getAuthUser', - (uid: string): Promise => cy.task('getAuthUser', uid), + (options && options.commandNames && options.commandNames.authCreateUser) || + 'authCreateUser', + ( + properties: auth.CreateRequest, + tenantId: string = Cypress.env('TEST_TENANT_ID'), + ) => + typedTask(cy, 'authCreateUser', { properties, tenantId }).then((user) => { + if (user === 'auth/email-already-exists') { + if (!properties.email) { + throw new Error( + 'User with email already exists yet no email was given', + ); + } + cy.log('Auth user with given email already exists.'); + return typedTask(cy, 'authGetUserByEmail', { + email: properties.email, + tenantId, + }).then((user) => (user === 'auth/user-not-found' ? null : user)); + } + if (user === 'auth/phone-number-already-exists') { + if (!properties.phoneNumber) { + throw new Error( + 'User with phone number already exists yet no phone number was given', + ); + } + cy.log('Auth user with given phone number already exists.'); + return typedTask(cy, 'authGetUserByPhoneNumber', { + phoneNumber: properties.phoneNumber, + tenantId, + }).then((user) => (user === 'auth/user-not-found' ? null : user)); + } + return user; + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.createUserWithClaims) || + 'createUserWithClaims', + ( + properties: auth.CreateRequest, + customClaims?: object | null, + tenantId: string = Cypress.env('TEST_TENANT_ID'), + ) => { + typedTask(cy, 'authCreateUser', { properties, tenantId }).then((user) => { + if (user === 'auth/email-already-exists') { + if (!properties.email) { + throw new Error( + 'User with email already exists yet no email was given', + ); + } + cy.log('Auth user with given email already exists.'); + return typedTask(cy, 'authGetUserByEmail', { + email: properties.email, + tenantId, + }).then((user) => (user === 'auth/user-not-found' ? null : user)); + } + if (user === 'auth/phone-number-already-exists') { + if (!properties.phoneNumber) { + throw new Error( + 'User with phone number already exists yet no phone number was given', + ); + } + cy.log('Auth user with given phone number already exists.'); + return typedTask(cy, 'authGetUserByPhoneNumber', { + phoneNumber: properties.phoneNumber, + tenantId, + }).then((user) => (user === 'auth/user-not-found' ? null : user)); + } + if (customClaims !== undefined && user) { + return typedTask(cy, 'authSetCustomUserClaims', { + uid: user.uid, + customClaims, + tenantId, + }).then(() => user.uid); + } + return user; + }); + }, + ); + + Cypress.Commands.add( + (options && options.commandNames && options.commandNames.authImportUsers) || + 'authImportUsers', + (...args: TaskNameToParams<'authImportUsers'>) => + typedTask(cy, 'authImportUsers', { + usersImport: args[0], + importOptions: args[1], + tenantId: args[2] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && options.commandNames && options.commandNames.authListUsers) || + 'authListUsers', + (...args: TaskNameToParams<'authListUsers'>) => + typedTask(cy, 'authListUsers', { + maxResults: args[0], + pageToken: args[1], + tenantId: args[2] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && options.commandNames && options.commandNames.login) || 'login', + ( + uid?: string, + customClaims?: any, + tenantId: string | undefined = Cypress.env('TEST_TENANT_ID'), + ): any => { + const userUid = uid || Cypress.env('TEST_UID'); + // Handle UID which is passed in + if (!userUid) { + throw new Error( + 'uid must be passed or TEST_UID set within environment to login', + ); + } + const auth = getAuth(tenantId); + // Resolve with current user if they already exist + if (auth.currentUser && userUid === auth.currentUser.uid) { + cy.log('Authed user already exists, login complete.'); + return undefined; + } + + cy.log('Creating custom token for login...'); + + // Generate a custom token using authCreateCustomToken task (if tasks are enabled) then login + return typedTask(cy, 'authCreateCustomToken', { + uid: userUid, + customClaims, + tenantId, + }).then((customToken) => loginWithCustomToken(auth, customToken)); + }, + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.loginWithEmailAndPassword) || + 'loginWithEmailAndPassword', + ( + email?: string, + password?: string, + extraInfo?: Omit< + Parameters[1], + 'email' | 'password' + >, + tenantId: string | undefined = Cypress.env('TEST_TENANT_ID'), + ): any => { + const userUid = Cypress.env('TEST_UID'); + const userEmail = email || Cypress.env('TEST_EMAIL'); + // Handle email which is passed in + if (!userEmail) { + throw new Error( + 'email must be passed or TEST_EMAIL set within environment to login', + ); + } + const userPassword = password || Cypress.env('TEST_PASSWORD'); + // Handle password which is passed in + if (!userPassword) { + throw new Error( + 'password must be passed or TEST_PASSWORD set within environment to login', + ); + } + const auth = getAuth(tenantId); + // Resolve with current user if they already exist + if (auth.currentUser && userEmail === auth.currentUser.email) { + cy.log('Authed user already exists, login complete.'); + return undefined; + } + return typedTask(cy, 'authGetUserByEmail', { + email: userEmail, + tenantId, + }).then((user) => { + if (user) + return loginWithEmailAndPassword(auth, userEmail, userPassword); + typedTask(cy, 'authCreateUser', { + properties: { + uid: userUid, + email: userEmail, + password: userPassword, + ...extraInfo, + }, + tenantId, + }); + return cy + .task('authCreateUser', { + properties: { + email: userEmail, + password: userPassword, + ...extraInfo, + }, + tenantId, + }) + .then(() => loginWithEmailAndPassword(auth, userEmail, userPassword)); + }); + }, + ); + + Cypress.Commands.add( + (options && options.commandNames && options.commandNames.logout) || + 'logout', + ( + tenantId: string | undefined = Cypress.env('TEST_TENANT_ID'), + ): Promise => + new Promise( + ( + resolve: (value?: any) => void, + reject: (reason?: any) => void, + ): any => { + const auth = getAuth(tenantId); + auth.onAuthStateChanged((auth: any) => { + if (!auth) { + resolve(); + } + }); + auth.signOut().catch(reject); + }, + ), + ); + + Cypress.Commands.add( + (options && options.commandNames && options.commandNames.authGetUser) || + 'authGetUser', + (uid?: string, tenantId?: string) => { + const userUid = uid || Cypress.env('TEST_UID'); + // Handle UID which is passed in + if (!userUid) { + throw new Error( + 'uid must be passed or TEST_UID set within environment to login', + ); + } + return typedTask(cy, 'authGetUser', { + uid: userUid, + tenantId: tenantId || Cypress.env('TEST_TENANT_ID'), + }).then((user) => { + if (user === 'auth/user-not-found') return null; + return user; + }); + }, + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authGetUserByEmail) || + 'authGetUserByEmail', + (email?: string, tenantId?: string) => { + const userEmail = email || Cypress.env('TEST_EMAIL'); + // Handle email which is passed in + if (!userEmail) { + throw new Error( + 'email must be passed or TEST_EMAIL set within environment to login', + ); + } + return typedTask(cy, 'authGetUserByEmail', { + email: userEmail, + tenantId: tenantId || Cypress.env('TEST_TENANT_ID'), + }).then((user) => { + if (user === 'auth/user-not-found') return null; + return user; + }); + }, + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authGetUserByPhoneNumber) || + 'authGetUserByPhoneNumber', + (...args: TaskNameToParams<'authGetUserByPhoneNumber'>) => + typedTask(cy, 'authGetUserByPhoneNumber', { + phoneNumber: args[0], + tenantId: args[1] || Cypress.env('TEST_TENANT_ID'), + }).then((user) => { + if (user === 'auth/user-not-found') return null; + return user; + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authGetUserByProviderUid) || + 'authGetUserByProviderUid', + (providerId: string, uid?: string, tenantId?: string) => { + const userUid = uid || Cypress.env('TEST_UID'); + // Handle UID which is passed in + if (!userUid) { + throw new Error( + 'uid must be passed or TEST_UID set within environment to login', + ); + } + typedTask(cy, 'authGetUserByProviderUid', { + providerId, + uid: userUid, + tenantId: tenantId || Cypress.env('TEST_TENANT_ID'), + }).then((user) => { + if (user === 'auth/user-not-found') return null; + return user; + }); + }, + ); + + Cypress.Commands.add( + (options && options.commandNames && options.commandNames.authGetUsers) || + 'authGetUsers', + (...args: TaskNameToParams<'authGetUsers'>) => + typedTask(cy, 'authGetUsers', { + identifiers: args[0], + tenantId: args[1] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && options.commandNames && options.commandNames.authUpdateUser) || + 'authUpdateUser', + (...args: TaskNameToParams<'authUpdateUser'>) => + typedTask(cy, 'authUpdateUser', { + uid: args[0], + properties: args[1], + tenantId: args[2] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authSetCustomUserClaims) || + 'authSetCustomUserClaims', + (...args: TaskNameToParams<'authSetCustomUserClaims'>) => + typedTask(cy, 'authSetCustomUserClaims', { + uid: args[0], + customClaims: args[1], + tenantId: args[2] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && options.commandNames && options.commandNames.authDeleteUser) || + 'authDeleteUser', + ( + uid?: string, + tenantId: string | undefined = Cypress.env('TEST_TENANT_ID'), + ) => { + const userUid = uid || Cypress.env('TEST_UID'); + // Handle UID which is passed in + if (!userUid) { + throw new Error( + 'uid must be passed or TEST_UID set within environment to login', + ); + } + + return typedTask(cy, 'authDeleteUser', { uid: userUid, tenantId }); + }, + ); + + Cypress.Commands.add( + (options && options.commandNames && options.commandNames.authDeleteUsers) || + 'authDeleteUsers', + (...args: TaskNameToParams<'authDeleteUsers'>) => + typedTask(cy, 'authDeleteUsers', { + uids: args[0], + tenantId: args[1] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.deleteAllAuthUsers) || + 'deleteAllAuthUsers', + (tenantId: string | undefined = Cypress.env('TEST_TENANT_ID')) => + cy.wrap(authDeleteAllUsers(cy, tenantId)), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authCreateCustomToken) || + 'authCreateCustomToken', + (uid?: string, customClaims?: object, tenantId?: string) => { + const userUid = uid || Cypress.env('TEST_UID'); + // Handle UID which is passed in + if (!userUid) { + throw new Error( + 'uid must be passed or TEST_UID set within environment to login', + ); + } + return typedTask(cy, 'authCreateCustomToken', { + uid: userUid, + customClaims, + tenantId: tenantId || Cypress.env('TEST_TENANT_ID'), + }); + }, + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authCreateSessionCookie) || + 'authCreateSessionCookie', + (...args: TaskNameToParams<'authCreateSessionCookie'>) => + typedTask(cy, 'authCreateSessionCookie', { + idToken: args[0], + sessionCookieOptions: args[1], + tenantId: args[2] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authVerifyIdToken) || + 'authVerifyIdToken', + (...args: TaskNameToParams<'authVerifyIdToken'>) => + typedTask(cy, 'authVerifyIdToken', { + idToken: args[0], + checkRevoked: args[1], + tenantId: args[2] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authRevokeRefreshTokens) || + 'authRevokeRefreshTokens', + (uid?: string, tenantId?: string) => { + const userUid = uid || Cypress.env('TEST_UID'); + // Handle UID which is passed in + if (!userUid) { + throw new Error( + 'uid must be passed or TEST_UID set within environment to login', + ); + } + return typedTask(cy, 'authRevokeRefreshTokens', { + uid: userUid, + tenantId: tenantId || Cypress.env('TEST_TENANT_ID'), + }); + }, + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authGenerateEmailVerificationLink) || + 'authGenerateEmailVerificationLink', + ( + email?: string, + actionCodeSettings?: auth.ActionCodeSettings, + tenantId?: string, + ) => { + const userEmail = email || Cypress.env('TEST_EMAIL'); + // Handle email which is passed in + if (!userEmail) { + throw new Error( + 'email must be passed or TEST_EMAIL set within environment to login', + ); + } + return typedTask(cy, 'authGenerateEmailVerificationLink', { + email: userEmail, + actionCodeSettings, + tenantId: tenantId || Cypress.env('TEST_TENANT_ID'), + }); + }, + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authGeneratePasswordResetLink) || + 'authGeneratePasswordResetLink', + ( + email?: string, + actionCodeSettings?: auth.ActionCodeSettings, + tenantId?: string, + ) => { + const userEmail = email || Cypress.env('TEST_EMAIL'); + // Handle email which is passed in + if (!userEmail) { + throw new Error( + 'email must be passed or TEST_EMAIL set within environment to login', + ); + } + return typedTask(cy, 'authGeneratePasswordResetLink', { + email: userEmail, + actionCodeSettings, + tenantId: tenantId || Cypress.env('TEST_TENANT_ID'), + }); + }, + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authGenerateSignInWithEmailLink) || + 'authGenerateSignInWithEmailLink', + (...args: TaskNameToParams<'authGenerateSignInWithEmailLink'>) => + typedTask(cy, 'authGenerateSignInWithEmailLink', { + email: args[0], + actionCodeSettings: args[1], + tenantId: args[2] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authGenerateVerifyAndChangeEmailLink) || + 'authGenerateVerifyAndChangeEmailLink', + (...args: TaskNameToParams<'authGenerateVerifyAndChangeEmailLink'>) => + typedTask(cy, 'authGenerateVerifyAndChangeEmailLink', { + email: args[0], + newEmail: args[1], + actionCodeSettings: args[2], + tenantId: args[3] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authCreateProviderConfig) || + 'authCreateProviderConfig', + (...args: TaskNameToParams<'authCreateProviderConfig'>) => + typedTask(cy, 'authCreateProviderConfig', { + providerConfig: args[0], + tenantId: args[1] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authGetProviderConfig) || + 'authGetProviderConfig', + (...args: TaskNameToParams<'authGetProviderConfig'>) => + typedTask(cy, 'authGetProviderConfig', { + providerId: args[0], + tenantId: args[1] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authListProviderConfigs) || + 'authListProviderConfigs', + (...args: TaskNameToParams<'authListProviderConfigs'>) => + typedTask(cy, 'authListProviderConfigs', { + providerFilter: args[0], + tenantId: args[1] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authUpdateProviderConfig) || + 'authUpdateProviderConfig', + (...args: TaskNameToParams<'authUpdateProviderConfig'>) => + typedTask(cy, 'authUpdateProviderConfig', { + providerId: args[0], + providerConfig: args[1], + tenantId: args[2] || Cypress.env('TEST_TENANT_ID'), + }), + ); + + Cypress.Commands.add( + (options && + options.commandNames && + options.commandNames.authDeleteProviderConfig) || + 'authDeleteProviderConfig', + (...args: TaskNameToParams<'authDeleteProviderConfig'>) => + typedTask(cy, 'authDeleteProviderConfig', { + providerId: args[0], + tenantId: args[1] || Cypress.env('TEST_TENANT_ID'), + }), ); } diff --git a/src/plugin.ts b/src/plugin.ts index 4eac3dcb..a5f9ed9b 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,16 +1,15 @@ import type { AppOptions } from 'firebase-admin'; +import type { TaskName } from './tasks'; import extendWithFirebaseConfig, { ExtendedCypressConfig, } from './extendWithFirebaseConfig'; -import * as tasks from './tasks'; +import tasks, { + TaskNameToParams, + TaskNameToReturn, + taskSettingKeys, +} from './tasks'; import { initializeFirebase } from './firebase-utils'; -type TaskKey = - | 'callRtdb' - | 'callFirestore' - | 'createCustomToken' - | 'getAuthUser'; - /** * Cypress plugin which attaches tasks used by custom commands * and returns modified Cypress config. Modified config includes @@ -35,30 +34,23 @@ export default function pluginWithTasks( } // Parse single argument from task into arguments for task methods while // also passing the admin instance - type tasksType = Record Promise>; - const tasksWithFirebase: tasksType = Object.keys(tasks).reduce( - (acc, taskName: string) => { - (acc as any)[taskName] = (taskSettings: any): any => { - if (taskSettings && taskSettings.uid) { - return (tasks as any)[taskName]( - adminInstance, - taskSettings.uid, - taskSettings, - ); - } - const { action, path: actionPath, options = {}, data } = taskSettings; - return (tasks as any)[taskName]( - adminInstance, - action, - actionPath, - options, - data, - ); - }; - return acc; - }, - {} as tasksType, - ); + type tasksType = { + [TN in TaskName]: ( + taskSettings: TaskNameToParams, + ) => TaskNameToReturn; + }; + const tasksWithFirebase: tasksType = ( + Object.keys(tasks) as TaskName[] + ).reduce((acc, taskName) => { + acc[taskName] = (taskSettings: any = {}): any => { + const taskArgs = taskSettingKeys[taskName].map( + (sk: string) => taskSettings[sk], + ); + // @ts-expect-error - TS cannot know that the right amount of args are passed + return tasks[taskName](adminInstance, ...taskArgs); + }; + return acc; + }, {} as tasksType); // Attach tasks to Cypress using on function cypressOnFunc('task', tasksWithFirebase); diff --git a/src/tasks.ts b/src/tasks.ts index 811eaf93..78c8d8d5 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -5,6 +5,7 @@ import { RTDBAction, CallRtdbOptions, CallFirestoreOptions, + AttachCustomCommandParams, } from './attachCustomCommands'; import { slashPathToFirestoreRef, @@ -76,14 +77,14 @@ export function convertValueToTimestampOrGeoPointIfPossible( ): firestore.FieldValue { /* eslint-disable no-underscore-dangle */ if ( - (dataVal && dataVal._methodName) === 'serverTimestamp' || - (dataVal && dataVal._methodName) === 'FieldValue.serverTimestamp' // v8 and earlier + (dataVal && dataVal._methodName === 'serverTimestamp') || + (dataVal && dataVal._methodName === 'FieldValue.serverTimestamp') // v8 and earlier ) { return firestoreStatics.FieldValue.serverTimestamp(); } if ( - (dataVal && dataVal._methodName) === 'deleteField' || - (dataVal && dataVal._methodName) === 'FieldValue.delete' // v8 and earlier + (dataVal && dataVal._methodName === 'deleteField') || + (dataVal && dataVal._methodName === 'FieldValue.delete') // v8 and earlier ) { return firestoreStatics.FieldValue.delete(); } @@ -256,7 +257,7 @@ export async function callFirestore( } // Falling back to null in the case of falsey value prevents Cypress error with message: // "You must return a promise, a value, or null to indicate that the task was handled." - return (typeof (snap && snap.data) === 'function' && snap.data()) || null; + return (snap && typeof snap.data === 'function' && snap.data()) || null; } if (action === 'delete') { @@ -306,7 +307,7 @@ export async function callFirestore( dataToSet, options && options.merge ? ({ - merge: options && options.merge, + merge: options.merge, } as FirebaseFirestore.SetOptions) : (undefined as any), ); @@ -329,42 +330,607 @@ export async function callFirestore( throw err; } } +/** + * Create a Firebase Auth user + * @param adminInstance - Admin SDK instance + * @param properties - The properties to set on the new user record to be created + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with a UserRecord + */ +export function authCreateUser( + adminInstance: any, + properties: auth.CreateRequest, + tenantId?: string, +): Promise< + | auth.UserRecord + | 'auth/email-already-exists' + | 'auth/phone-number-already-exists' +> { + return getAuth(adminInstance, tenantId) + .createUser(properties) + .catch((err) => { + if (err.code === 'auth/email-already-exists') + return 'auth/email-already-exists'; + if (err.code === 'auth/phone-number-already-exists') + return 'auth/phone-number-already-exists'; + throw err; + }); +} +/** + * Import list of Firebase Auth users + * @param adminInstance - Admin SDK instance + * @param usersImport - The list of user records to import to Firebase Auth + * @param importOptions - Optional options for the user import + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise that resolves when the operation completes with the result of the import + */ +export function authImportUsers( + adminInstance: any, + usersImport: auth.UserImportRecord[], + importOptions?: auth.UserImportOptions, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).importUsers( + usersImport, + importOptions, + ); +} + +/** + * List Firebase Auth users + * @param adminInstance - Admin SDK instance + * @param maxResults - The page size, 1000 if undefined + * @param pageToken - The next page token + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise that resolves with the current batch of downloaded users and the next page token + */ +export function authListUsers( + adminInstance: any, + maxResults?: number, + pageToken?: string, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).listUsers(maxResults, pageToken); +} + +/** + * Get Firebase Auth user based on UID + * @param adminInstance - Admin SDK instance + * @param uid - UID of the user whose data to fetch + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with a UserRecord + */ +export function authGetUser( + adminInstance: any, + uid: string, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId) + .getUser(uid) + .catch((err) => { + if (err.code === 'auth/user-not-found') return 'auth/user-not-found'; + throw err; + }); +} +/** + * Get Firebase Auth user based on email + * @param adminInstance - Admin SDK instance + * @param email - Email of the user whose data to fetch + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with a UserRecord + */ +export function authGetUserByEmail( + adminInstance: any, + email: string, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId) + .getUserByEmail(email) + .catch((err) => { + if (err.code === 'auth/user-not-found') return 'auth/user-not-found'; + throw err; + }); +} +/** + * Get Firebase Auth user based on phone number + * @param adminInstance - Admin SDK instance + * @param phoneNumber - Phone number of the user whose data to fetch + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with a UserRecord + */ +export function authGetUserByPhoneNumber( + adminInstance: any, + phoneNumber: string, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId) + .getUserByPhoneNumber(phoneNumber) + .catch((err) => { + if (err.code === 'auth/user-not-found') return 'auth/user-not-found'; + throw err; + }); +} +/** + * Get Firebase Auth user based on phone number + * @param adminInstance - Admin SDK instance + * @param providerId - The Provider ID + * @param uid - The user identifier for the given provider + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with a UserRecord + */ +export function authGetUserByProviderUid( + adminInstance: any, + providerId: string, + uid: string, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId) + .getUserByProviderUid(providerId, uid) + .catch((err) => { + if (err.code === 'auth/user-not-found') return 'auth/user-not-found'; + throw err; + }); +} +/** + * Get Firebase Auth users based on identifiers + * @param adminInstance - Admin SDK instance + * @param identifiers - The identifiers used to indicate which user records should be returned. + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with a GetUsersResult object + */ +export function authGetUsers( + adminInstance: any, + identifiers: auth.UserIdentifier[], + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).getUsers(identifiers); +} + +/** + * Update an existing Firebase Auth user + * @param adminInstance - Admin SDK instance + * @param uid - UID of the user to edit + * @param properties - The properties to update on the user + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise that resolves with a UserRecord + */ +export function authUpdateUser( + adminInstance: any, + uid: string, + properties: auth.UpdateRequest, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).updateUser(uid, properties); +} +/** + * Delete multiple Firebase Auth users + * @param adminInstance - Admin SDK instance + * @param uid - UID of the user to edit + * @param customClaims - The custom claims to set, null deletes the custom claims + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise that resolves with null when the operation completes + */ +export function authSetCustomUserClaims( + adminInstance: any, + uid: string, + customClaims: object | null, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId) + .setCustomUserClaims(uid, customClaims) + .then(() => null); +} + +/** + * Delete a Firebase Auth user + * @param adminInstance - Admin SDK instance + * @param uid - UID of the user to delete + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise that resolves to null when user is deleted + */ +export function authDeleteUser( + adminInstance: any, + uid: string, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId) + .deleteUser(uid) + .then(() => null); +} +/** + * Delete multiple Firebase Auth users + * @param adminInstance - Admin SDK instance + * @param uids - Array of UIDs of the users to delete + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves to a DeleteUsersResult object + */ +export function authDeleteUsers( + adminInstance: any, + uids: string[], + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).deleteUsers(uids); +} /** * Create a custom token * @param adminInstance - Admin SDK instance * @param uid - UID of user for which the custom token will be generated - * @param settings - Settings object + * @param customClaims - Optional custom claims to include in the token + * @param tenantId - Optional ID of tenant used for multi-tenancy * @returns Promise which resolves with a custom Firebase Auth token */ -export function createCustomToken( +export function authCreateCustomToken( adminInstance: any, uid: string, - settings?: any, + customClaims?: object, + tenantId?: string, ): Promise { // Use custom claims or default to { isTesting: true } - const customClaims = (settings && settings.customClaims) || { + const userCustomClaims = customClaims || { isTesting: true, }; // Create auth token - return getAuth(adminInstance, settings.tenantId).createCustomToken( + return getAuth(adminInstance, tenantId).createCustomToken( uid, - customClaims, + userCustomClaims, ); } /** - * Get Firebase Auth user based on UID + * Create a session cookie * @param adminInstance - Admin SDK instance - * @param uid - UID of user for which the custom token will be generated + * @param idToken - Firebase ID token + * @param sessionCookieOptions - Session cookie options * @param tenantId - Optional ID of tenant used for multi-tenancy - * @returns Promise which resolves with a custom Firebase Auth token + * @returns Promise which resolves with a session cookie + */ +export function authCreateSessionCookie( + adminInstance: any, + idToken: string, + sessionCookieOptions: auth.SessionCookieOptions, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).createSessionCookie( + idToken, + sessionCookieOptions, + ); +} + +/** + * Verify a Firebase ID token + * @param adminInstance - Admin SDK instance + * @param idToken - Firebase ID token + * @param checkRevoked - Whether to check if the token is revoked + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with a decoded ID token */ -export function getAuthUser( +export function authVerifyIdToken( + adminInstance: any, + idToken: string, + checkRevoked?: boolean, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).verifyIdToken(idToken, checkRevoked); +} + +/** + * Revoke all refresh tokens for a user + * @param adminInstance - Admin SDK instance + * @param uid - UID of the user for which to revoke tokens + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves when the operation completes + */ +export function authRevokeRefreshTokens( adminInstance: any, uid: string, tenantId?: string, -): Promise { - return getAuth(adminInstance, tenantId).getUser(uid); +): Promise { + return getAuth(adminInstance, tenantId).revokeRefreshTokens(uid); +} + +/** + * Generate an email verification link + * @param adminInstance - Admin SDK instance + * @param email - Email of the user + * @param actionCodeSettings - Action code settings + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with the email verification link + */ +export function authGenerateEmailVerificationLink( + adminInstance: any, + email: string, + actionCodeSettings?: auth.ActionCodeSettings, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).generateEmailVerificationLink( + email, + actionCodeSettings, + ); +} + +/** + * Generate a password reset link + * @param adminInstance - Admin SDK instance + * @param email - Email of the user + * @param actionCodeSettings - Action code settings + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with the password reset link + */ +export function authGeneratePasswordResetLink( + adminInstance: any, + email: string, + actionCodeSettings?: auth.ActionCodeSettings, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).generatePasswordResetLink( + email, + actionCodeSettings, + ); +} + +/** + * Generate a sign-in with email link + * @param adminInstance - Admin SDK instance + * @param email - Email of the user + * @param actionCodeSettings - Action code settings + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with the sign-in with email link + */ +export function authGenerateSignInWithEmailLink( + adminInstance: any, + email: string, + actionCodeSettings: auth.ActionCodeSettings, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).generateSignInWithEmailLink( + email, + actionCodeSettings, + ); +} + +/** + * Generate a link for email verification and email change + * @param adminInstance - Admin SDK instance + * @param email - Email of the user + * @param newEmail - New email of the user + * @param actionCodeSettings - Action code settings + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with the email verification link + */ +export function authGenerateVerifyAndChangeEmailLink( + adminInstance: any, + email: string, + newEmail: string, + actionCodeSettings?: auth.ActionCodeSettings, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).generateVerifyAndChangeEmailLink( + email, + newEmail, + actionCodeSettings, + ); +} + +/** + * Create a provider configuration + * @param adminInstance - Admin SDK instance + * @param providerConfig - The provider configuration + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with the provider configuration + */ +export function authCreateProviderConfig( + adminInstance: any, + providerConfig: auth.AuthProviderConfig, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).createProviderConfig(providerConfig); +} + +/** + * Get a provider configuration + * @param adminInstance - Admin SDK instance + * @param providerId - The provider ID + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with the provider configuration + */ +export function authGetProviderConfig( + adminInstance: any, + providerId: string, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).getProviderConfig(providerId); +} + +/** + * List provider configurations + * @param adminInstance - Admin SDK instance + * @param providerFilter - The provider filter + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with the provider configurations + */ +export function authListProviderConfigs( + adminInstance: any, + providerFilter: auth.AuthProviderConfigFilter, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).listProviderConfigs(providerFilter); +} + +/** + * Update a provider configuration + * @param adminInstance - Admin SDK instance + * @param providerId - The provider ID + * @param providerConfig - The provider configuration + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves with the provider configuration + */ +export function authUpdateProviderConfig( + adminInstance: any, + providerId: string, + providerConfig: auth.AuthProviderConfig, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId).updateProviderConfig( + providerId, + providerConfig, + ); +} + +/** + * Delete a provider configuration + * @param adminInstance - Admin SDK instance + * @param providerId - The provider ID + * @param tenantId - Optional ID of tenant used for multi-tenancy + * @returns Promise which resolves to null when the operation completes + */ +export function authDeleteProviderConfig( + adminInstance: any, + providerId: string, + tenantId?: string, +): Promise { + return getAuth(adminInstance, tenantId) + .deleteProviderConfig(providerId) + .then(() => null); +} + +/** + * Object containing all tasks created by the plugin + */ +const tasks = { + callRtdb, + callFirestore, + authCreateUser, + authImportUsers, + authListUsers, + authGetUser, + authGetUserByEmail, + authGetUserByPhoneNumber, + authGetUserByProviderUid, + authGetUsers, + authUpdateUser, + authSetCustomUserClaims, + authDeleteUser, + authDeleteUsers, + authCreateCustomToken, + authCreateSessionCookie, + authVerifyIdToken, + authRevokeRefreshTokens, + authGenerateEmailVerificationLink, + authGeneratePasswordResetLink, + authGenerateSignInWithEmailLink, + authGenerateVerifyAndChangeEmailLink, + authCreateProviderConfig, + authGetProviderConfig, + authListProviderConfigs, + authUpdateProviderConfig, + authDeleteProviderConfig, +}; +/** + * Type of all the names of tasks created by the plugin + */ +export type TaskName = keyof typeof tasks; + +/** + * Given a tuple, return a tuple with the first element dropped + */ +type DropFirstElem = T extends [any, ...infer U] ? U : never; +/** + * Given a task name, return the parameters of the task + */ +export type TaskNameToParams = DropFirstElem< + Parameters<(typeof tasks)[TN]> +>; +/** + * Given a task name, return the return type of the task + */ +export type TaskNameToReturn = ReturnType< + (typeof tasks)[TN] +>; + +/** + * Object mapping task names to their settings keys + */ +export const taskSettingKeys = { + callRtdb: ['action', 'path', 'options', 'data'], + callFirestore: ['action', 'path', 'options', 'data'], + authCreateUser: ['properties', 'tenantId'], + authImportUsers: ['usersImport', 'importOptions', 'tenantId'], + authListUsers: ['maxResults', 'pageToken', 'tenantId'], + authGetUser: ['uid', 'tenantId'], + authGetUserByEmail: ['email', 'tenantId'], + authGetUserByPhoneNumber: ['phoneNumber', 'tenantId'], + authGetUserByProviderUid: ['providerId', 'uid', 'tenantId'], + authGetUsers: ['identifiers', 'tenantId'], + authUpdateUser: ['uid', 'properties', 'tenantId'], + authSetCustomUserClaims: ['uid', 'customClaims', 'tenantId'], + authDeleteUser: ['uid', 'tenantId'], + authDeleteUsers: ['uids', 'tenantId'], + authCreateCustomToken: ['uid', 'customClaims', 'tenantId'], + authCreateSessionCookie: ['idToken', 'sessionCookieOptions', 'tenantId'], + authVerifyIdToken: ['idToken', 'checkRevoked', 'tenantId'], + authRevokeRefreshTokens: ['uid', 'tenantId'], + authGenerateEmailVerificationLink: [ + 'email', + 'actionCodeSettings', + 'tenantId', + ], + authGeneratePasswordResetLink: ['email', 'actionCodeSettings', 'tenantId'], + authGenerateSignInWithEmailLink: ['email', 'actionCodeSettings', 'tenantId'], + authGenerateVerifyAndChangeEmailLink: [ + 'email', + 'newEmail', + 'actionCodeSettings', + 'tenantId', + ], + authCreateProviderConfig: ['providerConfig', 'tenantId'], + authGetProviderConfig: ['providerId', 'tenantId'], + authListProviderConfigs: ['providerFilter', 'tenantId'], + authUpdateProviderConfig: ['providerId', 'providerConfig', 'tenantId'], + authDeleteProviderConfig: ['providerId', 'tenantId'], +} as const satisfies { [TN in TaskName]: readonly string[] }; + +/** + * Given a task name, return the settings for the task + */ +type TaskNameToSettings = [ + (typeof taskSettingKeys)[TN], + TaskNameToParams, + // make shorthands for the settings keys and params of the task +] extends [infer TNK, infer TNP] + ? { + // get only the indexes and not other array properties + [I in Extract< + keyof TNK, + `${number}` + // only those keys that do not have undefined as a value and thus are required + // @ts-expect-error - TS cannot know that the amount of params coincides with the amount of taskSettingKeys + > as undefined extends TNP[I] ? never : TNK[I]]: TNP[I]; + } & { + // get only the indexes and not other array properties + [I in Extract< + keyof TNK, + `${number}` + // only those keys that do have undefined as a value and thus are optional + // @ts-expect-error - TS cannot know that the amount of params coincides with the amount of taskSettingKeys + > as undefined extends TNP[I] ? TNK[I] : never]?: TNP[I]; + } + : never; + +/** + * A drop-in replacement for cy.task that provides type safe tasks + * @param cy - The Cypress object + * @param taskName - The name of the task + * @param taskSettings - The settings for the task + * @returns - A Cypress Chainable with the return type of the task + */ +export function typedTask( + cy: AttachCustomCommandParams['cy'], + taskName: TN, + taskSettings: TaskNameToSettings, +): Cypress.Chainable>> { + return cy.task(taskName, taskSettings); } + +export default tasks; diff --git a/test/unit/attachCustomCommands.spec.ts b/test/unit/attachCustomCommands.spec.ts index 169a7eb8..8919fa76 100644 --- a/test/unit/attachCustomCommands.spec.ts +++ b/test/unit/attachCustomCommands.spec.ts @@ -25,6 +25,41 @@ const firebase = { firestore: { Timestamp: { now: () => 'TIMESTAMP' } }, }; +const allCommandNames = [ + 'callRtdb', + 'callFirestore', + 'authCreateUser', + 'createUserWithClaims', + 'authImportUsers', + 'authListUsers', + 'login', + 'loginWithEmailAndPassword', + 'logout', + 'authGetUser', + 'authGetUserByEmail', + 'authGetUserByPhoneNumber', + 'authGetUserByProviderUid', + 'authGetUsers', + 'authUpdateUser', + 'authSetCustomUserClaims', + 'authDeleteUser', + 'authDeleteUsers', + 'deleteAllAuthUsers', + 'authCreateCustomToken', + 'authCreateSessionCookie', + 'authVerifyIdToken', + 'authRevokeRefreshTokens', + 'authGenerateEmailVerificationLink', + 'authGeneratePasswordResetLink', + 'authGenerateSignInWithEmailLink', + 'authGenerateVerifyAndChangeEmailLink', + 'authCreateProviderConfig', + 'authGetProviderConfig', + 'authListProviderConfigs', + 'authUpdateProviderConfig', + 'authDeleteProviderConfig', +]; + describe('attachCustomCommands', () => { beforeEach(() => { currentUser = {}; @@ -44,7 +79,7 @@ describe('attachCustomCommands', () => { }); describe('cy.login', () => { - it('Is attached as a custom command', () => { + it('is attached as a custom command', () => { expect(addSpy).to.have.been.calledWith('login'); }); @@ -71,7 +106,7 @@ describe('attachCustomCommands', () => { expect(returnVal).to.be.undefined; }); - it('calls task with uid and custom claims', async () => { + it('calls task with parameters and custom claims', async () => { await loadedCustomCommands.login('123ABC'); expect(taskSpy).to.have.been.calledOnce; expect(signInWithCustomToken).to.have.been.calledOnce; @@ -82,7 +117,7 @@ describe('attachCustomCommands', () => { attachCustomCommands({ cy, Cypress, firebase }); envStub.withArgs('TEST_UID').returns('foo'); await loadedCustomCommands.login(); - expect(taskSpy).to.have.been.calledOnceWith('createCustomToken', { + expect(taskSpy).to.have.been.calledOnceWith('authCreateCustomToken', { uid: 'foo', customClaims: undefined, tenantId: undefined, @@ -94,7 +129,7 @@ describe('attachCustomCommands', () => { Cypress = { Commands: { add: addSpy }, env: envStub }; attachCustomCommands({ cy, Cypress, firebase }); await loadedCustomCommands.login('123ABC', undefined, 'tenant-id'); - expect(taskSpy).to.have.been.calledOnceWith('createCustomToken', { + expect(taskSpy).to.have.been.calledOnceWith('authCreateCustomToken', { uid: '123ABC', customClaims: undefined, tenantId: 'tenant-id', @@ -107,7 +142,7 @@ describe('attachCustomCommands', () => { attachCustomCommands({ cy, Cypress, firebase }); envStub.withArgs('TEST_TENANT_ID').returns('env-tenant-id'); await loadedCustomCommands.login('123ABC'); - expect(taskSpy).to.have.been.calledOnceWith('createCustomToken', { + expect(taskSpy).to.have.been.calledOnceWith('authCreateCustomToken', { uid: '123ABC', customClaims: undefined, tenantId: 'env-tenant-id', @@ -116,8 +151,23 @@ describe('attachCustomCommands', () => { }); }); + describe('cy.loginWithEmailAndPassword', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('loginWithEmailAndPassword'); + }); + + // it('calls task', async () => { + // // Return empty auth so logout is resolved + // onAuthStateChanged = sinon.spy((authHandleFunc) => { + // authHandleFunc(); + // }); + // await loadedCustomCommands.logout(); + // expect(onAuthStateChanged).to.have.been.calledOnce; + // }); + }); + describe('cy.logout', () => { - it('Is attached as a custom command', () => { + it('is attached as a custom command', () => { expect(addSpy).to.have.been.calledWith('logout'); }); @@ -132,7 +182,7 @@ describe('attachCustomCommands', () => { }); describe('cy.callFirestore', () => { - it('Is attached as a custom command', () => { + it('is attached as a custom command', () => { expect(addSpy).to.have.been.calledWith('callFirestore'); }); @@ -186,7 +236,7 @@ describe('attachCustomCommands', () => { }); describe('cy.callRtdb', () => { - it('Is attached as a custom command', () => { + it('is attached as a custom command', () => { expect(addSpy).to.have.been.calledWith('callRtdb'); }); @@ -278,67 +328,462 @@ describe('attachCustomCommands', () => { }); }); - describe('cy.getAuthUser', () => { - it('Is attached as a custom command', () => { - expect(addSpy).to.have.been.calledWith('getAuthUser'); + describe('firebase auth functions', () => { + describe('cy.authCreateUser', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authCreateUser'); + }); + + it('calls task with parameters', async () => { + const uid = 'TESTING_USER_UID'; + await loadedCustomCommands.authCreateUser({ uid }); + expect(taskSpy).to.have.been.calledWith('authCreateUser', { + properties: { uid }, + tenantId: false, + }); + }); }); + describe('cy.authImportUsers', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authImportUsers'); + }); - it('calls task with uid', async () => { - const uid = 'TESTING_USER_UID'; - await loadedCustomCommands.getAuthUser(uid); - expect(taskSpy).to.have.been.calledWith('getAuthUser', uid); + it('calls task with parameters', async () => { + const uid1 = 'TESTING_USER_UID'; + const uid2 = 'TESTING_USER_UID'; + await loadedCustomCommands.authImportUsers([ + { uid: uid1 }, + { uid: uid2 }, + ]); + expect(taskSpy).to.have.been.calledWith('authImportUsers', { + usersImport: [{ uid: uid1 }, { uid: uid2 }], + importOptions: undefined, + tenantId: false, + }); + }); }); - }); + describe('cy.authListUsers', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authListUsers'); + }); - describe('options', () => { - it('Aliases login command', () => { - const commandNames = { login: 'testing' }; - attachCustomCommands({ cy, Cypress, firebase }, { commandNames }); - expect(addSpy).to.have.been.calledWith(commandNames.login); - expect(addSpy).to.have.been.calledWith('logout'); - expect(addSpy).to.have.been.calledWith('callRtdb'); - expect(addSpy).to.have.been.calledWith('callFirestore'); - expect(addSpy).to.have.been.calledWith('getAuthUser'); + it('calls task with parameters', async () => { + await loadedCustomCommands.authListUsers(3); + expect(taskSpy).to.have.been.calledWith('authListUsers', { + maxResults: 3, + pageToken: undefined, + tenantId: false, + }); + }); }); + describe('cy.authGetUser', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authGetUser'); + }); - it('Aliases logout command', () => { - const commandNames = { logout: 'testing' }; - attachCustomCommands({ cy, Cypress, firebase }, { commandNames }); - expect(addSpy).to.have.been.calledWith(commandNames.logout); - expect(addSpy).to.have.been.calledWith('login'); - expect(addSpy).to.have.been.calledWith('callRtdb'); - expect(addSpy).to.have.been.calledWith('callFirestore'); - expect(addSpy).to.have.been.calledWith('getAuthUser'); + it('calls task with parameters', async () => { + const uid = 'TESTING_USER_UID'; + await loadedCustomCommands.authGetUser(uid); + expect(taskSpy).to.have.been.calledWith('authGetUser', { + uid, + tenantId: false, + }); + }); }); + describe('cy.authGetUserByEmail', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authGetUserByEmail'); + }); - it('Aliases callRtdb command', () => { - const commandNames = { callRtdb: 'testing' }; - attachCustomCommands({ cy, Cypress, firebase }, { commandNames }); - expect(addSpy).to.have.been.calledWith(commandNames.callRtdb); - expect(addSpy).to.have.been.calledWith('login'); - expect(addSpy).to.have.been.calledWith('logout'); - expect(addSpy).to.have.been.calledWith('callFirestore'); - expect(addSpy).to.have.been.calledWith('getAuthUser'); + it('calls task with parameters', async () => { + const email = 'testuser@email.com'; + await loadedCustomCommands.authGetUserByEmail(email); + expect(taskSpy).to.have.been.calledWith('authGetUserByEmail', { + email, + tenantId: false, + }); + }); }); + describe('cy.authGetUserByPhoneNumber', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authGetUserByPhoneNumber'); + }); - it('Aliases callFirestore command', () => { - const commandNames = { callFirestore: 'testing' }; - attachCustomCommands({ cy, Cypress, firebase }, { commandNames }); - expect(addSpy).to.have.been.calledWith(commandNames.callFirestore); - expect(addSpy).to.have.been.calledWith('login'); - expect(addSpy).to.have.been.calledWith('logout'); - expect(addSpy).to.have.been.calledWith('callRtdb'); - expect(addSpy).to.have.been.calledWith('getAuthUser'); + it('calls task with parameters', async () => { + const phoneNumber = 'A_PHONE_NUMBER'; + await loadedCustomCommands.authGetUserByPhoneNumber(phoneNumber); + expect(taskSpy).to.have.been.calledWith('authGetUserByPhoneNumber', { + phoneNumber, + tenantId: false, + }); + }); }); + describe('cy.authGetUserByProviderUid', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authGetUserByProviderUid'); + }); - it('Aliases getAuthUser command', () => { - const commandNames = { getAuthUser: 'testing' }; - attachCustomCommands({ cy, Cypress, firebase }, { commandNames }); - expect(addSpy).to.have.been.calledWith(commandNames.getAuthUser); - expect(addSpy).to.have.been.calledWith('login'); - expect(addSpy).to.have.been.calledWith('logout'); - expect(addSpy).to.have.been.calledWith('callRtdb'); - expect(addSpy).to.have.been.calledWith('callFirestore'); + it('calls task with parameters', async () => { + const providerId = 'PROVIDER_ID'; + const uid = 'TESTING_USER_UID'; + await loadedCustomCommands.authGetUserByProviderUid(providerId, uid); + expect(taskSpy).to.have.been.calledWith('authGetUserByProviderUid', { + providerId, + uid, + tenantId: false, + }); + }); + }); + describe('cy.authGetUsers', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authGetUsers'); + }); + + it('calls task with parameters', async () => { + const uid = 'TESTING_USER_UID'; + const email = 'testuser@email.com'; + const phoneNumber = 'A_PHONE_NUMBER'; + const providerId = 'PROVIDER_ID'; + const identifiers = [ + { uid }, + { email }, + { phoneNumber }, + { providerId, providerUid: uid }, + ]; + await loadedCustomCommands.authGetUsers(identifiers); + expect(taskSpy).to.have.been.calledWith('authGetUsers', { + identifiers, + tenantId: false, + }); + }); + }); + describe('cy.authUpdateUser', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authUpdateUser'); + }); + + it('calls task with parameters', async () => { + const uid = 'TESTING_USER_UID'; + const update = { displayName: 'Test User' }; + await loadedCustomCommands.authUpdateUser(uid, update); + expect(taskSpy).to.have.been.calledWith('authUpdateUser', { + uid, + properties: update, + tenantId: false, + }); + }); + }); + describe('cy.authSetCustomUserClaims', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authSetCustomUserClaims'); + }); + + it('calls task with parameters', async () => { + const uid = 'TESTING_USER_UID'; + const claims = { role: 'Admin' }; + await loadedCustomCommands.authSetCustomUserClaims(uid, claims); + expect(taskSpy).to.have.been.calledWith('authSetCustomUserClaims', { + uid, + customClaims: claims, + tenantId: false, + }); + }); + }); + describe('cy.authDeleteUser', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authDeleteUser'); + }); + + it('calls task with parameters', async () => { + const uid = 'TESTING_USER_UID'; + await loadedCustomCommands.authDeleteUser(uid); + expect(taskSpy).to.have.been.calledWith('authDeleteUser', { + uid, + tenantId: false, + }); + }); + }); + describe('cy.authDeleteUsers', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authDeleteUsers'); + }); + + it('calls task with parameters', async () => { + const uid1 = 'TESTING_USER_UID'; + const uid2 = 'TESTING_USER_UID'; + await loadedCustomCommands.authDeleteUsers([uid1, uid2]); + expect(taskSpy).to.have.been.calledWith('authDeleteUsers', { + uids: [uid1, uid2], + tenantId: false, + }); + }); + }); + describe('cy.authCreateCustomToken', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authCreateCustomToken'); + }); + + it('calls task with parameters', async () => { + const uid = 'TESTING_USER_UID'; + const claims = { role: 'Admin' }; + await loadedCustomCommands.authCreateCustomToken(uid, claims); + expect(taskSpy).to.have.been.calledWith('authCreateCustomToken', { + uid, + customClaims: claims, + tenantId: false, + }); + }); + }); + describe('cy.authCreateSessionCookie', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authCreateSessionCookie'); + }); + + it('calls task with parameters', async () => { + const idToken = 'TESTING'; + const cookieOpts = { expiresIn: 60 * 60 * 24 * 5 }; + await loadedCustomCommands.authCreateSessionCookie(idToken, cookieOpts); + expect(taskSpy).to.have.been.calledWith('authCreateSessionCookie', { + idToken, + sessionCookieOptions: cookieOpts, + tenantId: false, + }); + }); + }); + describe('cy.authVerifyIdToken', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authVerifyIdToken'); + }); + + it('calls task with parameters', async () => { + const idToken = 'TESTING'; + const checkRevoked = true; + await loadedCustomCommands.authVerifyIdToken(idToken, checkRevoked); + expect(taskSpy).to.have.been.calledWith('authVerifyIdToken', { + idToken, + checkRevoked, + tenantId: false, + }); + }); + }); + describe('cy.authRevokeRefreshTokens', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authRevokeRefreshTokens'); + }); + + it('calls task with parameters', async () => { + const uid = 'TESTING_USER_UID'; + await loadedCustomCommands.authRevokeRefreshTokens(uid); + expect(taskSpy).to.have.been.calledWith('authRevokeRefreshTokens', { + uid, + tenantId: false, + }); + }); + }); + describe('cy.authGenerateEmailVerificationLink', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith( + 'authGenerateEmailVerificationLink', + ); + }); + + it('calls task with parameters', async () => { + const email = 'testuser@email.com'; + const actionCodeSettings = { url: 'https://example.com' }; + await loadedCustomCommands.authGenerateEmailVerificationLink( + email, + actionCodeSettings, + ); + expect(taskSpy).to.have.been.calledWith( + 'authGenerateEmailVerificationLink', + { + email, + actionCodeSettings, + tenantId: false, + }, + ); + }); + }); + describe('cy.authGeneratePasswordResetLink', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authGeneratePasswordResetLink'); + }); + + it('calls task with parameters', async () => { + const email = 'testuser@email.com'; + const actionCodeSettings = { url: 'https://example.com' }; + await loadedCustomCommands.authGeneratePasswordResetLink( + email, + actionCodeSettings, + ); + expect(taskSpy).to.have.been.calledWith( + 'authGeneratePasswordResetLink', + { + email, + actionCodeSettings, + tenantId: false, + }, + ); + }); + }); + describe('cy.authGenerateSignInWithEmailLink', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith( + 'authGenerateSignInWithEmailLink', + ); + }); + + it('calls task with parameters', async () => { + const email = 'testuser@email.com'; + const actionCodeSettings = { url: 'https://example.com' }; + await loadedCustomCommands.authGenerateSignInWithEmailLink( + email, + actionCodeSettings, + ); + expect(taskSpy).to.have.been.calledWith( + 'authGenerateSignInWithEmailLink', + { + email, + actionCodeSettings, + tenantId: false, + }, + ); + }); + }); + describe('cy.authGenerateVerifyAndChangeEmailLink', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith( + 'authGenerateVerifyAndChangeEmailLink', + ); + }); + + it('calls task with parameters', async () => { + const email = 'testuser@email.com'; + const newEmail = 'second@email.com'; + const actionCodeSettings = { url: 'https://example.com' }; + await loadedCustomCommands.authGenerateVerifyAndChangeEmailLink( + email, + newEmail, + actionCodeSettings, + ); + expect(taskSpy).to.have.been.calledWith( + 'authGenerateVerifyAndChangeEmailLink', + { + email, + newEmail, + actionCodeSettings, + tenantId: false, + }, + ); + }); + }); + describe('cy.authCreateProviderConfig', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authCreateProviderConfig'); + }); + + it('calls task with parameters', async () => { + const providerConfig = { + clientId: 'CLIENT_ID', + issuer: 'ISSUER', + }; + await loadedCustomCommands.authCreateProviderConfig(providerConfig); + expect(taskSpy).to.have.been.calledWith('authCreateProviderConfig', { + providerConfig, + tenantId: false, + }); + }); + }); + describe('cy.authGetProviderConfig', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authGetProviderConfig'); + }); + + it('calls task with parameters', async () => { + const providerId = 'PROVIDER_ID'; + await loadedCustomCommands.authGetProviderConfig(providerId); + expect(taskSpy).to.have.been.calledWith('authGetProviderConfig', { + providerId, + tenantId: false, + }); + }); + }); + describe('cy.authListProviderConfigs', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authListProviderConfigs'); + }); + + it('calls task with parameters', async () => { + const providerFilter = { type: 'oidc', maxResults: 2 }; + await loadedCustomCommands.authListProviderConfigs(providerFilter); + expect(taskSpy).to.have.been.calledWith('authListProviderConfigs', { + providerFilter, + tenantId: false, + }); + }); + }); + describe('cy.authUpdateProviderConfig', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authUpdateProviderConfig'); + }); + + it('calls task with parameters', async () => { + const providerId = 'PROVIDER_ID'; + const providerConfig = { + clientId: 'CLIENT_ID', + issuer: 'ISSUER', + }; + await loadedCustomCommands.authUpdateProviderConfig( + providerId, + providerConfig, + ); + expect(taskSpy).to.have.been.calledWith('authUpdateProviderConfig', { + providerId, + providerConfig, + tenantId: false, + }); + }); + }); + describe('cy.authDeleteProviderConfig', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('authDeleteProviderConfig'); + }); + + it('calls task with parameters', async () => { + const providerId = 'PROVIDER_ID'; + await loadedCustomCommands.authDeleteProviderConfig(providerId); + expect(taskSpy).to.have.been.calledWith('authDeleteProviderConfig', { + providerId, + tenantId: false, + }); + }); + }); + }); + + describe('cy.createUserWithClaims', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('createUserWithClaims'); + }); + }); + + describe('cy.deleteAllAuthUsers', () => { + it('is attached as a custom command', () => { + expect(addSpy).to.have.been.calledWith('deleteAllAuthUsers'); + }); + }); + + describe('options', () => { + allCommandNames.forEach((commandName) => { + it(`Aliases ${commandName} command`, () => { + const commandNames = { [commandName]: 'testing' }; + attachCustomCommands({ cy, Cypress, firebase }, { commandNames }); + expect(addSpy).to.have.been.calledWith('testing'); + allCommandNames + .filter((name) => name !== commandName) + .forEach((name) => { + expect(addSpy).to.have.been.calledWith(name); + }); + }); }); }); }); diff --git a/test/unit/plugin.spec.ts b/test/unit/plugin.spec.ts index fbe9cf7f..ad8bb1b3 100644 --- a/test/unit/plugin.spec.ts +++ b/test/unit/plugin.spec.ts @@ -58,9 +58,9 @@ describe('plugin', () => { ); expect(results).to.be.an('object'); expect(onFuncSpy).to.have.been.calledOnceWith('task'); - expect(assignedTasksObj).to.have.property('createCustomToken'); + expect(assignedTasksObj).to.have.property('authCreateCustomToken'); const uid = 'SomeUid'; - (assignedTasksObj as any).createCustomToken({ uid }); + (assignedTasksObj as any).authCreateCustomToken({ uid }); expect(createCustomTokenSpy).to.have.been.calledWith(uid); }); });