mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-08 15:05:22 +02:00
update e2e tests to work with new auth
This commit is contained in:
parent
3bc4ac1687
commit
5375affd8e
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}>
|
||||
<Input
|
||||
type="text"
|
||||
name="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.currentTarget.value)}
|
||||
|
@ -64,6 +65,7 @@ export default function LoginPage() {
|
|||
|
||||
<InputPassword
|
||||
autoComplete="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
showPasswordRequirements={!isValid}
|
||||
|
|
|
@ -63,18 +63,21 @@ export default function RegisterPage() {
|
|||
<form className="space-y-4 w-full px-4" onSubmit={onSubmit}>
|
||||
<Input
|
||||
type="text"
|
||||
name="firstName"
|
||||
label="First name"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.currentTarget.value)}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
name="lastName"
|
||||
label="Last name"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.currentTarget.value)}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
name="email"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.currentTarget.value)}
|
||||
|
@ -82,6 +85,7 @@ export default function RegisterPage() {
|
|||
|
||||
<InputPassword
|
||||
autoComplete="password"
|
||||
name="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
showPasswordRequirements={!isValid}
|
||||
|
|
|
@ -11,14 +11,10 @@ export default defineConfig({
|
|||
baseUrl: 'http://localhost:4200',
|
||||
env: {
|
||||
API_URL: 'http://localhost:3333/v1',
|
||||
AUTH0_ID: 'REPLACE_THIS',
|
||||
AUTH0_CLIENT_ID: 'REPLACE_THIS',
|
||||
AUTH0_NAME: 'Engineering CI',
|
||||
AUTH0_EMAIL: 'REPLACE_THIS',
|
||||
AUTH0_PASSWORD: 'REPLACE_THIS',
|
||||
AUTH0_DOMAIN: 'REPLACE_THIS',
|
||||
STRIPE_WEBHOOK_SECRET:
|
||||
'REPLACE_THIS',
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
NEXT_PUBLIC_NEXTAUTH_URL: 'http://localhost:4200',
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
||||
STRIPE_WEBHOOK_SECRET: 'REPLACE_THIS',
|
||||
STRIPE_CUSTOMER_ID: 'REPLACE_THIS',
|
||||
STRIPE_SUBSCRIPTION_ID: 'REPLACE_THIS',
|
||||
},
|
||||
|
|
|
@ -19,81 +19,6 @@ function openEditAccountModal() {
|
|||
}
|
||||
|
||||
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', () => {
|
||||
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()
|
||||
|
|
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'
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Chainable<Subject> {
|
||||
login(username: string, password: string): Chainable<any>
|
||||
apiRequest(...params: Parameters<typeof cy.request>): Chainable<any>
|
||||
getByTestId(...parameters: Parameters<typeof cy.get>): Chainable<any>
|
||||
selectDate(date: Date): Chainable<any>
|
||||
preserveAccessToken(): Chainable<any>
|
||||
restoreAccessToken(): Chainable<any>
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
declare namespace Cypress {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Chainable<Subject> {
|
||||
login(): Chainable<any>
|
||||
apiRequest(...params: Parameters<typeof cy.request>): Chainable<any>
|
||||
getByTestId(...parameters: Parameters<typeof cy.get>): 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) => {
|
||||
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<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('/')
|
||||
})
|
||||
}
|
||||
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)
|
||||
})
|
||||
|
|
|
@ -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('/')
|
||||
})
|
||||
|
|
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()
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue