mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 07:25:19 +02:00
Merge pull request #159 from tmyracle/fix-test-suites
Update e2e tests to work with new auth
This commit is contained in:
commit
a33cef920f
10 changed files with 71 additions and 184 deletions
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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',
|
||||||
},
|
},
|
||||||
|
|
|
@ -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()
|
||||||
|
|
9
apps/e2e/src/e2e/auth.cy.ts
Normal file
9
apps/e2e/src/e2e/auth.cy.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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('/')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -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('/')
|
||||||
})
|
})
|
||||||
|
|
2
apps/e2e/src/support/index.ts
Normal file
2
apps/e2e/src/support/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import './commands'
|
||||||
|
import './e2e'
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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({
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue