Cover the remaining backend service branches

Pushes backend branch coverage from 80 to 87 by exercising the
previously-unhit paths: transactions service notes encryption on
create/update, source==destination validation on create and update,
not-found and TRANSFER-conversion branches on update, the date-range
sub-branches on findAll, and findOne; the advisor's flat / spending-up
trend variants, the no-prior-period fallback, the zero-income savings
rate, the empty-categories placeholder, and the no-env-var ollama
defaults; aggregations.getSummary end-to-end; the date-input fallback
and default 365-day window in valuations.list; the encryption
interceptor's paginated-response and primitive-passthrough paths plus
its no-body and no-matching-fields request paths; the files service's
existing-userdir and no-extname paths plus the UPLOAD_DIR fallback;
and the activity-log service's open-ended date-range filters.

The residual ~3 percent gap to 90 is almost entirely
ts-jest decorator-metadata branches on controllers and DTOs, which
aren't real code paths and can't be tested away without swapping
coverage providers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 16:02:23 -07:00
parent 6cd785bfcf
commit 0bd90d1fa0
7 changed files with 458 additions and 0 deletions
@@ -181,5 +181,27 @@ describe('ActivityLogService', () => {
expect.objectContaining({ skip: 20, take: 10 }),
);
});
it('applies an open-ended range when only startDate is provided', async () => {
mockPrisma.activityLog.findMany.mockResolvedValue([]);
mockPrisma.activityLog.count.mockResolvedValue(0);
await service.findAll(userId, { startDate: '2026-04-01' });
const call = mockPrisma.activityLog.findMany.mock.calls[0][0];
expect(call.where.createdAt.gte).toEqual(new Date('2026-04-01'));
expect(call.where.createdAt.lte).toBeUndefined();
});
it('applies an open-ended range when only endDate is provided', async () => {
mockPrisma.activityLog.findMany.mockResolvedValue([]);
mockPrisma.activityLog.count.mockResolvedValue(0);
await service.findAll(userId, { endDate: '2026-04-30' });
const call = mockPrisma.activityLog.findMany.mock.calls[0][0];
expect(call.where.createdAt.lte).toEqual(new Date('2026-04-30'));
expect(call.where.createdAt.gte).toBeUndefined();
});
});
});
@@ -418,6 +418,125 @@ describe('AdvisorService', () => {
});
});
describe('trend line variants', () => {
const advisePromptSection = (call: any, label: string) => {
const body = JSON.parse(call[1].body);
const sys = body.messages.find((m: any) => m.role === 'system');
const idx = sys.content.indexOf('Trend:');
return sys.content.slice(idx, idx + 200);
};
it('reports rising spending when this period spent more than the previous period', async () => {
mockAggregations.getSummary
.mockResolvedValueOnce({
netWorth: 0,
totalDebt: 0,
income: 5000,
expense: 4000,
})
.mockResolvedValueOnce({
netWorth: 0,
totalDebt: 0,
income: 5000,
expense: 3000,
});
mockOllamaOnce('opening');
await service.getAdvice(userId);
const trend = advisePromptSection(mockFetch.mock.calls[0], 'Trend');
expect(trend).toMatch(/spending is up/i);
});
it('reports flat spending when current and previous expenses match', async () => {
mockAggregations.getSummary.mockResolvedValue({
netWorth: 0,
totalDebt: 0,
income: 5000,
expense: 3000,
});
mockOllamaOnce('opening');
await service.getAdvice(userId);
const trend = advisePromptSection(mockFetch.mock.calls[0], 'Trend');
expect(trend).toMatch(/spending is flat/i);
});
it('says there is no prior-period data when previous expense is zero', async () => {
mockAggregations.getSummary
.mockResolvedValueOnce({
netWorth: 0,
totalDebt: 0,
income: 5000,
expense: 100,
})
.mockResolvedValueOnce({
netWorth: 0,
totalDebt: 0,
income: 0,
expense: 0,
});
mockOllamaOnce('opening');
await service.getAdvice(userId);
const trend = advisePromptSection(mockFetch.mock.calls[0], 'Trend');
expect(trend).toMatch(/no prior-period data/i);
});
it('renders 0.0% savings rate when there is no income', async () => {
mockAggregations.getSummary.mockResolvedValue({
netWorth: 0,
totalDebt: 0,
income: 0,
expense: 50,
});
mockOllamaOnce('opening');
await service.getAdvice(userId);
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
const sys = body.messages.find((m: any) => m.role === 'system');
expect(sys.content).toMatch(/savings rate: 0\.0%/i);
});
it('renders "(none yet)" placeholder when there are no top categories', async () => {
mockAggregations.getSpendingByCategory.mockResolvedValue([]);
mockOllamaOnce('opening');
await service.getAdvice(userId);
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
const sys = body.messages.find((m: any) => m.role === 'system');
expect(sys.content).toMatch(/\(none yet\)/);
});
});
describe('configuration fallbacks', () => {
it('falls back to localhost ollama and llama3 when no env vars are set', async () => {
const fallbackConfig = { get: jest.fn(() => undefined) };
const module: TestingModule = await Test.createTestingModule({
providers: [
AdvisorService,
{ provide: AggregationsService, useValue: mockAggregations },
{ provide: ConfigService, useValue: fallbackConfig },
],
}).compile();
const fallbackService = module.get<AdvisorService>(AdvisorService);
mockOllamaOnce('opening');
await fallbackService.chat(userId, []);
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:11434/api/chat',
expect.objectContaining({ method: 'POST' }),
);
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
expect(body.model).toBe('llama3');
});
});
describe('system prompt anti-math hardening', () => {
it('puts the no-math rule near the top of the prompt', async () => {
mockOllamaOnce('opening analysis');
@@ -336,6 +336,35 @@ describe('AggregationsService', () => {
});
});
describe('getSummary', () => {
it('returns the merged net worth, total debt, and income/expense totals', async () => {
mockPrisma.account.aggregate
// getNetWorth — assets
.mockResolvedValueOnce({ _sum: { balance: 12000 } })
// getNetWorth — liabilities
.mockResolvedValueOnce({ _sum: { balance: 500 } })
// getTotalDebt
.mockResolvedValueOnce({ _sum: { balance: 500 } });
mockPrisma.transaction.groupBy.mockResolvedValue([
{ type: 'INCOME', _sum: { amount: 5000 } },
{ type: 'EXPENSE', _sum: { amount: 3200 } },
]);
const result = await service.getSummary(
userId,
'2026-04-01',
'2026-04-30',
);
expect(result).toEqual({
netWorth: 11500,
totalDebt: 500,
income: 5000,
expense: 3200,
});
});
});
describe('getAccountBalanceHistory', () => {
it('throws NotFound if the account is not owned by the user', async () => {
mockPrisma.account.findFirst.mockResolvedValue(null);
@@ -91,4 +91,55 @@ describe('EncryptionInterceptor', () => {
done();
});
});
it('decrypts the data array inside a paginated response envelope', (done) => {
const context = createMockContext({});
const handler = createMockHandler({
data: [
{ id: '1', accountNumber: 'encrypted_AAAA' },
{ id: '2', accountNumber: 'encrypted_BBBB' },
],
total: 2,
page: 1,
limit: 20,
});
interceptor.intercept(context, handler).subscribe((result) => {
expect(result.total).toBe(2);
expect(result.page).toBe(1);
expect(result.data[0].accountNumber).toBe('AAAA');
expect(result.data[1].accountNumber).toBe('BBBB');
done();
});
});
it('passes through primitive responses untouched', (done) => {
const context = createMockContext({});
const handler = createMockHandler('plain string');
interceptor.intercept(context, handler).subscribe((result) => {
expect(result).toBe('plain string');
done();
});
});
it('skips request encryption when there is no body', (done) => {
const context = createMockContext(undefined);
const handler = createMockHandler({ id: '1' });
interceptor.intercept(context, handler).subscribe(() => {
expect(mockEncryptionService.encryptField).not.toHaveBeenCalled();
done();
});
});
it('only encrypts fields that are actually present on the request body', (done) => {
const context = createMockContext({ name: 'no secrets here' });
const handler = createMockHandler({ id: '1' });
interceptor.intercept(context, handler).subscribe(() => {
expect(mockEncryptionService.encryptField).not.toHaveBeenCalled();
done();
});
});
});
@@ -92,6 +92,55 @@ describe('FilesService', () => {
const result = service.saveFile('user-123', file);
expect(result).toMatch(/\.pdf$/);
});
it('skips mkdirSync when the user upload dir already exists', () => {
(fs.existsSync as jest.Mock).mockReturnValue(true);
const file = {
originalname: 'receipt.jpg',
mimetype: 'image/jpeg',
size: 1024,
buffer: Buffer.from('x'),
} as Express.Multer.File;
service.saveFile('user-123', file);
expect(fs.mkdirSync).not.toHaveBeenCalled();
expect(fs.writeFileSync).toHaveBeenCalled();
});
it('falls back to the .bin extension when the file has no extname', () => {
(fs.existsSync as jest.Mock).mockReturnValue(false);
const file = {
originalname: 'noext',
mimetype: 'image/jpeg',
size: 1024,
buffer: Buffer.from('x'),
} as Express.Multer.File;
const result = service.saveFile('user-123', file);
expect(result).toMatch(/\.bin$/);
});
});
describe('uploadDir fallback', () => {
it('defaults to ./uploads when UPLOAD_DIR is not configured', async () => {
const fallbackModule: TestingModule = await Test.createTestingModule({
providers: [
FilesService,
{
provide: ConfigService,
useValue: { get: jest.fn().mockReturnValue(undefined) },
},
],
}).compile();
const fallbackService = fallbackModule.get<FilesService>(FilesService);
const fullPath = fallbackService.getFilePath('receipts/u/x.jpg');
// path.join collapses './uploads' to 'uploads' so just look for the segment.
expect(fullPath).toContain('uploads');
});
});
describe('deleteFile', () => {
@@ -244,6 +244,46 @@ describe('TransactionsService', () => {
}),
).rejects.toThrow(NotFoundException);
});
it('encrypts notes on create when provided', async () => {
mockPrisma.account.findMany.mockResolvedValue([
{ id: 'acc-1', type: AccountType.CHECKING },
]);
txClient.transaction.create.mockResolvedValue({
...baseTxn,
notes: 'enc:secret',
});
await service.create(userId, {
accountId: 'acc-1',
amount: 10,
type: TransactionType.EXPENSE,
description: 'Coffee',
date: '2026-04-01',
notes: 'private memo',
});
expect(mockEncryption.encryptField).toHaveBeenCalledWith('private memo');
const created = txClient.transaction.create.mock.calls[0][0];
// The data passed to Prisma should carry the encrypted (mocked passthrough) value.
expect(created.data.notes).toBe('private memo');
});
it('rejects a TRANSFER whose source and destination are the same account', async () => {
mockPrisma.account.findMany.mockResolvedValue([
{ id: 'acc-1', type: AccountType.CHECKING },
]);
await expect(
service.create(userId, {
accountId: 'acc-1',
destinationAccountId: 'acc-1',
amount: 100,
type: TransactionType.TRANSFER,
description: 'self transfer',
date: '2026-04-01',
}),
).rejects.toThrow(/must differ/i);
});
});
describe('findAll', () => {
@@ -254,6 +294,68 @@ describe('TransactionsService', () => {
const where = mockPrisma.transaction.findMany.mock.calls[0][0].where;
expect(where.OR).toEqual([{ accountId: 'acc-1' }, { destinationAccountId: 'acc-1' }]);
});
it('applies a date range when both startDate and endDate are provided', async () => {
mockPrisma.transaction.findMany.mockResolvedValue([]);
mockPrisma.transaction.count.mockResolvedValue(0);
await service.findAll(userId, {
startDate: '2026-04-01',
endDate: '2026-04-30',
});
const where = mockPrisma.transaction.findMany.mock.calls[0][0].where;
expect(where.date.gte).toBeInstanceOf(Date);
expect(where.date.lte).toBeInstanceOf(Date);
});
it('applies an open-ended date range when only startDate is provided', async () => {
mockPrisma.transaction.findMany.mockResolvedValue([]);
mockPrisma.transaction.count.mockResolvedValue(0);
await service.findAll(userId, { startDate: '2026-04-01' });
const where = mockPrisma.transaction.findMany.mock.calls[0][0].where;
expect(where.date.gte).toBeInstanceOf(Date);
expect(where.date.lte).toBeUndefined();
});
it('applies an open-ended date range when only endDate is provided', async () => {
mockPrisma.transaction.findMany.mockResolvedValue([]);
mockPrisma.transaction.count.mockResolvedValue(0);
await service.findAll(userId, { endDate: '2026-04-30' });
const where = mockPrisma.transaction.findMany.mock.calls[0][0].where;
expect(where.date.lte).toBeInstanceOf(Date);
expect(where.date.gte).toBeUndefined();
});
it('filters by categoryId and type when supplied', async () => {
mockPrisma.transaction.findMany.mockResolvedValue([]);
mockPrisma.transaction.count.mockResolvedValue(0);
await service.findAll(userId, {
categoryId: 'cat-1',
type: TransactionType.EXPENSE,
});
const where = mockPrisma.transaction.findMany.mock.calls[0][0].where;
expect(where.categoryId).toBe('cat-1');
expect(where.type).toBe('EXPENSE');
});
});
describe('findOne', () => {
it('returns the decrypted transaction when found', async () => {
mockPrisma.transaction.findFirst.mockResolvedValue({
...baseTxn,
notes: 'enc:secret',
});
const result = await service.findOne(userId, 'txn-1');
expect(result.id).toBe('txn-1');
// Mock passthrough means we still see 'enc:secret', but decryptField must have been called.
expect(mockEncryption.decryptField).toHaveBeenCalledWith('enc:secret');
});
it('throws NotFoundException when the transaction does not exist', async () => {
mockPrisma.transaction.findFirst.mockResolvedValue(null);
await expect(service.findOne(userId, 'missing')).rejects.toThrow(
NotFoundException,
);
});
});
describe('update', () => {
@@ -329,6 +431,58 @@ describe('TransactionsService', () => {
expect(txClient.account.update).not.toHaveBeenCalled();
});
it('throws NotFoundException when updating a transaction that does not exist', async () => {
mockPrisma.transaction.findFirst.mockResolvedValue(null);
await expect(
service.update(userId, 'missing', { description: 'x' }),
).rejects.toThrow(NotFoundException);
});
it('encrypts notes on update when the field is supplied', async () => {
const existing = { ...baseTxn, type: TransactionType.EXPENSE, amount: 100 };
mockPrisma.transaction.findFirst.mockResolvedValue(existing);
txClient.transaction.update.mockResolvedValue({ ...existing, notes: 'new memo' });
await service.update(userId, 'txn-1', { notes: 'new memo' } as any);
expect(mockEncryption.encryptField).toHaveBeenCalledWith('new memo');
});
it('throws BadRequestException when an update would convert to TRANSFER without a destination', async () => {
const existing = { ...baseTxn, type: TransactionType.EXPENSE, amount: 100 };
mockPrisma.transaction.findFirst.mockResolvedValue(existing);
mockPrisma.account.findMany.mockResolvedValue([
{ id: 'acc-1', type: AccountType.CHECKING },
]);
await expect(
service.update(userId, 'txn-1', {
type: TransactionType.TRANSFER,
} as any),
).rejects.toThrow(BadRequestException);
});
it('throws BadRequestException when an updated TRANSFER points back at its source', async () => {
const existing = {
...baseTxn,
accountId: 'acc-1',
destinationAccountId: 'acc-2',
amount: 50,
type: TransactionType.TRANSFER,
};
mockPrisma.transaction.findFirst.mockResolvedValue(existing);
mockPrisma.account.findMany.mockResolvedValue([
{ id: 'acc-1', type: AccountType.CHECKING },
{ id: 'acc-2', type: AccountType.SAVINGS },
]);
await expect(
service.update(userId, 'txn-1', {
destinationAccountId: 'acc-1',
} as any),
).rejects.toThrow(/must differ/i);
});
});
describe('remove', () => {
@@ -118,6 +118,23 @@ describe('ValuationsService', () => {
const stored: Date = call.data.date;
expect(stored.toISOString()).toBe('2026-04-17T12:00:00.000Z');
});
it('falls back to native Date parsing for an unparseable date string', async () => {
// The "YYYY-MM-DD" fast path can't extract components → falls through to
// `new Date(dateStr)`. The exact Date is implementation-defined, but the
// call must still succeed.
mockPrisma.account.findFirst.mockResolvedValue({ id: accountId });
mockPrisma.accountValuation.create.mockResolvedValue({});
mockPrisma.accountValuation.findFirst.mockResolvedValue(null);
await service.create(userId, accountId, {
date: 'not-a-date' as any,
value: 100,
});
const call = mockPrisma.accountValuation.create.mock.calls[0][0];
expect(call.data.date).toBeInstanceOf(Date);
});
});
describe('list', () => {
@@ -140,6 +157,23 @@ describe('ValuationsService', () => {
expect(result).toEqual(rows);
});
it('uses the default 365-day window when days is omitted', async () => {
mockPrisma.account.findFirst.mockResolvedValue({ id: accountId });
mockPrisma.accountValuation.findMany.mockResolvedValue([]);
await service.list(userId, accountId);
const call = mockPrisma.accountValuation.findMany.mock.calls[0][0];
const cutoff: Date = call.where.date.gte;
const daysAgo =
(Date.now() - cutoff.getTime()) / (24 * 60 * 60 * 1000);
// Allow a small tolerance for clock drift between the default
// computation and the assertion. Strict equality would race with the
// system clock.
expect(daysAgo).toBeGreaterThan(364);
expect(daysAgo).toBeLessThan(366);
});
it('rejects an account not owned by the user', async () => {
mockPrisma.account.findFirst.mockResolvedValue(null);
await expect(service.list(userId, 'stranger', 30)).rejects.toThrow(NotFoundException);