1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 07:25:19 +02:00

update e2e tests to work with new auth

This commit is contained in:
Tyler Myracle 2024-01-19 01:57:53 -06:00
parent 3bc4ac1687
commit 5375affd8e
10 changed files with 71 additions and 184 deletions

View file

@ -57,6 +57,7 @@ export default function LoginPage() {
<form className="space-y-4 w-full px-4" onSubmit={onSubmit}> <form className="space-y-4 w-full px-4" onSubmit={onSubmit}>
<Input <Input
type="text" type="text"
name="email"
label="Email" label="Email"
value={email} value={email}
onChange={(e) => setEmail(e.currentTarget.value)} onChange={(e) => setEmail(e.currentTarget.value)}
@ -64,6 +65,7 @@ export default function LoginPage() {
<InputPassword <InputPassword
autoComplete="password" autoComplete="password"
name="password"
label="Password" label="Password"
value={password} value={password}
showPasswordRequirements={!isValid} showPasswordRequirements={!isValid}

View file

@ -63,18 +63,21 @@ export default function RegisterPage() {
<form className="space-y-4 w-full px-4" onSubmit={onSubmit}> <form className="space-y-4 w-full px-4" onSubmit={onSubmit}>
<Input <Input
type="text" type="text"
name="firstName"
label="First name" label="First name"
value={firstName} value={firstName}
onChange={(e) => setFirstName(e.currentTarget.value)} onChange={(e) => setFirstName(e.currentTarget.value)}
/> />
<Input <Input
type="text" type="text"
name="lastName"
label="Last name" label="Last name"
value={lastName} value={lastName}
onChange={(e) => setLastName(e.currentTarget.value)} onChange={(e) => setLastName(e.currentTarget.value)}
/> />
<Input <Input
type="text" type="text"
name="email"
label="Email" label="Email"
value={email} value={email}
onChange={(e) => setEmail(e.currentTarget.value)} onChange={(e) => setEmail(e.currentTarget.value)}
@ -82,6 +85,7 @@ export default function RegisterPage() {
<InputPassword <InputPassword
autoComplete="password" autoComplete="password"
name="password"
label="Password" label="Password"
value={password} value={password}
showPasswordRequirements={!isValid} showPasswordRequirements={!isValid}

View file

@ -11,14 +11,10 @@ export default defineConfig({
baseUrl: 'http://localhost:4200', baseUrl: 'http://localhost:4200',
env: { env: {
API_URL: 'http://localhost:3333/v1', API_URL: 'http://localhost:3333/v1',
AUTH0_ID: 'REPLACE_THIS', NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
AUTH0_CLIENT_ID: 'REPLACE_THIS', NEXT_PUBLIC_NEXTAUTH_URL: 'http://localhost:4200',
AUTH0_NAME: 'Engineering CI', NEXTAUTH_URL: process.env.NEXTAUTH_URL,
AUTH0_EMAIL: 'REPLACE_THIS', STRIPE_WEBHOOK_SECRET: 'REPLACE_THIS',
AUTH0_PASSWORD: 'REPLACE_THIS',
AUTH0_DOMAIN: 'REPLACE_THIS',
STRIPE_WEBHOOK_SECRET:
'REPLACE_THIS',
STRIPE_CUSTOMER_ID: 'REPLACE_THIS', STRIPE_CUSTOMER_ID: 'REPLACE_THIS',
STRIPE_SUBSCRIPTION_ID: 'REPLACE_THIS', STRIPE_SUBSCRIPTION_ID: 'REPLACE_THIS',
}, },

View file

@ -19,81 +19,6 @@ function openEditAccountModal() {
} }
describe('Accounts', () => { describe('Accounts', () => {
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', () => { it('should interpolate and display manual vehicle account data', () => {
cy.getByTestId('add-account-button').click() cy.getByTestId('add-account-button').click()
cy.contains('h4', 'Add account') cy.contains('h4', 'Add account')
@ -149,7 +74,8 @@ describe('Accounts', () => {
cy.contains('h4', 'Add real estate') cy.contains('h4', 'Add real estate')
// Details // 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="line1"]').focus().type('123 Example St')
cy.get('input[name="city"]').type('New York') cy.get('input[name="city"]').type('New York')
cy.get('input[name="state"]').type('NY') cy.get('input[name="state"]').type('NY')
@ -188,7 +114,9 @@ describe('Accounts', () => {
openEditAccountModal() openEditAccountModal()
cy.getByTestId('property-form').within(() => { 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"]') cy.get('input[name="line1"]')
.should('have.value', '123 Example St') .should('have.value', '123 Example St')
.clear() .clear()

View file

@ -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')
})
})
})

View file

@ -1,17 +1,13 @@
import jwtDecode from 'jwt-decode' // eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
declare global { // eslint-disable-next-line @typescript-eslint/no-unused-vars
// eslint-disable-next-line @typescript-eslint/no-namespace interface Chainable<Subject> {
namespace Cypress { login(): Chainable<any>
// eslint-disable-next-line @typescript-eslint/no-unused-vars apiRequest(...params: Parameters<typeof cy.request>): Chainable<any>
interface Chainable<Subject> { getByTestId(...parameters: Parameters<typeof cy.get>): Chainable<any>
login(username: string, password: string): Chainable<any> selectDate(date: Date): Chainable<any>
apiRequest(...params: Parameters<typeof cy.request>): Chainable<any> preserveAccessToken(): Chainable<any>
getByTestId(...parameters: Parameters<typeof cy.get>): Chainable<any> restoreAccessToken(): Chainable<any>
selectDate(date: Date): Chainable<any>
preserveAccessToken(): Chainable<any>
restoreAccessToken(): Chainable<any>
}
} }
} }
@ -20,13 +16,10 @@ Cypress.Commands.add('getByTestId', (testId, ...rest) => {
}) })
Cypress.Commands.add('apiRequest', ({ url, headers = {}, ...options }, ...rest) => { Cypress.Commands.add('apiRequest', ({ url, headers = {}, ...options }, ...rest) => {
const accessToken = window.localStorage.getItem('token')
return cy.request( return cy.request(
{ {
url: `${Cypress.env('API_URL')}/${url}`, url: `${Cypress.env('API_URL')}/${url}`,
headers: { headers: {
Authorization: `Bearer ${accessToken}`,
...headers, ...headers,
}, },
...options, ...options,
@ -35,74 +28,11 @@ Cypress.Commands.add('apiRequest', ({ url, headers = {}, ...options }, ...rest)
) )
}) })
/** Cypress.Commands.add('login', () => {
* Logs in with the engineering CI account cy.visit('/login')
*/ cy.get('input[name="email"]').type('bond@007.com')
Cypress.Commands.add('login', (username, password) => { cy.get('input[name="password"]').type('TestPassword123')
// Preserves login across tests cy.get('button[type="submit"]').click()
cy.session('login-session-key', login, { //eslint-disable-next-line cypress/no-unnecessary-waiting
validate() { cy.wait(1000)
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<any>(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('/')
})
}
}) })

View file

@ -2,8 +2,7 @@ import './commands'
beforeEach(() => { beforeEach(() => {
// Login // 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()
cy.login(Cypress.env('AUTH0_EMAIL'), Cypress.env('AUTH0_PASSWORD'))
// Delete the current user to wipe all data before test // Delete the current user to wipe all data before test
cy.apiRequest({ cy.apiRequest({
@ -14,6 +13,6 @@ beforeEach(() => {
expect(response.status).to.equal(200) expect(response.status).to.equal(200)
}) })
// Re-login (JWT should still be valid) // Go back to dashboard
cy.visit('/') cy.visit('/')
}) })

View file

@ -0,0 +1,2 @@
import './commands'
import './e2e'

View file

@ -6,13 +6,13 @@ import endpoint from '../lib/endpoint'
const router = Router() const router = Router()
router.use((req, res, next) => { const testUserId = 'test_ec3ee8a4-fa01-4f11-8ac5-9c49dd7fbae4'
const roles = req.user?.['https://maybe.co/roles']
if (roles?.includes('CIUser') || roles?.includes('Admin')) { router.use((req, res, next) => {
if (req.user?.sub === testUserId) {
next() next()
} else { } 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), trialLapsed: z.boolean().default(false),
}), }),
resolve: async ({ ctx, input }) => { resolve: async ({ ctx, input }) => {
ctx.logger.debug(`Resetting CI user ${ctx.user!.authId}`)
await ctx.prisma.$transaction([ 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({ ctx.prisma.user.create({
data: { data: {
authId: ctx.user!.authId, authId: testUserId,
email: 'REPLACE_THIS', email: 'bond@007.com',
firstName: 'James',
lastName: 'Bond',
dob: new Date('1990-01-01'), dob: new Date('1990-01-01'),
linkAccountDismissedAt: new Date(), // ensures our auto-account link doesn't trigger linkAccountDismissedAt: new Date(), // ensures our auto-account link doesn't trigger

View file

@ -1,4 +1,5 @@
import { Institution, PrismaClient, Provider } from '@prisma/client' import { Institution, PrismaClient, Provider } from '@prisma/client'
import bcrypt from 'bcrypt'
const prisma = new PrismaClient() const prisma = new PrismaClient()
@ -27,7 +28,23 @@ async function main() {
}, },
] ]
const hashedPassword = await bcrypt.hash('TestPassword123', 10)
await prisma.$transaction([ 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 // create institution linked to provider institutions
...institutions.map(({ id, name, providers }) => ...institutions.map(({ id, name, providers }) =>
prisma.institution.upsert({ prisma.institution.upsert({