From 5375affd8ebaee3a8f6d4d0837ea354de25e120c Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Fri, 19 Jan 2024 01:57:53 -0600 Subject: [PATCH] update e2e tests to work with new auth --- apps/client/pages/login.tsx | 2 + apps/client/pages/register.tsx | 4 + apps/e2e/cypress.config.ts | 12 +-- apps/e2e/src/e2e/accounts.cy.ts | 82 ++---------------- apps/e2e/src/e2e/auth.cy.ts | 9 ++ apps/e2e/src/support/commands.ts | 104 ++++------------------- apps/e2e/src/support/e2e.ts | 5 +- apps/e2e/src/support/index.ts | 2 + apps/server/src/app/routes/e2e.router.ts | 18 ++-- prisma/seed.ts | 17 ++++ 10 files changed, 71 insertions(+), 184 deletions(-) create mode 100644 apps/e2e/src/e2e/auth.cy.ts create mode 100644 apps/e2e/src/support/index.ts diff --git a/apps/client/pages/login.tsx b/apps/client/pages/login.tsx index 84634495..aabcc715 100644 --- a/apps/client/pages/login.tsx +++ b/apps/client/pages/login.tsx @@ -57,6 +57,7 @@ export default function LoginPage() {
setEmail(e.currentTarget.value)} @@ -64,6 +65,7 @@ export default function LoginPage() { setFirstName(e.currentTarget.value)} /> setLastName(e.currentTarget.value)} /> setEmail(e.currentTarget.value)} @@ -82,6 +85,7 @@ export default function RegisterPage() { { - it('should sync and edit a plaid connection', () => { - cy.apiRequest({ - method: 'POST', - url: 'e2e/plaid/connect', - }).then((response) => { - expect(response.status).to.eql(200) - - // The only time we need to manually send a Plaid webhook is in Github actions when testing a PR - if (Cypress.env('WEBHOOK_TYPE') === 'mock') { - const { plaidItemId } = response.body.data.json - - cy.request({ - method: 'POST', - url: `${Cypress.env('API_URL')}/plaid/webhook`, - body: { - webhook_type: 'TRANSACTIONS', - webhook_code: 'HISTORICAL_UPDATE', - item_id: plaidItemId, - }, - }) - .its('status') - .should('equal', 200) - } - }) - - // Check account sidebar names and balances - cy.visit('/accounts') - cy.getByTestId('account-group', { timeout: 20_000 }) - - assertSidebarAccounts([ - ['Assets', '$20,000'], - ['Cash', '$20,000'], - ['Sandbox Savings', '$15,000'], - ['Sandbox Checking', '$5,000'], - ['Debts', '$950'], - ['Credit Cards', '$950'], - ['Sandbox CC', '$950'], - ]) - - // Check current net worth - cy.visit('/') - cy.getByTestId('current-data-value').should('contain.text', '$19,050.00') - - // Visit each account page, edit details, re-validate amounts - cy.contains('a', 'Sandbox Checking').click() - cy.getByTestId('current-data-value').should('contain.text', '$5,000.00') - - cy.contains('a', 'Sandbox Savings').click() - cy.getByTestId('current-data-value').should('contain.text', '$15,000.00') - - cy.contains('a', 'Sandbox CC').click() - cy.getByTestId('current-data-value').should('contain.text', '$950.00') - - openEditAccountModal() - - cy.getByTestId('connected-account-form').within(() => { - cy.get('input[name="name"]') - .should('have.value', 'Sandbox CC') - .clear() - .type('Credit Credit') - cy.get('input[name="categoryUser"]').should('have.value', 'credit') - cy.get('input[name="startDate"]') - .should('have.value', '') - .type(DateTime.now().minus({ months: 1 }).toFormat('MMddyyyy')) - cy.root().submit() - }) - - // Should be able to submit empty start date on connected account - openEditAccountModal() - cy.getByTestId('connected-account-form').within(() => { - cy.get('input[name="startDate"]').clear() - cy.root().submit() - }) - }) - it('should interpolate and display manual vehicle account data', () => { cy.getByTestId('add-account-button').click() cy.contains('h4', 'Add account') @@ -149,7 +74,8 @@ describe('Accounts', () => { cy.contains('h4', 'Add real estate') // Details - cy.get('input[name="country"]').select('GB') + cy.contains('label', 'Country').click() + cy.contains('button', 'Uganda').click() cy.get('input[name="line1"]').focus().type('123 Example St') cy.get('input[name="city"]').type('New York') cy.get('input[name="state"]').type('NY') @@ -188,7 +114,9 @@ describe('Accounts', () => { openEditAccountModal() cy.getByTestId('property-form').within(() => { - cy.get('input[name="country]').should('have.value', 'GB').clear().select('FR') + cy.get('input[name="country"]').should('have.value', 'UG') + cy.contains('label', 'Country').click() + cy.contains('button', 'United States').click() cy.get('input[name="line1"]') .should('have.value', '123 Example St') .clear() diff --git a/apps/e2e/src/e2e/auth.cy.ts b/apps/e2e/src/e2e/auth.cy.ts new file mode 100644 index 00000000..8150a9ea --- /dev/null +++ b/apps/e2e/src/e2e/auth.cy.ts @@ -0,0 +1,9 @@ +describe('Auth', () => { + beforeEach(() => cy.visit('/')) + + describe('Logging in', () => { + it('should show the home page of an authenticated user', () => { + cy.contains('h5', 'Assets & Debts') + }) + }) +}) diff --git a/apps/e2e/src/support/commands.ts b/apps/e2e/src/support/commands.ts index 9084edc3..676fb30b 100644 --- a/apps/e2e/src/support/commands.ts +++ b/apps/e2e/src/support/commands.ts @@ -1,17 +1,13 @@ -import jwtDecode from 'jwt-decode' - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Cypress { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Chainable { - login(username: string, password: string): Chainable - apiRequest(...params: Parameters): Chainable - getByTestId(...parameters: Parameters): Chainable - selectDate(date: Date): Chainable - preserveAccessToken(): Chainable - restoreAccessToken(): Chainable - } +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace Cypress { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Chainable { + login(): Chainable + apiRequest(...params: Parameters): Chainable + getByTestId(...parameters: Parameters): Chainable + selectDate(date: Date): Chainable + preserveAccessToken(): Chainable + restoreAccessToken(): Chainable } } @@ -20,13 +16,10 @@ Cypress.Commands.add('getByTestId', (testId, ...rest) => { }) Cypress.Commands.add('apiRequest', ({ url, headers = {}, ...options }, ...rest) => { - const accessToken = window.localStorage.getItem('token') - return cy.request( { url: `${Cypress.env('API_URL')}/${url}`, headers: { - Authorization: `Bearer ${accessToken}`, ...headers, }, ...options, @@ -35,74 +28,11 @@ Cypress.Commands.add('apiRequest', ({ url, headers = {}, ...options }, ...rest) ) }) -/** - * Logs in with the engineering CI account - */ -Cypress.Commands.add('login', (username, password) => { - // Preserves login across tests - cy.session('login-session-key', login, { - validate() { - cy.apiRequest({ url: 'e2e' }).its('status').should('eq', 200) - }, - }) - - function login() { - const client_id = Cypress.env('AUTH0_CLIENT_ID') - const audience = 'https://maybe-finance-api/v1' - const scope = 'openid profile email offline_access' - const accessTokenStorageKey = `@@auth0spajs@@::${client_id}::${audience}::${scope}` - const AUTH_DOMAIN = Cypress.env('AUTH0_DOMAIN') - - cy.log(`Logging in as ${username}`) - - /** - * Uses the official Cypress Auth0 strategy for testing with Auth0 - * https://docs.cypress.io/guides/testing-strategies/auth0-authentication#Auth0-Application-Setup - * - * Relevant Auth0 endpoint - * https://auth0.com/docs/api/authentication?javascript#resource-owner-password - */ - cy.request({ - method: 'POST', - url: `https://${AUTH_DOMAIN}/oauth/token`, - body: { - grant_type: 'password', - username, - password, - audience, - scope, - client_id, - }, - }).then(({ body }) => { - const claims = jwtDecode(body.id_token) - - const { nickname, name, picture, updated_at, email, email_verified, sub, exp } = claims - - const item = { - body: { - ...body, - decodedToken: { - claims, - user: { - nickname, - name, - picture, - updated_at, - email, - email_verified, - sub, - }, - audience, - client_id, - }, - }, - expiresAt: exp, - } - - window.localStorage.setItem(accessTokenStorageKey, JSON.stringify(item)) - window.localStorage.setItem('token', body.access_token) - - cy.visit('/') - }) - } +Cypress.Commands.add('login', () => { + cy.visit('/login') + cy.get('input[name="email"]').type('bond@007.com') + cy.get('input[name="password"]').type('TestPassword123') + cy.get('button[type="submit"]').click() + //eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(1000) }) diff --git a/apps/e2e/src/support/e2e.ts b/apps/e2e/src/support/e2e.ts index a8e64fd1..be528804 100644 --- a/apps/e2e/src/support/e2e.ts +++ b/apps/e2e/src/support/e2e.ts @@ -2,8 +2,7 @@ import './commands' beforeEach(() => { // Login - // Rate limit 30 / min - https://auth0.com/docs/troubleshoot/customer-support/operational-policies/rate-limit-policy#limits-for-non-production-tenants-of-paying-customers-and-all-tenants-of-free-customers - cy.login(Cypress.env('AUTH0_EMAIL'), Cypress.env('AUTH0_PASSWORD')) + cy.login() // Delete the current user to wipe all data before test cy.apiRequest({ @@ -14,6 +13,6 @@ beforeEach(() => { expect(response.status).to.equal(200) }) - // Re-login (JWT should still be valid) + // Go back to dashboard cy.visit('/') }) diff --git a/apps/e2e/src/support/index.ts b/apps/e2e/src/support/index.ts new file mode 100644 index 00000000..5e5628a4 --- /dev/null +++ b/apps/e2e/src/support/index.ts @@ -0,0 +1,2 @@ +import './commands' +import './e2e' diff --git a/apps/server/src/app/routes/e2e.router.ts b/apps/server/src/app/routes/e2e.router.ts index 0b4bc830..5b9edb29 100644 --- a/apps/server/src/app/routes/e2e.router.ts +++ b/apps/server/src/app/routes/e2e.router.ts @@ -6,13 +6,13 @@ import endpoint from '../lib/endpoint' const router = Router() -router.use((req, res, next) => { - const roles = req.user?.['https://maybe.co/roles'] +const testUserId = 'test_ec3ee8a4-fa01-4f11-8ac5-9c49dd7fbae4' - if (roles?.includes('CIUser') || roles?.includes('Admin')) { +router.use((req, res, next) => { + if (req.user?.sub === testUserId) { next() } else { - res.status(401).send('Route only available to CIUser and Admin roles') + res.status(401).send('Route only available to test users') } }) @@ -47,14 +47,14 @@ router.post( trialLapsed: z.boolean().default(false), }), resolve: async ({ ctx, input }) => { - ctx.logger.debug(`Resetting CI user ${ctx.user!.authId}`) - await ctx.prisma.$transaction([ - ctx.prisma.$executeRaw`DELETE FROM "user" WHERE auth_id=${ctx.user!.authId};`, + ctx.prisma.$executeRaw`DELETE FROM "user" WHERE auth_id=${testUserId};`, ctx.prisma.user.create({ data: { - authId: ctx.user!.authId, - email: 'REPLACE_THIS', + authId: testUserId, + email: 'bond@007.com', + firstName: 'James', + lastName: 'Bond', dob: new Date('1990-01-01'), linkAccountDismissedAt: new Date(), // ensures our auto-account link doesn't trigger diff --git a/prisma/seed.ts b/prisma/seed.ts index da910b02..7448631e 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,5 @@ import { Institution, PrismaClient, Provider } from '@prisma/client' +import bcrypt from 'bcrypt' const prisma = new PrismaClient() @@ -27,7 +28,23 @@ async function main() { }, ] + const hashedPassword = await bcrypt.hash('TestPassword123', 10) + await prisma.$transaction([ + // create testing auth user + prisma.authUser.upsert({ + where: { + id: 'test_ec3ee8a4-fa01-4f11-8ac5-9c49dd7fbae4', + }, + create: { + id: 'test_ec3ee8a4-fa01-4f11-8ac5-9c49dd7fbae4', + firstName: 'James', + lastName: 'Bond', + email: 'bond@007.com', + password: hashedPassword, + }, + update: {}, + }), // create institution linked to provider institutions ...institutions.map(({ id, name, providers }) => prisma.institution.upsert({