183 lines
5.6 KiB
TypeScript
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');
|
|
});
|
|
});
|