From bce2825fd0591624d6852833db958cb0505ded9a Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Wed, 17 Jan 2024 19:33:48 -0600 Subject: [PATCH] add test --- apps/server/src/app/lib/endpoint.ts | 4 + .../app/__tests__/helpers/user.test-helper.ts | 40 +++--- .../app/__tests__/teller.integration.spec.ts | 118 ++++++++++++++++++ .../src/providers/teller/teller.etl.ts | 15 +-- libs/teller-api/src/teller-api.ts | 15 ++- libs/teller-api/src/types/accounts.ts | 2 +- tools/generators/index.ts | 1 + tools/generators/tellerGenerator.ts | 68 ++++++---- 8 files changed, 206 insertions(+), 57 deletions(-) create mode 100644 apps/workers/src/app/__tests__/teller.integration.spec.ts create mode 100644 tools/generators/index.ts diff --git a/apps/server/src/app/lib/endpoint.ts b/apps/server/src/app/lib/endpoint.ts index f6340672..9aa946a9 100644 --- a/apps/server/src/app/lib/endpoint.ts +++ b/apps/server/src/app/lib/endpoint.ts @@ -70,6 +70,10 @@ const redis = new Redis(env.NX_REDIS_URL, { retryStrategy: ServerUtil.redisRetryStrategy({ maxAttempts: 5 }), }) +export function closeRedis() { + redis.disconnect() +} + export const queueService = new QueueService( logger.child({ service: 'QueueService' }), process.env.NODE_ENV === 'test' diff --git a/apps/workers/src/app/__tests__/helpers/user.test-helper.ts b/apps/workers/src/app/__tests__/helpers/user.test-helper.ts index 0956a22b..322b7dba 100644 --- a/apps/workers/src/app/__tests__/helpers/user.test-helper.ts +++ b/apps/workers/src/app/__tests__/helpers/user.test-helper.ts @@ -1,22 +1,28 @@ import type { PrismaClient, User } from '@prisma/client' +import { faker } from '@faker-js/faker' -export async function resetUser(prisma: PrismaClient, authId = 'TODO'): Promise { - // eslint-disable-next-line - const [_, __, ___, user] = await prisma.$transaction([ - prisma.$executeRaw`DELETE FROM "user" WHERE auth_id=${authId};`, +export async function resetUser(prisma: PrismaClient, authId = '__TEST_USER_ID__'): Promise { + try { + // eslint-disable-next-line + 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 - prisma.$executeRaw`DELETE from security;`, - prisma.$executeRaw`DELETE from security_pricing;`, + // Deleting a user does not cascade to securities, so delete all security records + prisma.$executeRaw`DELETE from security;`, + prisma.$executeRaw`DELETE from security_pricing;`, - prisma.user.create({ - data: { - authId, - email: 'test@example.com', - finicityCustomerId: 'TEST', - }, - }), - ]) - - return user + prisma.user.create({ + data: { + authId, + email: faker.internet.email(), + finicityCustomerId: faker.string.uuid(), + tellerUserId: faker.string.uuid(), + }, + }), + ]) + return user + } catch (e) { + console.error('error in reset user transaction', e) + throw e + } } diff --git a/apps/workers/src/app/__tests__/teller.integration.spec.ts b/apps/workers/src/app/__tests__/teller.integration.spec.ts new file mode 100644 index 00000000..74c19457 --- /dev/null +++ b/apps/workers/src/app/__tests__/teller.integration.spec.ts @@ -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) + } + }) +}) diff --git a/libs/server/features/src/providers/teller/teller.etl.ts b/libs/server/features/src/providers/teller/teller.etl.ts index 8b6d8762..b81e2a4b 100644 --- a/libs/server/features/src/providers/teller/teller.etl.ts +++ b/libs/server/features/src/providers/teller/teller.etl.ts @@ -101,16 +101,7 @@ export class TellerETL implements IETL { private async _extractAccounts(accessToken: string) { const accounts = await this.teller.getAccounts({ accessToken }) - const accountsWithBalances = await Promise.all( - accounts.map(async (a) => { - const balance = await this.teller.getAccountBalances({ - accountId: a.id, - accessToken, - }) - return { ...a, balance } - }) - ) - return accountsWithBalances + return accounts } private _loadAccounts(connection: Connection, { accounts }: Pick) { @@ -212,7 +203,7 @@ export class TellerETL implements IETL { ${Prisma.join( chunk.map((tellerTransaction) => { const { - id, + id: transactionId, account_id, description, amount, @@ -226,7 +217,7 @@ export class TellerETL implements IETL { (SELECT id FROM account WHERE account_connection_id = ${ connection.id } AND teller_account_id = ${account_id.toString()}), - ${id}, + ${transactionId}, ${date}::date, ${description}, ${DbUtil.toDecimal(-amount)}, diff --git a/libs/teller-api/src/teller-api.ts b/libs/teller-api/src/teller-api.ts index 599af838..59d70483 100644 --- a/libs/teller-api/src/teller-api.ts +++ b/libs/teller-api/src/teller-api.ts @@ -34,7 +34,20 @@ export class TellerApi { */ async getAccounts({ accessToken }: AuthenticatedRequest): Promise { - return this.get(`/accounts`, accessToken) + const accounts = await this.get(`/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 } /** diff --git a/libs/teller-api/src/types/accounts.ts b/libs/teller-api/src/types/accounts.ts index a5628f92..34620987 100644 --- a/libs/teller-api/src/types/accounts.ts +++ b/libs/teller-api/src/types/accounts.ts @@ -55,7 +55,7 @@ export type AccountWithBalances = Account & { balance: AccountBalance } -export type GetAccountsResponse = Account[] +export type GetAccountsResponse = AccountWithBalances[] export type GetAccountResponse = Account export type DeleteAccountResponse = void diff --git a/tools/generators/index.ts b/tools/generators/index.ts new file mode 100644 index 00000000..42ab4166 --- /dev/null +++ b/tools/generators/index.ts @@ -0,0 +1 @@ +export * as TellerGenerator from './tellerGenerator' diff --git a/tools/generators/tellerGenerator.ts b/tools/generators/tellerGenerator.ts index a798c2f8..8ee131b4 100644 --- a/tools/generators/tellerGenerator.ts +++ b/tools/generators/tellerGenerator.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker' -import { TellerTypes } from '../../libs/teller-api/src' +import type { TellerTypes } from '../../libs/teller-api/src' function generateSubType( type: TellerTypes.AccountTypes @@ -204,29 +204,45 @@ export function generateEnrollment(): TellerTypes.Enrollment & { institutionId: } } -export function generateConnections(count: number) { - const enrollments: (TellerTypes.Enrollment & { institutionId: string })[] = [] - const accountsWithBalances: TellerTypes.AccountWithBalances[] = [] - const transactions: TellerTypes.Transaction[] = [] - for (let i = 0; i < count; i++) { - enrollments.push(generateEnrollment()) - } - enrollments.forEach((enrollment) => { - const accountCount: number = faker.number.int({ min: 1, max: 5 }) - const transactionsCount: number = faker.number.int({ min: 1, max: 50 }) - const enrollmentId = enrollment.enrollment.id - const institutionName = enrollment.enrollment.institution.name - const institutionId = enrollment.institutionId - accountsWithBalances.push( - ...generateAccountsWithBalances({ - count: accountCount, - enrollmentId, - institutionName, - institutionId, - }) - ) - accountsWithBalances.forEach((account) => { - transactions.push(...generateTransactions(transactionsCount, account.id)) - }) - }) +type GenerateConnectionsResponse = { + enrollment: TellerTypes.Enrollment & { institutionId: string } + accounts: TellerTypes.Account[] + accountsWithBalances: TellerTypes.AccountWithBalances[] + transactions: TellerTypes.Transaction[] +} + +export function generateConnection(): GenerateConnectionsResponse { + const accountsWithBalances: TellerTypes.AccountWithBalances[] = [] + const accounts: TellerTypes.Account[] = [] + const transactions: TellerTypes.Transaction[] = [] + + const enrollment = generateEnrollment() + + const accountCount: number = faker.number.int({ min: 1, max: 3 }) + + const enrollmentId = enrollment.enrollment.id + const institutionName = enrollment.enrollment.institution.name + const institutionId = enrollment.institutionId + accountsWithBalances.push( + ...generateAccountsWithBalances({ + count: accountCount, + 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, + } }