mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 15:35:22 +02:00
add test
This commit is contained in:
parent
40f424b8fe
commit
bce2825fd0
8 changed files with 206 additions and 57 deletions
|
@ -70,6 +70,10 @@ const redis = new Redis(env.NX_REDIS_URL, {
|
||||||
retryStrategy: ServerUtil.redisRetryStrategy({ maxAttempts: 5 }),
|
retryStrategy: ServerUtil.redisRetryStrategy({ maxAttempts: 5 }),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export function closeRedis() {
|
||||||
|
redis.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
export const queueService = new QueueService(
|
export const queueService = new QueueService(
|
||||||
logger.child({ service: 'QueueService' }),
|
logger.child({ service: 'QueueService' }),
|
||||||
process.env.NODE_ENV === 'test'
|
process.env.NODE_ENV === 'test'
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
import type { PrismaClient, User } from '@prisma/client'
|
import type { PrismaClient, User } from '@prisma/client'
|
||||||
|
import { faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export async function resetUser(prisma: PrismaClient, authId = 'TODO'): Promise<User> {
|
export async function resetUser(prisma: PrismaClient, authId = '__TEST_USER_ID__'): Promise<User> {
|
||||||
// eslint-disable-next-line
|
try {
|
||||||
const [_, __, ___, user] = await prisma.$transaction([
|
// eslint-disable-next-line
|
||||||
prisma.$executeRaw`DELETE FROM "user" WHERE auth_id=${authId};`,
|
const [_, __, ___, user] = await prisma.$transaction([
|
||||||
|
prisma.$executeRaw`DELETE FROM "user" WHERE auth_id=${authId};`,
|
||||||
|
|
||||||
// Deleting a user does not cascade to securities, so delete all security records
|
// Deleting a user does not cascade to securities, so delete all security records
|
||||||
prisma.$executeRaw`DELETE from security;`,
|
prisma.$executeRaw`DELETE from security;`,
|
||||||
prisma.$executeRaw`DELETE from security_pricing;`,
|
prisma.$executeRaw`DELETE from security_pricing;`,
|
||||||
|
|
||||||
prisma.user.create({
|
prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
authId,
|
authId,
|
||||||
email: 'test@example.com',
|
email: faker.internet.email(),
|
||||||
finicityCustomerId: 'TEST',
|
finicityCustomerId: faker.string.uuid(),
|
||||||
},
|
tellerUserId: faker.string.uuid(),
|
||||||
}),
|
},
|
||||||
])
|
}),
|
||||||
|
])
|
||||||
return user
|
return user
|
||||||
|
} catch (e) {
|
||||||
|
console.error('error in reset user transaction', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
118
apps/workers/src/app/__tests__/teller.integration.spec.ts
Normal file
118
apps/workers/src/app/__tests__/teller.integration.spec.ts
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import type { User } from '@prisma/client'
|
||||||
|
import { TellerGenerator } from '../../../../../tools/generators'
|
||||||
|
import { TellerApi } from '@maybe-finance/teller-api'
|
||||||
|
jest.mock('@maybe-finance/teller-api')
|
||||||
|
import {
|
||||||
|
TellerETL,
|
||||||
|
TellerService,
|
||||||
|
type IAccountConnectionProvider,
|
||||||
|
} from '@maybe-finance/server/features'
|
||||||
|
import { createLogger } from '@maybe-finance/server/shared'
|
||||||
|
import prisma from '../lib/prisma'
|
||||||
|
import { resetUser } from './helpers/user.test-helper'
|
||||||
|
import { transports } from 'winston'
|
||||||
|
import { cryptoService } from '../lib/di'
|
||||||
|
|
||||||
|
const logger = createLogger({ level: 'debug', transports: [new transports.Console()] })
|
||||||
|
const teller = jest.mocked(new TellerApi())
|
||||||
|
const tellerETL = new TellerETL(logger, prisma, teller, cryptoService)
|
||||||
|
const service: IAccountConnectionProvider = new TellerService(
|
||||||
|
logger,
|
||||||
|
prisma,
|
||||||
|
teller,
|
||||||
|
tellerETL,
|
||||||
|
cryptoService,
|
||||||
|
'TELLER_WEBHOOK_URL',
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Teller', () => {
|
||||||
|
let user: User
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
user = await resetUser(prisma)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('syncs connection', async () => {
|
||||||
|
const tellerConnection = TellerGenerator.generateConnection()
|
||||||
|
const tellerAccounts = tellerConnection.accountsWithBalances
|
||||||
|
const tellerTransactions = tellerConnection.transactions
|
||||||
|
|
||||||
|
teller.getAccounts.mockResolvedValue(tellerAccounts)
|
||||||
|
|
||||||
|
teller.getTransactions.mockImplementation(async ({ accountId }) => {
|
||||||
|
return Promise.resolve(tellerTransactions.filter((t) => t.account_id === accountId))
|
||||||
|
})
|
||||||
|
|
||||||
|
const connection = await prisma.accountConnection.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
name: 'TEST_TELLER',
|
||||||
|
type: 'teller',
|
||||||
|
tellerEnrollmentId: tellerConnection.enrollment.enrollment.id,
|
||||||
|
tellerInstitutionId: tellerConnection.enrollment.institutionId,
|
||||||
|
tellerAccessToken: cryptoService.encrypt(tellerConnection.enrollment.accessToken),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await service.sync(connection)
|
||||||
|
|
||||||
|
const { accounts } = await prisma.accountConnection.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id: connection.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
accounts: {
|
||||||
|
include: {
|
||||||
|
transactions: true,
|
||||||
|
investmentTransactions: true,
|
||||||
|
holdings: true,
|
||||||
|
valuations: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// all accounts
|
||||||
|
expect(accounts).toHaveLength(tellerConnection.accounts.length)
|
||||||
|
for (const account of accounts) {
|
||||||
|
expect(account.transactions).toHaveLength(
|
||||||
|
tellerTransactions.filter((t) => t.account_id === account.tellerAccountId).length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// credit accounts
|
||||||
|
const creditAccounts = tellerAccounts.filter((a) => a.type === 'credit')
|
||||||
|
expect(accounts.filter((a) => a.type === 'CREDIT')).toHaveLength(creditAccounts.length)
|
||||||
|
for (const creditAccount of creditAccounts) {
|
||||||
|
const account = accounts.find((a) => a.tellerAccountId === creditAccount.id)!
|
||||||
|
expect(account.transactions).toHaveLength(
|
||||||
|
tellerTransactions.filter((t) => t.account_id === account.tellerAccountId).length
|
||||||
|
)
|
||||||
|
expect(account.holdings).toHaveLength(0)
|
||||||
|
expect(account.valuations).toHaveLength(0)
|
||||||
|
expect(account.investmentTransactions).toHaveLength(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// depository accounts
|
||||||
|
const depositoryAccounts = tellerAccounts.filter((a) => a.type === 'depository')
|
||||||
|
expect(accounts.filter((a) => a.type === 'DEPOSITORY')).toHaveLength(
|
||||||
|
depositoryAccounts.length
|
||||||
|
)
|
||||||
|
for (const depositoryAccount of depositoryAccounts) {
|
||||||
|
const account = accounts.find((a) => a.tellerAccountId === depositoryAccount.id)!
|
||||||
|
expect(account.transactions).toHaveLength(
|
||||||
|
tellerTransactions.filter((t) => t.account_id === account.tellerAccountId).length
|
||||||
|
)
|
||||||
|
expect(account.holdings).toHaveLength(0)
|
||||||
|
expect(account.valuations).toHaveLength(0)
|
||||||
|
expect(account.investmentTransactions).toHaveLength(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -101,16 +101,7 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
||||||
|
|
||||||
private async _extractAccounts(accessToken: string) {
|
private async _extractAccounts(accessToken: string) {
|
||||||
const accounts = await this.teller.getAccounts({ accessToken })
|
const accounts = await this.teller.getAccounts({ accessToken })
|
||||||
const accountsWithBalances = await Promise.all(
|
return accounts
|
||||||
accounts.map(async (a) => {
|
|
||||||
const balance = await this.teller.getAccountBalances({
|
|
||||||
accountId: a.id,
|
|
||||||
accessToken,
|
|
||||||
})
|
|
||||||
return { ...a, balance }
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return accountsWithBalances
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _loadAccounts(connection: Connection, { accounts }: Pick<TellerData, 'accounts'>) {
|
private _loadAccounts(connection: Connection, { accounts }: Pick<TellerData, 'accounts'>) {
|
||||||
|
@ -212,7 +203,7 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
||||||
${Prisma.join(
|
${Prisma.join(
|
||||||
chunk.map((tellerTransaction) => {
|
chunk.map((tellerTransaction) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id: transactionId,
|
||||||
account_id,
|
account_id,
|
||||||
description,
|
description,
|
||||||
amount,
|
amount,
|
||||||
|
@ -226,7 +217,7 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
||||||
(SELECT id FROM account WHERE account_connection_id = ${
|
(SELECT id FROM account WHERE account_connection_id = ${
|
||||||
connection.id
|
connection.id
|
||||||
} AND teller_account_id = ${account_id.toString()}),
|
} AND teller_account_id = ${account_id.toString()}),
|
||||||
${id},
|
${transactionId},
|
||||||
${date}::date,
|
${date}::date,
|
||||||
${description},
|
${description},
|
||||||
${DbUtil.toDecimal(-amount)},
|
${DbUtil.toDecimal(-amount)},
|
||||||
|
|
|
@ -34,7 +34,20 @@ export class TellerApi {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async getAccounts({ accessToken }: AuthenticatedRequest): Promise<GetAccountsResponse> {
|
async getAccounts({ accessToken }: AuthenticatedRequest): Promise<GetAccountsResponse> {
|
||||||
return this.get<GetAccountsResponse>(`/accounts`, accessToken)
|
const accounts = await this.get<GetAccountsResponse>(`/accounts`, accessToken)
|
||||||
|
const accountsWithBalances = await Promise.all(
|
||||||
|
accounts.map(async (account) => {
|
||||||
|
const balance = await this.getAccountBalances({
|
||||||
|
accountId: account.id,
|
||||||
|
accessToken,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...account,
|
||||||
|
balance,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return accountsWithBalances
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -55,7 +55,7 @@ export type AccountWithBalances = Account & {
|
||||||
balance: AccountBalance
|
balance: AccountBalance
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetAccountsResponse = Account[]
|
export type GetAccountsResponse = AccountWithBalances[]
|
||||||
export type GetAccountResponse = Account
|
export type GetAccountResponse = Account
|
||||||
export type DeleteAccountResponse = void
|
export type DeleteAccountResponse = void
|
||||||
|
|
||||||
|
|
1
tools/generators/index.ts
Normal file
1
tools/generators/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * as TellerGenerator from './tellerGenerator'
|
|
@ -1,5 +1,5 @@
|
||||||
import { faker } from '@faker-js/faker'
|
import { faker } from '@faker-js/faker'
|
||||||
import { TellerTypes } from '../../libs/teller-api/src'
|
import type { TellerTypes } from '../../libs/teller-api/src'
|
||||||
|
|
||||||
function generateSubType(
|
function generateSubType(
|
||||||
type: TellerTypes.AccountTypes
|
type: TellerTypes.AccountTypes
|
||||||
|
@ -204,29 +204,45 @@ export function generateEnrollment(): TellerTypes.Enrollment & { institutionId:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateConnections(count: number) {
|
type GenerateConnectionsResponse = {
|
||||||
const enrollments: (TellerTypes.Enrollment & { institutionId: string })[] = []
|
enrollment: TellerTypes.Enrollment & { institutionId: string }
|
||||||
const accountsWithBalances: TellerTypes.AccountWithBalances[] = []
|
accounts: TellerTypes.Account[]
|
||||||
const transactions: TellerTypes.Transaction[] = []
|
accountsWithBalances: TellerTypes.AccountWithBalances[]
|
||||||
for (let i = 0; i < count; i++) {
|
transactions: TellerTypes.Transaction[]
|
||||||
enrollments.push(generateEnrollment())
|
}
|
||||||
}
|
|
||||||
enrollments.forEach((enrollment) => {
|
export function generateConnection(): GenerateConnectionsResponse {
|
||||||
const accountCount: number = faker.number.int({ min: 1, max: 5 })
|
const accountsWithBalances: TellerTypes.AccountWithBalances[] = []
|
||||||
const transactionsCount: number = faker.number.int({ min: 1, max: 50 })
|
const accounts: TellerTypes.Account[] = []
|
||||||
const enrollmentId = enrollment.enrollment.id
|
const transactions: TellerTypes.Transaction[] = []
|
||||||
const institutionName = enrollment.enrollment.institution.name
|
|
||||||
const institutionId = enrollment.institutionId
|
const enrollment = generateEnrollment()
|
||||||
accountsWithBalances.push(
|
|
||||||
...generateAccountsWithBalances({
|
const accountCount: number = faker.number.int({ min: 1, max: 3 })
|
||||||
count: accountCount,
|
|
||||||
enrollmentId,
|
const enrollmentId = enrollment.enrollment.id
|
||||||
institutionName,
|
const institutionName = enrollment.enrollment.institution.name
|
||||||
institutionId,
|
const institutionId = enrollment.institutionId
|
||||||
})
|
accountsWithBalances.push(
|
||||||
)
|
...generateAccountsWithBalances({
|
||||||
accountsWithBalances.forEach((account) => {
|
count: accountCount,
|
||||||
transactions.push(...generateTransactions(transactionsCount, account.id))
|
enrollmentId,
|
||||||
})
|
institutionName,
|
||||||
})
|
institutionId,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
for (const account of accountsWithBalances) {
|
||||||
|
const { balance, ...accountWithoutBalance } = account
|
||||||
|
accounts.push(accountWithoutBalance)
|
||||||
|
const transactionsCount: number = faker.number.int({ min: 1, max: 5 })
|
||||||
|
const generatedTransactions = generateTransactions(transactionsCount, account.id)
|
||||||
|
transactions.push(...generatedTransactions)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enrollment,
|
||||||
|
accounts,
|
||||||
|
accountsWithBalances,
|
||||||
|
transactions,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue