1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

Merge pull request #175 from tmyracle/remove-plaid-from-tests

Remove Plaid from tests
This commit is contained in:
Josh Pigford 2024-01-20 16:51:34 -06:00 committed by GitHub
commit 0d5d7d5a7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 124 additions and 143 deletions

View file

@ -37,7 +37,7 @@ And dozens upon dozens of smaller features.
This is the current state of building the app. We're actively working to make this process much more streamlined! This is the current state of building the app. We're actively working to make this process much more streamlined!
*You'll need Docker installed to run the app locally.* _You'll need Docker installed to run the app locally._
[Docker Desktop](https://www.docker.com/products/docker-desktop/) is an easy way to get started. [Docker Desktop](https://www.docker.com/products/docker-desktop/) is an easy way to get started.
First, copy the `.env.example` file to `.env`: First, copy the `.env.example` file to `.env`:

View file

@ -44,7 +44,7 @@ const WithAuth = function ({ children }: PropsWithChildren) {
} }
}, [session, status, router]) }, [session, status, router])
if (session) { if (session && status === 'authenticated') {
return ( return (
<OnboardingGuard> <OnboardingGuard>
<UserAccountContextProvider> <UserAccountContextProvider>

View file

@ -1,21 +1,14 @@
import type { AxiosInstance } from 'axios' import type { AxiosInstance } from 'axios'
import type { SharedType } from '@maybe-finance/shared' import type { Prisma, AccountConnection, User } from '@prisma/client'
import type { Prisma, AccountConnection, AccountSyncStatus, User } from '@prisma/client' import { AccountConnectionType, AccountSyncStatus } from '@prisma/client'
import type { ItemRemoveResponse } from 'plaid'
import { startServer, stopServer } from './utils/server' import { startServer, stopServer } from './utils/server'
import { getAxiosClient } from './utils/axios' import { getAxiosClient } from './utils/axios'
import prisma from '../lib/prisma' import prisma from '../lib/prisma'
import { TestUtil } from '@maybe-finance/shared'
import { InMemoryQueue } from '@maybe-finance/server/shared' import { InMemoryQueue } from '@maybe-finance/server/shared'
import { default as _plaid } from '../lib/plaid'
import nock from 'nock' import nock from 'nock'
import { resetUser } from './utils/user' import { resetUser } from './utils/user'
jest.mock('../middleware/validate-plaid-jwt.ts') jest.mock('../lib/teller.ts')
jest.mock('plaid')
// For TypeScript support
const plaid = jest.mocked(_plaid)
const authId = '__TEST_USER_ID__' const authId = '__TEST_USER_ID__'
let axios: AxiosInstance let axios: AxiosInstance
@ -49,13 +42,13 @@ beforeEach(async () => {
connectionData = { connectionData = {
data: { data: {
name: 'Chase Test', name: 'Chase Test',
type: 'plaid' as SharedType.AccountConnectionType, type: AccountConnectionType.teller,
plaidItemId: 'test-plaid-item-server', tellerEnrollmentId: 'test-teller-item-workers',
plaidInstitutionId: 'ins_3', tellerInstitutionId: 'chase_test',
plaidAccessToken: tellerAccessToken:
'U2FsdGVkX1+WMq9lfTS9Zkbgrn41+XT1hvSK5ain/udRPujzjVCAx/lyPG7EumVZA+nVKXPauGwI+d7GZgtqTA9R3iCZNusU6LFPnmFOCE4=', 'U2FsdGVkX1+WMq9lfTS9Zkbgrn41+XT1hvSK5ain/udRPujzjVCAx/lyPG7EumVZA+nVKXPauGwI+d7GZgtqTA9R3iCZNusU6LFPnmFOCE4=', // need correct encoding here
userId: user!.id, userId: user.id,
syncStatus: 'PENDING' as AccountSyncStatus, syncStatus: AccountSyncStatus.PENDING,
}, },
} }
@ -91,16 +84,9 @@ describe('/v1/connections API', () => {
}) })
it('DELETE /:id', async () => { it('DELETE /:id', async () => {
plaid.itemRemove.mockResolvedValueOnce(
TestUtil.axiosSuccess<ItemRemoveResponse>({
request_id: 'test request id',
})
)
const res = await axios.delete<AccountConnection>(`/connections/${connection.id}`) const res = await axios.delete<AccountConnection>(`/connections/${connection.id}`)
expect(res.status).toEqual(200) expect(res.status).toEqual(200)
expect(plaid.itemRemove).toHaveBeenCalledTimes(1)
const res2 = await axios.get<AccountConnection>(`/connections/${connection.id}`) const res2 = await axios.get<AccountConnection>(`/connections/${connection.id}`)

View file

@ -5,7 +5,6 @@ import { DateTime } from 'luxon'
import { PgService } from '@maybe-finance/server/shared' import { PgService } from '@maybe-finance/server/shared'
import { AccountQueryService, UserService } from '@maybe-finance/server/features' import { AccountQueryService, UserService } from '@maybe-finance/server/features'
import { resetUser } from './utils/user' import { resetUser } from './utils/user'
jest.mock('plaid')
const prisma = new PrismaClient() const prisma = new PrismaClient()

View file

@ -1,26 +1,21 @@
// ===================================================== // =====================================================
// Keep these imports above the rest to avoid errors // Keep these imports above the rest to avoid errors
// ===================================================== // =====================================================
import type { SharedType } from '@maybe-finance/shared' import { TellerGenerator } from 'tools/generators'
import type { AccountsGetResponse, TransactionsGetResponse } from 'plaid' import type { User, AccountConnection } from '@prisma/client'
import type { AccountConnection, User } from '@prisma/client' import { AccountConnectionType } from '@prisma/client'
import { TestUtil } from '@maybe-finance/shared'
import { PlaidTestData } from '../../../../../tools/test-data'
import { Prisma } from '@prisma/client'
import prisma from '../lib/prisma' import prisma from '../lib/prisma'
import { default as _plaid } from '../lib/plaid' import { default as _teller } from '../lib/teller'
import nock from 'nock'
import { DateTime } from 'luxon'
import { resetUser } from './helpers/user.test-helper' import { resetUser } from './helpers/user.test-helper'
import { Interval } from 'luxon'
// Import the workers process // Import the workers process
import '../../main' import '../../main'
import { queueService, securityPricingService } from '../lib/di' import { queueService } from '../lib/di'
jest.mock('plaid')
// For TypeScript support // For TypeScript support
const plaid = jest.mocked(_plaid) jest.mock('../lib/teller')
const teller = jest.mocked(_teller)
let user: User | null let user: User | null
let connection: AccountConnection let connection: AccountConnection
@ -30,25 +25,6 @@ if (process.env.IS_VSCODE_DEBUG === 'true') {
jest.setTimeout(100000) jest.setTimeout(100000)
} }
beforeAll(() => {
nock.disableNetConnect()
nock('https://api.polygon.io')
.get((uri) => uri.includes('v2/aggs/ticker/AAPL/range/1/day'))
.reply(200, PlaidTestData.AAPL)
.persist()
nock('https://api.polygon.io')
.get((uri) => uri.includes('v2/aggs/ticker/WMT/range/1/day'))
.reply(200, PlaidTestData.WMT)
.persist()
nock('https://api.polygon.io')
.get((uri) => uri.includes('v2/aggs/ticker/VOO/range/1/day'))
.reply(200, PlaidTestData.VOO)
.persist()
})
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
@ -57,10 +33,10 @@ beforeEach(async () => {
connection = await prisma.accountConnection.create({ connection = await prisma.accountConnection.create({
data: { data: {
name: 'Chase Test', name: 'Chase Test',
type: 'plaid' as SharedType.AccountConnectionType, type: AccountConnectionType.teller,
plaidItemId: 'test-plaid-item-workers', tellerEnrollmentId: 'test-teller-item-workers',
plaidInstitutionId: 'ins_3', tellerInstitutionId: 'chase_test',
plaidAccessToken: tellerAccessToken:
'U2FsdGVkX1+WMq9lfTS9Zkbgrn41+XT1hvSK5ain/udRPujzjVCAx/lyPG7EumVZA+nVKXPauGwI+d7GZgtqTA9R3iCZNusU6LFPnmFOCE4=', // need correct encoding here 'U2FsdGVkX1+WMq9lfTS9Zkbgrn41+XT1hvSK5ain/udRPujzjVCAx/lyPG7EumVZA+nVKXPauGwI+d7GZgtqTA9R3iCZNusU6LFPnmFOCE4=', // need correct encoding here
userId: user.id, userId: user.id,
syncStatus: 'PENDING', syncStatus: 'PENDING',
@ -84,7 +60,7 @@ describe('Message queue tests', () => {
it('Should handle sync errors', async () => { it('Should handle sync errors', async () => {
const syncQueue = queueService.getQueue('sync-account-connection') const syncQueue = queueService.getQueue('sync-account-connection')
plaid.accountsGet.mockRejectedValueOnce('forced error for Jest tests') teller.getAccounts.mockRejectedValueOnce(new Error('forced error for Jest tests'))
await syncQueue.add('sync-connection', { accountConnectionId: connection.id }) await syncQueue.add('sync-connection', { accountConnectionId: connection.id })
@ -92,7 +68,7 @@ describe('Message queue tests', () => {
where: { id: connection.id }, where: { id: connection.id },
}) })
expect(plaid.accountsGet).toHaveBeenCalledTimes(1) expect(teller.getAccounts).toHaveBeenCalledTimes(1)
expect(updatedConnection?.status).toEqual('ERROR') expect(updatedConnection?.status).toEqual('ERROR')
}) })
@ -117,28 +93,23 @@ describe('Message queue tests', () => {
const syncQueue = queueService.getQueue('sync-account-connection') const syncQueue = queueService.getQueue('sync-account-connection')
// Mock will return a basic banking checking account // Mock will return a basic banking checking account
plaid.accountsGet.mockResolvedValueOnce( const mockAccounts = TellerGenerator.generateAccountsWithBalances({
TestUtil.axiosSuccess<AccountsGetResponse>({ count: 1,
accounts: [PlaidTestData.checkingAccount], institutionId: 'chase_test',
item: PlaidTestData.item, enrollmentId: 'test-teller-item-workers',
request_id: 'bkVE1BHWMAZ9Rnr', institutionName: 'Chase Test',
}) as any accountType: 'depository',
) accountSubType: 'checking',
})
teller.getAccounts.mockResolvedValueOnce(mockAccounts)
plaid.transactionsGet.mockResolvedValueOnce( const mockTransactions = TellerGenerator.generateTransactions(10, mockAccounts[0].id)
TestUtil.axiosSuccess<TransactionsGetResponse>({ teller.getTransactions.mockResolvedValueOnce(mockTransactions)
accounts: [PlaidTestData.checkingAccount],
transactions: PlaidTestData.checkingTransactions,
item: PlaidTestData.item,
total_transactions: PlaidTestData.checkingTransactions.length,
request_id: '45QSn',
}) as any
)
await syncQueue.add('sync-connection', { accountConnectionId: connection.id }) await syncQueue.add('sync-connection', { accountConnectionId: connection.id })
expect(plaid.accountsGet).toHaveBeenCalledTimes(1) expect(teller.getAccounts).toHaveBeenCalledTimes(1)
expect(plaid.transactionsGet).toHaveBeenCalledTimes(1) expect(teller.getTransactions).toHaveBeenCalledTimes(1)
const item = await prisma.accountConnection.findUniqueOrThrow({ const item = await prisma.accountConnection.findUniqueOrThrow({
where: { id: connection.id }, where: { id: connection.id },
@ -146,7 +117,7 @@ describe('Message queue tests', () => {
accounts: { accounts: {
include: { include: {
balances: { balances: {
where: PlaidTestData.testDates.prismaWhereFilter, where: TellerGenerator.testDates.prismaWhereFilter,
orderBy: { date: 'asc' }, orderBy: { date: 'asc' },
}, },
transactions: true, transactions: true,
@ -162,61 +133,25 @@ describe('Message queue tests', () => {
const [account] = item.accounts const [account] = item.accounts
expect(account.transactions).toHaveLength(PlaidTestData.checkingTransactions.length) const intervalDates = Interval.fromDateTimes(
expect(account.balances.map((b) => b.balance)).toEqual( TellerGenerator.lowerBound,
[ TellerGenerator.now
3630,
5125,
5125,
5125,
5125,
5125,
5125,
5125,
5125,
5125,
5115,
5115,
5115,
5089.45,
5089.45,
PlaidTestData.checkingAccount.balances.current!,
].map((v) => new Prisma.Decimal(v))
) )
.splitBy({ day: 1 })
.map((date: Interval) => date.start.toISODate())
const startingBalance = Number(mockAccounts[0].balance.available)
const balances = TellerGenerator.calculateDailyBalances(
startingBalance,
mockTransactions,
intervalDates
)
expect(account.transactions).toHaveLength(10)
expect(account.balances.map((b) => b.balance)).toEqual(balances)
expect(account.holdings).toHaveLength(0) expect(account.holdings).toHaveLength(0)
expect(account.valuations).toHaveLength(0) expect(account.valuations).toHaveLength(0)
expect(account.investmentTransactions).toHaveLength(0) expect(account.investmentTransactions).toHaveLength(0)
}) })
it('Should sync valid security prices', async () => {
const security = await prisma.security.create({
data: {
name: 'Walmart Inc.',
symbol: 'WMT',
cusip: '93114210310',
pricingLastSyncedAt: new Date(),
},
})
await securityPricingService.sync(security)
const prices = await prisma.securityPricing.findMany({
where: { securityId: security.id },
orderBy: { date: 'asc' },
})
expect(prices).toHaveLength(PlaidTestData.WMT.results.length)
expect(
prices.map((p) => ({
date: DateTime.fromJSDate(p.date, { zone: 'utc' }).toISODate(),
price: p.priceClose.toNumber(),
}))
).toEqual(
PlaidTestData.WMT.results.map((p) => ({
date: DateTime.fromMillis(p.t, { zone: 'utc' }).toISODate(),
price: p.c,
}))
)
})
}) })

View file

@ -8,17 +8,30 @@ const prisma = new PrismaClient()
*/ */
async function main() { async function main() {
const institutions: (Pick<Institution, 'id' | 'name'> & { const institutions: (Pick<Institution, 'id' | 'name'> & {
providers: { provider: Provider; providerId: string; rank?: number }[] providers: { provider: Provider; providerId: string; logoUrl: string; rank?: number }[]
})[] = [ })[] = [
{ {
id: 1, id: 1,
name: 'Capital One', name: 'Capital One',
providers: [{ provider: 'PLAID', providerId: 'ins_9', rank: 1 }], providers: [
{
provider: Provider.TELLER,
providerId: 'capital_one',
logoUrl: 'https://teller.io/images/banks/capital_one.jpg',
rank: 1,
},
],
}, },
{ {
id: 2, id: 2,
name: 'Discover Bank', name: 'Wells Fargo',
providers: [{ provider: 'PLAID', providerId: 'ins_33' }], providers: [
{
provider: Provider.TELLER,
providerId: 'wells_fargo',
logoUrl: 'https://teller.io/images/banks/wells_fargo.jpg',
},
],
}, },
] ]

View file

@ -1,5 +1,7 @@
import { faker } from '@faker-js/faker' import { faker } from '@faker-js/faker'
import type { TellerTypes } from '../../libs/teller-api/src' import type { TellerTypes } from '../../libs/teller-api/src'
import type { Prisma } from '@prisma/client'
import { DateTime } from 'luxon'
function generateSubType( function generateSubType(
type: TellerTypes.AccountTypes type: TellerTypes.AccountTypes
@ -23,6 +25,8 @@ type GenerateAccountsParams = {
enrollmentId: string enrollmentId: string
institutionName: string institutionName: string
institutionId: string institutionId: string
accountType?: TellerTypes.AccountTypes
accountSubType?: TellerTypes.DepositorySubtypes | TellerTypes.CreditSubtype
} }
export function generateAccounts({ export function generateAccounts({
@ -30,12 +34,15 @@ export function generateAccounts({
enrollmentId, enrollmentId,
institutionName, institutionName,
institutionId, institutionId,
accountType,
accountSubType,
}: GenerateAccountsParams) { }: GenerateAccountsParams) {
const accounts: TellerTypes.Account[] = [] const accounts: TellerTypes.Account[] = []
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const accountId = faker.string.uuid() const accountId = faker.string.uuid()
const lastFour = faker.finance.creditCardNumber().slice(-4) const lastFour = faker.finance.creditCardNumber().slice(-4)
const type: TellerTypes.AccountTypes = faker.helpers.arrayElement(['depository', 'credit']) const type: TellerTypes.AccountTypes =
accountType ?? faker.helpers.arrayElement(['depository', 'credit'])
let subType: TellerTypes.DepositorySubtypes | TellerTypes.CreditSubtype let subType: TellerTypes.DepositorySubtypes | TellerTypes.CreditSubtype
subType = generateSubType(type) subType = generateSubType(type)
@ -99,6 +106,8 @@ type GenerateAccountsWithBalancesParams = {
enrollmentId: string enrollmentId: string
institutionName: string institutionName: string
institutionId: string institutionId: string
accountType?: TellerTypes.AccountTypes
accountSubType?: TellerTypes.DepositorySubtypes | TellerTypes.CreditSubtype
} }
export function generateAccountsWithBalances({ export function generateAccountsWithBalances({
@ -106,7 +115,9 @@ export function generateAccountsWithBalances({
enrollmentId, enrollmentId,
institutionName, institutionName,
institutionId, institutionId,
}: GenerateAccountsWithBalancesParams): TellerTypes.AccountWithBalances[] { accountType,
accountSubType,
}: GenerateAccountsWithBalancesParams): TellerTypes.GetAccountsResponse {
const accountsWithBalances: TellerTypes.AccountWithBalances[] = [] const accountsWithBalances: TellerTypes.AccountWithBalances[] = []
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const account = generateAccounts({ const account = generateAccounts({
@ -114,6 +125,8 @@ export function generateAccountsWithBalances({
enrollmentId, enrollmentId,
institutionName, institutionName,
institutionId, institutionId,
accountType,
accountSubType,
})[0] })[0]
const balance = generateBalance(account.id) const balance = generateBalance(account.id)
accountsWithBalances.push({ accountsWithBalances.push({
@ -170,7 +183,10 @@ export function generateTransactions(count: number, accountId: string): TellerTy
running_balance: null, running_balance: null,
description: faker.word.words({ count: { min: 3, max: 10 } }), description: faker.word.words({ count: { min: 3, max: 10 } }),
id: transactionId, id: transactionId,
date: faker.date.recent({ days: 30 }).toISOString().split('T')[0], // recent date in 'YYYY-MM-DD' format date: faker.date
.between({ from: lowerBound.toJSDate(), to: now.toJSDate() })
.toISOString()
.split('T')[0], // recent date in 'YYYY-MM-DD' format
account_id: accountId, account_id: accountId,
links: { links: {
account: `https://api.teller.io/accounts/${accountId}`, account: `https://api.teller.io/accounts/${accountId}`,
@ -246,3 +262,35 @@ export function generateConnection(): GenerateConnectionsResponse {
transactions, transactions,
} }
} }
export const now = DateTime.fromISO('2022-01-03', { zone: 'utc' })
export const lowerBound = DateTime.fromISO('2021-12-01', { zone: 'utc' })
export const testDates = {
now,
lowerBound,
totalDays: now.diff(lowerBound, 'days').days,
prismaWhereFilter: {
date: {
gte: lowerBound.toJSDate(),
lte: now.toJSDate(),
},
} as Prisma.AccountBalanceWhereInput,
}
export function calculateDailyBalances(startingBalance, transactions, dateInterval) {
transactions.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
const balanceChanges = {}
transactions.forEach((transaction) => {
const date = new Date(transaction.date).toISOString().split('T')[0]
balanceChanges[date] = (balanceChanges[date] || 0) + Number(transaction.amount)
})
return dateInterval.map((date) => {
return Object.keys(balanceChanges)
.filter((d) => d <= date)
.reduce((acc, d) => acc + balanceChanges[d], startingBalance)
})
}