/** * Integration tests for ScreenerController + /health * Uses Fastify inject() — no real Yahoo calls, no live server. */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import Fastify from 'fastify'; import cors from '@fastify/cors'; import { ScreenerController } from '../server/controllers/screener.controller'; import type { ScreenerEngine } from '../server/services/ScreenerEngine'; import type { ScreenerResult, MarketContext } from '../server/types'; // ── Fixture data ──────────────────────────────────────────────────────────── const MARKET_CTX: MarketContext = { sp500Price: 5000, riskFreeRate: 4.5, vixLevel: 18, rateRegime: 'NORMAL', volatilityRegime: 'NORMAL', benchmarks: { marketPE: 22, techPE: 30, reitYield: 3.5, igSpread: 1.0 }, }; const EMPTY_RESULT: ScreenerResult = { STOCK: [], ETF: [], BOND: [], ERROR: [], marketContext: MARKET_CTX, }; // ── Stub ──────────────────────────────────────────────────────────────────── const stubEngine = { screenTickers: async (_tickers: string[]) => EMPTY_RESULT, getMarketContext: async () => MARKET_CTX, } as unknown as ScreenerEngine; // ── App factory ───────────────────────────────────────────────────────────── async function buildTestApp() { const app = Fastify({ logger: false }); await app.register(cors, { origin: '*' }); new ScreenerController(stubEngine).register(app); app.get('/health', async () => ({ status: 'ok' })); await app.ready(); return app; } // ── Tests ──────────────────────────────────────────────────────────────────── test('GET /health → 200 { status: ok }', async () => { const app = await buildTestApp(); const res = await app.inject({ method: 'GET', url: '/health' }); assert.equal(res.statusCode, 200); assert.deepEqual(res.json(), { status: 'ok' }); }); test('POST /api/screen → 200 with STOCK/ETF/BOND/ERROR/marketContext keys', async () => { const app = await buildTestApp(); const res = await app.inject({ method: 'POST', url: '/api/screen', payload: { tickers: ['AAPL'] }, }); assert.equal(res.statusCode, 200); const body = res.json(); assert.ok(Array.isArray(body.STOCK), 'STOCK should be array'); assert.ok(Array.isArray(body.ETF), 'ETF should be array'); assert.ok(Array.isArray(body.BOND), 'BOND should be array'); assert.ok(Array.isArray(body.ERROR), 'ERROR should be array'); assert.ok(body.marketContext, 'marketContext should be present'); }); test('POST /api/screen → marketContext has expected shape', async () => { const app = await buildTestApp(); const res = await app.inject({ method: 'POST', url: '/api/screen', payload: { tickers: ['MSFT'] }, }); const { marketContext } = res.json(); assert.ok(typeof marketContext.riskFreeRate === 'number'); assert.ok(typeof marketContext.sp500Price === 'number'); assert.ok(typeof marketContext.vixLevel === 'number'); assert.ok(marketContext.benchmarks); }); test('POST /api/screen with missing tickers → 400', async () => { const app = await buildTestApp(); const res = await app.inject({ method: 'POST', url: '/api/screen', payload: {}, }); assert.equal(res.statusCode, 400); }); test('POST /api/screen with empty tickers array → 400', async () => { const app = await buildTestApp(); const res = await app.inject({ method: 'POST', url: '/api/screen', payload: { tickers: [] }, }); assert.equal(res.statusCode, 400); }); test('POST /api/screen with too many tickers (>50) → 400', async () => { const app = await buildTestApp(); const res = await app.inject({ method: 'POST', url: '/api/screen', payload: { tickers: Array.from({ length: 51 }, (_, i) => `T${i}`) }, }); assert.equal(res.statusCode, 400); });