refactor: retire legacy tbank-sync status and sync=observe (WealthLedger/backend#196)
This commit is contained in:
@@ -30,10 +30,11 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
};
|
||||
const auth = (owner = 'owner') =>
|
||||
`Bearer ${new JwtService().sign({ sub: owner, type: 'access' }, { secret: process.env.JWT_SECRET })}`;
|
||||
const cursorParam = (cursor?: null | string) => (cursor ? `&cursor=${encodeURIComponent(cursor)}` : '');
|
||||
const get = (query = '', account = 'all', owner = 'owner') =>
|
||||
request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v1/brokerage-accounts/${account}/operations?${period}&${query}`,
|
||||
`/api/v1/brokerage-accounts/${account}/operations?${[period, query].filter(Boolean).join('&')}`,
|
||||
)
|
||||
.set('Authorization', auth(owner))
|
||||
.timeout(30_000);
|
||||
@@ -138,7 +139,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
let complete = false;
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/api/v1/brokerage-accounts/a/operations?from=2025-02-01&to=2025-02-28&sync=observe')
|
||||
.get('/api/v1/brokerage-accounts/a/operations?from=2025-02-01&to=2025-02-28')
|
||||
.set('Authorization', auth())
|
||||
.expect(200);
|
||||
const result = response.body as BrokerageOperationsResourceDto;
|
||||
@@ -162,7 +163,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
const ids: string[] = [];
|
||||
do {
|
||||
const response = await get(
|
||||
`limit=2${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`,
|
||||
`limit=2${cursorParam(cursor)}`,
|
||||
).expect(200);
|
||||
const result = response.body as BrokerageOperationsResourceDto;
|
||||
expect(result.meta.availability).toBe('available');
|
||||
@@ -243,7 +244,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
do {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v1/brokerage-accounts/a/operations?from=${interval.from.toISOString()}&to=2025-01-02T00:00:00.900Z&limit=1&sync=observe${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`,
|
||||
`/api/v1/brokerage-accounts/a/operations?from=${interval.from.toISOString()}&to=2025-01-02T00:00:00.900Z&limit=1${cursorParam(cursor)}`,
|
||||
)
|
||||
.set('Authorization', auth())
|
||||
.expect(200);
|
||||
@@ -258,7 +259,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
const income = (
|
||||
await request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v1/brokerage-accounts/a/income-analytics?${period}&sync=observe`,
|
||||
`/api/v1/brokerage-accounts/a/income-analytics?${period}`,
|
||||
)
|
||||
.set('Authorization', auth())
|
||||
.expect(200)
|
||||
@@ -291,10 +292,10 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
periodKind === 'default'
|
||||
? ''
|
||||
: `&to=${observedAt.toISOString().slice(0, 10)}${periodKind === 'today timestamp' ? 'T23:59:59.999Z' : ''}`;
|
||||
const page = (cursor?: string, observe = false) =>
|
||||
const page = (cursor?: string) =>
|
||||
request(app.getHttpServer())
|
||||
.get(
|
||||
`/api/v1/brokerage-accounts/all/operations?${observe ? 'sync=observe&' : ''}limit=1${query}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`,
|
||||
`/api/v1/brokerage-accounts/all/operations?limit=1${query}${cursorParam(cursor)}`,
|
||||
)
|
||||
.set('Authorization', auth());
|
||||
const first = (await page().expect(200))
|
||||
@@ -307,7 +308,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
expect(second.meta.availability).toBe('available');
|
||||
expect(second.meta.completeness).toBe('complete');
|
||||
expect(second.meta.coverage?.missingRanges).toEqual([]);
|
||||
const observed = (await page(first.data.nextCursor, true).expect(200))
|
||||
const observed = (await page(first.data.nextCursor).expect(200))
|
||||
.body as BrokerageOperationsResourceDto;
|
||||
expect(observed.meta.availability).toBe('available');
|
||||
expect(observed.meta.completeness).toBe('complete');
|
||||
@@ -317,7 +318,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
it('keeps data revisions stable while observational reads expose current coverage', async () => {
|
||||
const observe = () =>
|
||||
request(app.getHttpServer())
|
||||
.get('/api/v1/brokerage-accounts/all/operations?sync=observe&limit=100')
|
||||
.get('/api/v1/brokerage-accounts/all/operations?limit=100')
|
||||
.set('Authorization', auth());
|
||||
const first = (await observe().expect(200))
|
||||
.body as BrokerageOperationsResourceDto;
|
||||
@@ -340,7 +341,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
it('reads shared metadata from SQLite without invalidating a row cursor', async () => {
|
||||
const before = (await get('limit=1&sync=observe').expect(200))
|
||||
const before = (await get('limit=1').expect(200))
|
||||
.body as BrokerageOperationsResourceDto;
|
||||
assert(before.data?.nextCursor);
|
||||
const { revision } = await prisma.tbankConnection.findUniqueOrThrow({
|
||||
@@ -371,22 +372,22 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
new Date(),
|
||||
);
|
||||
await prisma.$disconnect();
|
||||
const after = (await get('limit=1&sync=observe').expect(200))
|
||||
const after = (await get('limit=1').expect(200))
|
||||
.body as BrokerageOperationsResourceDto;
|
||||
expect(after.data?.items[0].logoName).toBe('saved.png');
|
||||
expect(after.meta.dataRevision).toBe(before.meta.dataRevision);
|
||||
await get(
|
||||
`limit=1&sync=observe&cursor=${encodeURIComponent(before.data.nextCursor)}`,
|
||||
`limit=1&cursor=${encodeURIComponent(before.data.nextCursor)}`,
|
||||
).expect(200);
|
||||
expect(upstream.getOperationsByCursor).not.toHaveBeenCalled();
|
||||
});
|
||||
it('binds a signed cursor to equivalent filters, user, membership, row revision and connection; status alone does not break it', async () => {
|
||||
const first = (
|
||||
await get('limit=1&categories=trade,income&sync=observe').expect(200)
|
||||
await get('limit=1&categories=trade,income').expect(200)
|
||||
).body as BrokerageOperationsResourceDto;
|
||||
assert(first.data?.nextCursor);
|
||||
const cursor = encodeURIComponent(first.data.nextCursor);
|
||||
const next = `limit=1&categories=income,trade,income&sync=observe&cursor=${cursor}`;
|
||||
const next = `limit=1&categories=income,trade,income&cursor=${cursor}`;
|
||||
await prisma.tbankOperationCoverage.updateMany({
|
||||
data: { fetchedAt: new Date(0) },
|
||||
});
|
||||
@@ -400,10 +401,10 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
data: { operationsRevision: { increment: 1 } },
|
||||
});
|
||||
await get(next).expect(400);
|
||||
const single = (await get('limit=1&sync=observe', 'a').expect(200))
|
||||
const single = (await get('limit=1', 'a').expect(200))
|
||||
.body as BrokerageOperationsResourceDto;
|
||||
assert(single.data?.nextCursor);
|
||||
const singleNext = `limit=1&sync=observe&cursor=${encodeURIComponent(single.data.nextCursor)}`;
|
||||
const singleNext = `limit=1&cursor=${encodeURIComponent(single.data.nextCursor)}`;
|
||||
await prisma.tbankConnection.update({
|
||||
where: { userId: 'owner' },
|
||||
data: { membershipRevision: { increment: 1 } },
|
||||
@@ -431,7 +432,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
});
|
||||
const partial = (
|
||||
await get(
|
||||
`instrumentUid=${uid}&categories=income&limit=100&sync=observe`,
|
||||
`instrumentUid=${uid}&categories=income&limit=100`,
|
||||
'a',
|
||||
).expect(200)
|
||||
).body as BrokerageOperationsResourceDto;
|
||||
@@ -439,7 +440,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
expect(partial.meta.completeness).toBe('incomplete');
|
||||
expect(partial.data).toBeNull();
|
||||
expect(partial.meta.coverage?.missingRanges.length).toBeGreaterThan(0);
|
||||
const whole = (await get('sync=observe', 'a').expect(200))
|
||||
const whole = (await get('', 'a').expect(200))
|
||||
.body as BrokerageOperationsResourceDto;
|
||||
expect(whole.meta.availability).toBe('available');
|
||||
expect(whole.meta.completeness).toBe('complete');
|
||||
@@ -450,7 +451,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
await prisma.tbankAccount.updateMany({ data: { openedOn: null } });
|
||||
await prisma.tbankOperationCoverage.deleteMany({ where: { accountId: 'a' } });
|
||||
await prisma.tbankSyncWork.deleteMany({ where: { userId: 'owner' } });
|
||||
const result = (await get('sync=observe', 'a').expect(200))
|
||||
const result = (await get('', 'a').expect(200))
|
||||
.body as BrokerageOperationsResourceDto;
|
||||
expect(result.meta.coverage?.historyBoundary).toBe('unknown');
|
||||
expect(result.meta.coverage?.historyState).toBe('missing');
|
||||
@@ -474,7 +475,7 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
expect(cold.data).toBeNull();
|
||||
expect(cold.meta.availability).toBe('initial-loading');
|
||||
expect(cold.meta.completeness).toBe('incomplete');
|
||||
await get('sync=observe', 'a').expect(200);
|
||||
await get('', 'a').expect(200);
|
||||
expect(upstream.getOperationsByCursor).toHaveBeenCalledTimes(1);
|
||||
// eslint-disable-next-line sonarjs/no-extra-arguments -- The typed callback is assigned the promise resolver before invocation.
|
||||
unblock({ hasNext: false, items: [] });
|
||||
@@ -485,12 +486,12 @@ describe('persistent ledger HTTP and cursor (SQLite)', () => {
|
||||
await delay(5);
|
||||
}
|
||||
const count = upstream.getOperationsByCursor.mock.calls.length;
|
||||
const result = (await get('sync=observe', 'a').expect(200))
|
||||
const result = (await get('', 'a').expect(200))
|
||||
.body as BrokerageOperationsResourceDto;
|
||||
expect(result.data?.items).toEqual([]);
|
||||
expect(result.meta.availability).toBe('available');
|
||||
expect(result.meta.completeness).toBe('complete');
|
||||
await get('sync=observe', 'a').expect(200);
|
||||
await get('', 'a').expect(200);
|
||||
expect(upstream.getOperationsByCursor.mock.calls.length).toBeGreaterThanOrEqual(count);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user