mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-08 15:05: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 }),
|
||||
})
|
||||
|
||||
export function closeRedis() {
|
||||
redis.disconnect()
|
||||
}
|
||||
|
||||
export const queueService = new QueueService(
|
||||
logger.child({ service: 'QueueService' }),
|
||||
process.env.NODE_ENV === 'test'
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
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> {
|
||||
try {
|
||||
// eslint-disable-next-line
|
||||
const [_, __, ___, user] = await prisma.$transaction([
|
||||
prisma.$executeRaw`DELETE FROM "user" WHERE auth_id=${authId};`,
|
||||
|
@ -12,11 +14,15 @@ export async function resetUser(prisma: PrismaClient, authId = 'TODO'): Promise<
|
|||
prisma.user.create({
|
||||
data: {
|
||||
authId,
|
||||
email: 'test@example.com',
|
||||
finicityCustomerId: 'TEST',
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
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) {
|
||||
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<TellerData, 'accounts'>) {
|
||||
|
@ -212,7 +203,7 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
|||
${Prisma.join(
|
||||
chunk.map((tellerTransaction) => {
|
||||
const {
|
||||
id,
|
||||
id: transactionId,
|
||||
account_id,
|
||||
description,
|
||||
amount,
|
||||
|
@ -226,7 +217,7 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
|||
(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)},
|
||||
|
|
|
@ -34,7 +34,20 @@ export class TellerApi {
|
|||
*/
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export type GetAccountsResponse = Account[]
|
||||
export type GetAccountsResponse = AccountWithBalances[]
|
||||
export type GetAccountResponse = Account
|
||||
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 { TellerTypes } from '../../libs/teller-api/src'
|
||||
import type { TellerTypes } from '../../libs/teller-api/src'
|
||||
|
||||
function generateSubType(
|
||||
type: TellerTypes.AccountTypes
|
||||
|
@ -204,16 +204,22 @@ 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())
|
||||
type GenerateConnectionsResponse = {
|
||||
enrollment: TellerTypes.Enrollment & { institutionId: string }
|
||||
accounts: TellerTypes.Account[]
|
||||
accountsWithBalances: TellerTypes.AccountWithBalances[]
|
||||
transactions: TellerTypes.Transaction[]
|
||||
}
|
||||
enrollments.forEach((enrollment) => {
|
||||
const accountCount: number = faker.number.int({ min: 1, max: 5 })
|
||||
const transactionsCount: number = faker.number.int({ min: 1, max: 50 })
|
||||
|
||||
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
|
||||
|
@ -225,8 +231,18 @@ export function generateConnections(count: number) {
|
|||
institutionId,
|
||||
})
|
||||
)
|
||||
accountsWithBalances.forEach((account) => {
|
||||
transactions.push(...generateTransactions(transactionsCount, account.id))
|
||||
})
|
||||
})
|
||||
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