import { ConfigService } from '@nestjs/config'; import assert from 'node:assert/strict'; import { TbankAccessService } from '../src/integrations/tbank/tbank-access.service'; import { TbankTokenService } from '../src/integrations/tbank/tbank-token.service'; import { CatalogDiscoveryService } from '../src/modules/investments/instruments/application/catalog-discovery.service'; import { CatalogDiscoveryRepository, type CatalogInstrument, type CatalogQuery, } from '../src/modules/investments/instruments/infrastructure/catalog-discovery.repository'; import { PrismaService } from '../src/prisma/prisma.service'; // Real transactions prove query normalization, confirmed-empty identity and cross-user isolation of shared reads. describe('shared catalog discovery (SQLite)', () => { const prisma = new PrismaService( new ConfigService({ DATABASE_URL: process.env.DATABASE_URL }), ); const tokens = new TbankTokenService( new ConfigService({ TOKEN_ENCRYPTION_KEY: process.env.TOKEN_ENCRYPTION_KEY, }) as never, ); const upstream = { findShares: jest.fn(), findBonds: jest.fn() }; const access = new TbankAccessService(prisma, tokens); const repository = new CatalogDiscoveryRepository(prisma); const createService = () => new CatalogDiscoveryService( repository, access, upstream as never, { requestMany: jest.fn().mockResolvedValue([]) } as never, ); let service: CatalogDiscoveryService; const query: CatalogQuery = { instrumentType: 'share', query: 'SBER' }; const instrument = { uid: 'uid', name: 'Synthetic share', ticker: 'SBER', isin: 'TEST', instrumentKind: 'INSTRUMENT_TYPE_SHARE', }; const seed = (userId: string) => prisma.tbankConnection.create({ data: { userId, revision: `${userId}-revision`, encryptedToken: tokens.encrypt(`synthetic-${userId}`), connectedAt: new Date(), }, }); // Writes a finished discovery directly, so the read path is tested without any producer. async function seedCatalog( query: string, instruments: Array<{ payload: CatalogInstrument; uid: string }>, ) { for (const item of instruments) { await prisma.tbankInstrument.create({ data: { provider: 'tbank', uid: item.uid, instrumentType: 'share', metadata: {}, fetchedAt: new Date(), }, }); await prisma.tbankInstrumentSource.create({ data: { provider: 'tbank', instrumentUid: item.uid, source: 'tbank', kind: 'catalog', rangeKey: 'share', payload: item.payload, fetchedAt: new Date(), }, }); } await prisma.tbankCatalogDiscovery.create({ data: { provider: 'tbank', instrumentType: 'share', query, instrumentUids: instruments.map((item) => item.uid), fetchedAt: new Date(), }, }); } beforeAll(async () => { await prisma.$connect(); for (const id of ['owner', 'other']) { await prisma.user.create({ data: { id, email: `${id}@example.invalid`, passwordHash: 'synthetic' }, }); } }); afterAll(async () => prisma.$disconnect()); beforeEach(async () => { await prisma.tbankConnection.deleteMany(); await prisma.tbankCatalogDiscovery.deleteMany(); await prisma.tbankInstrumentSource.deleteMany(); await prisma.tbankInstrument.deleteMany(); await seed('owner'); await seed('other'); jest.resetAllMocks(); service = createService(); }); it('treats an unknown query as unavailable and a seeded query as available for another user', async () => { const cold = await service.read('owner', { ...query, query: ' sber ' }); expect(cold.data).toBeNull(); expect(cold.meta.availability).toBe('temporarily-unavailable'); await seedCatalog('SBER', [{ uid: 'uid', payload: instrument }]); const warm = await service.read('other', query); expect(warm.data).toEqual([instrument]); expect(warm.meta.availability).toBe('available'); expect(upstream.findShares).not.toHaveBeenCalled(); }); it('keeps a confirmed empty result distinct from an unknown query', async () => { await seedCatalog('GAZP', []); const empty = await service.read('owner', { ...query, query: 'GAZP' }); expect(empty.data).toEqual([]); expect(empty.meta.availability).toBe('available'); const unknown = await service.read('owner', { ...query, query: 'LKOH' }); expect(unknown.data).toBeNull(); }); it('keeps the stored selection order and does not expose private fields', async () => { const first = { ...instrument, uid: 'first', name: 'First' }; const second = { ...instrument, uid: 'second', name: 'Second' }; await seedCatalog('SBER', [ { uid: 'second', payload: second }, { uid: 'first', payload: first }, ]); const result = await service.read('owner', query); assert(result.data); expect(result.data.map((item) => item.uid)).toEqual(['second', 'first']); expect(result.data[0]).not.toHaveProperty('accountId'); }); });