Files
backend/src/modules/investments/investment-resource.mapper.spec.ts
T

183 lines
5.6 KiB
TypeScript

jest.mock('../../prisma/prisma.service', () => ({
PrismaService: class {},
}));
import { investmentResourceMeta } from './investment-resource.mapper';
import { hourlyAttempt } from './tbank-sync/tbank-sync.repository';
describe('investment resource source freshness', () => {
afterEach(() => jest.useRealTimers());
it('uses each required source TTL and oldest successful timestamp', () => {
jest.useFakeTimers().setSystemTime(new Date('2026-09-25T12:00:00Z'));
const membership = investmentResourceMeta({
resourceKey: 'membership',
dataRevision: '1',
availability: 'complete',
fetchedAt: new Date('2026-09-25T11:58:00Z'),
freshForMs: 300_000,
work: null,
});
const input = {
resourceKey: 'portfolio',
dataRevision: '1',
availability: 'complete' as const,
fetchedAt: new Date('2026-09-25T11:59:30Z'),
freshForMs: 60_000,
work: null,
dependencies: [membership],
};
const fresh = investmentResourceMeta(input);
expect(fresh.freshness).toBe('fresh');
expect(fresh.lastSuccessfulAt).toBe('2026-09-25T11:58:00.000Z');
jest.advanceTimersByTime(30_000);
const stale = investmentResourceMeta(input);
expect(stale.freshness).toBe('stale');
expect(stale.lastSuccessfulAt).toBe(fresh.lastSuccessfulAt);
expect(
investmentResourceMeta({
...input,
dependencies: [{ ...membership, freshness: 'unknown' }],
}).freshness,
).toBe('unknown');
});
it('keeps confirmed data state independent from a failed hourly attempt', () => {
jest.useFakeTimers().setSystemTime(new Date('2026-09-25T11:30:00Z'));
const meta = investmentResourceMeta({
resourceKey: 'portfolio/a',
dataRevision: '1',
availability: 'complete',
fetchedAt: new Date('2026-09-25T11:00:00Z'),
freshForMs: 3_600_000,
work: null,
lastAttempt: {
startedAt: '2026-09-25T12:00:00.000Z',
finishedAt: '2026-09-25T12:01:00.000Z',
state: 'partial',
problems: [
{
section: 'portfolio',
accountId: 'a',
errorCode: 'TBANK_UNAVAILABLE',
},
],
},
});
expect(meta).toMatchObject({
availability: 'available',
completeness: 'complete',
freshness: 'fresh',
lastSuccessfulAt: '2026-09-25T11:00:00.000Z',
lastAttempt: {
state: 'partial',
startedAt: '2026-09-25T12:00:00.000Z',
finishedAt: '2026-09-25T12:01:00.000Z',
},
});
});
it('projects only the matching hourly section into a resource attempt', () => {
const attempt = hourlyAttempt(
{
startedAt: new Date('2026-09-25T12:00:00Z'),
finishedAt: new Date('2026-09-25T12:01:00Z'),
state: 'partial',
sections: [
{ section: 'portfolio', accountId: 'a', errorCode: 'TBANK_UNAVAILABLE' },
{ section: 'operations', accountId: 'a', errorCode: null },
{ section: 'events', errorCode: 'CALENDAR_UNAVAILABLE' },
],
},
'portfolio',
'a',
);
const meta = investmentResourceMeta({
resourceKey: 'portfolio/a',
dataRevision: '1',
availability: 'complete',
fetchedAt: new Date('2026-09-25T11:00:00Z'),
freshForMs: 3_600_000,
work: { id: 'legacy-work', state: 'failed', errorCode: 'LEGACY_FAILURE' },
lastAttempt: attempt,
});
expect(meta).toMatchObject({
availability: 'available',
completeness: 'complete',
lastAttempt: {
state: 'partial',
startedAt: '2026-09-25T12:00:00.000Z',
finishedAt: '2026-09-25T12:01:00.000Z',
problems: [{ section: 'portfolio', accountId: 'a', errorCode: 'TBANK_UNAVAILABLE' }],
},
});
expect(JSON.stringify(meta)).not.toContain('legacy-work');
});
it('projects no confirmed result as incomplete without inventing a successful attempt', () => {
expect(
investmentResourceMeta({
resourceKey: 'accounts',
dataRevision: '0',
availability: 'missing',
fetchedAt: null,
freshForMs: 3_600_000,
work: { id: 'work', state: 'running', errorCode: null },
}),
).toMatchObject({
availability: 'initial-loading',
completeness: 'incomplete',
freshness: 'unknown',
lastSuccessfulAt: null,
lastAttempt: {
state: 'running',
startedAt: null,
finishedAt: null,
problems: [],
},
});
});
});
describe('dependent resource lifecycle', () => {
const input = {
resourceKey: 'source',
dataRevision: '1',
availability: 'complete' as const,
fetchedAt: new Date(),
freshForMs: 60_000,
work: null,
};
it('exposes dependency work without losing saved availability or timestamps', () => {
const failed = investmentResourceMeta({
...input,
work: { id: 'failed', state: 'failed', errorCode: 'TBANK_UNAVAILABLE' },
});
const running = investmentResourceMeta({
...input,
work: { id: 'running', state: 'running', errorCode: null },
});
const parent = investmentResourceMeta({
...input,
dependencies: [failed, running],
});
expect(parent.lastAttempt.state).toBe('none');
expect(parent.availability).toBe('available');
const terminal = investmentResourceMeta({
...input,
dependencies: [failed],
});
expect(terminal.sync).toEqual(failed.sync);
expect(terminal.lastSuccessfulAt).toBe(parent.lastSuccessfulAt);
expect(
investmentResourceMeta({
...input,
dependencies: [{ ...failed, unavailableReason: 'calendar_boundary' }],
}).sync.state,
).toBe('idle');
});
});