From eaff1d86bbeed198b1aaf382d02f230324a192f0 Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 21 Jul 2026 11:18:35 +0200 Subject: [PATCH] =?UTF-8?q?test(10-05):=20controller=20spec=20=E2=80=94=20?= =?UTF-8?q?global=20read=20not=20tenant-scoped,=20admin=20config=20applies?= =?UTF-8?q?=20to=20scheduler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET / findMany where clause asserted to have no tenantId key - GET /:id returns tender / throws NotFoundException for missing id - PUT /source-config isActive=true+pollIntervalMin=30 -> setInterval(30) single-arg - PUT /source-config isActive=false -> stopJob() --- .../src/tenders/tenders.controller.spec.ts | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 apps/api/src/tenders/tenders.controller.spec.ts diff --git a/apps/api/src/tenders/tenders.controller.spec.ts b/apps/api/src/tenders/tenders.controller.spec.ts new file mode 100644 index 0000000..7cbd66c --- /dev/null +++ b/apps/api/src/tenders/tenders.controller.spec.ts @@ -0,0 +1,107 @@ +import { NotFoundException } from '@nestjs/common'; +import { describe, expect, it, vi } from 'vitest'; +import { TendersController } from './tenders.controller'; + +/** + * TendersController.spec — automated proof for INGEST-06 and the + * tenant-gated-not-scoped invariant (RESEARCH.md V4, T-10-14): + * + * - The global read path (`GET /`) never adds the tenant's id to the + * prisma `where` clause — the catalog is platform-wide, not row-scoped. + * - Saving the admin source-config drives the scheduler's setInterval() + * (single argument, no tenant id) / stopJob() exactly as the DKV analog + * does, minus the tenant argument. + * + * Uses the same hand-rolled prisma-shaped fake convention as + * tender-ingestion.service.spec.ts / tender-scheduler.service.spec.ts + * (in-memory fakes, no live DB connection). + */ + +function makeFakePrisma() { + const tenders = new Map(); + tenders.set('t1', { id: 't1', title: 'Beispielausschreibung', status: 'active' }); + const configs = new Map(); + configs.set('doe-opendata', { + id: 'cfg1', + sourceType: 'doe-opendata', + pollIntervalMin: 60, + isActive: true, + lastIngestedDay: null, + }); + + return { + tender: { + findMany: vi.fn(async (_args?: any) => Array.from(tenders.values())), + count: vi.fn(async (_args?: any) => tenders.size), + findUnique: vi.fn(async ({ where }: any) => tenders.get(where.id) ?? null), + }, + tenderSourcePollConfig: { + findUnique: vi.fn(async ({ where }: any) => configs.get(where.sourceType) ?? null), + upsert: vi.fn(async ({ where, update, create }: any) => { + const existing = configs.get(where.sourceType); + const record = existing ? { ...existing, ...update } : { ...create }; + configs.set(where.sourceType, record); + return record; + }), + }, + }; +} + +describe('TendersController — global read (not tenant-scoped)', () => { + it('GET / calls prisma.tender.findMany with a where clause that has no tenantId key', async () => { + const prisma = makeFakePrisma(); + const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any; + const controller = new TendersController(prisma as any, scheduler); + + await controller.listTenders({}); + + expect(prisma.tender.findMany).toHaveBeenCalledTimes(1); + const callArgs = prisma.tender.findMany.mock.calls[0][0]; + expect(callArgs.where).not.toHaveProperty('tenantId'); + }); + + it('GET /:id returns the tender when found and never scopes by tenant', async () => { + const prisma = makeFakePrisma(); + const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any; + const controller = new TendersController(prisma as any, scheduler); + + const result = await controller.getTender('t1'); + + expect(result.id).toBe('t1'); + const callArgs = prisma.tender.findUnique.mock.calls[0][0]; + expect(callArgs.where).toEqual({ id: 't1' }); + }); + + it('GET /:id throws NotFoundException for a missing id', async () => { + const prisma = makeFakePrisma(); + const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any; + const controller = new TendersController(prisma as any, scheduler); + + await expect(controller.getTender('missing')).rejects.toBeInstanceOf(NotFoundException); + }); +}); + +describe('TendersController — admin source-config applies live to the scheduler (INGEST-06)', () => { + it('PUT /source-config with isActive=true + pollIntervalMin=30 calls scheduler.setInterval(30) with a single argument', async () => { + const prisma = makeFakePrisma(); + const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any; + const controller = new TendersController(prisma as any, scheduler); + + await controller.saveSourceConfig({ isActive: true, pollIntervalMin: 30 }); + + expect(scheduler.setInterval).toHaveBeenCalledTimes(1); + expect(scheduler.setInterval).toHaveBeenCalledWith(30); + expect(scheduler.stopJob).not.toHaveBeenCalled(); + }); + + it('PUT /source-config with isActive=false calls scheduler.stopJob()', async () => { + const prisma = makeFakePrisma(); + const scheduler = { setInterval: vi.fn(), stopJob: vi.fn() } as any; + const controller = new TendersController(prisma as any, scheduler); + + await controller.saveSourceConfig({ isActive: false }); + + expect(scheduler.stopJob).toHaveBeenCalledTimes(1); + expect(scheduler.setInterval).not.toHaveBeenCalled(); + }); +});