Session 29: Content generation templates — slate threads, POTD, recaps, matchup previews (1660 tests)
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// Integration: /api/content routes (Session 29).
|
||||
// The template service is mocked at the collector boundary via redis +
|
||||
// real generators; recap reads a mocked results cache.
|
||||
|
||||
const express = require('express');
|
||||
const request = require('supertest');
|
||||
|
||||
const mockStore = {};
|
||||
jest.mock('../../src/utils/redis', () => ({
|
||||
cacheGet: jest.fn(async (k) => (k in mockStore ? mockStore[k] : null)),
|
||||
getRedisClient: () => ({ scan: async () => ['0', []], lrange: async () => [] }),
|
||||
isDegraded: () => false,
|
||||
}));
|
||||
|
||||
// Mock collectSlateData so the route doesn't hit live adapters; keep the
|
||||
// real generators so we exercise the actual content shapes.
|
||||
jest.mock('../../src/services/contentTemplateService', () => {
|
||||
const actual = jest.requireActual('../../src/services/contentTemplateService');
|
||||
return { ...actual, collectSlateData: jest.fn() };
|
||||
});
|
||||
const template = require('../../src/services/contentTemplateService');
|
||||
|
||||
function mountApp() {
|
||||
delete require.cache[require.resolve('../../src/routes/content')];
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use('/api/content', require('../../src/routes/content'));
|
||||
return app;
|
||||
}
|
||||
|
||||
const SLATE = {
|
||||
sport: 'mlb',
|
||||
schedule: [{ id: 'g1', homeTeam: { name: 'Reds', abbreviation: 'CIN' }, awayTeam: { name: 'D-backs', abbreviation: 'ARI' }, gameTime: '2026-06-13T23:10:00Z', venue: 'GABP' }],
|
||||
gameLines: { '20260613_ARI@CIN': { homeTeam: 'CIN', awayTeam: 'ARI', books: { bet365: { homeML: '-120', awayML: '+100', total: '9.5', homeSpread: '-1.5' } } } },
|
||||
grades: [],
|
||||
streaks: [{ player: 'Acuna', team: 'ARI', description: '3-game hit streak', currentStreak: 3 }],
|
||||
movers: [],
|
||||
bestLines: [],
|
||||
dataLevel: 'lines',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
for (const k of Object.keys(mockStore)) delete mockStore[k];
|
||||
jest.clearAllMocks();
|
||||
template.collectSlateData.mockResolvedValue(SLATE);
|
||||
});
|
||||
|
||||
describe('GET /api/content/slate/:sport', () => {
|
||||
test('returns a slate thread structure', async () => {
|
||||
const res = await request(mountApp()).get('/api/content/slate/mlb');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.type).toBe('slate_thread');
|
||||
expect(res.body.dataLevel).toBe('lines');
|
||||
expect(res.body.posts[0].role).toBe('hook');
|
||||
});
|
||||
|
||||
test('?format=text adds formatted post strings', async () => {
|
||||
const res = await request(mountApp()).get('/api/content/slate/mlb?format=text');
|
||||
expect(Array.isArray(res.body.text)).toBe(true);
|
||||
expect(res.body.text[0]).toMatch(/VYNDR SLATE/);
|
||||
});
|
||||
|
||||
test('unsupported sport → 404', async () => {
|
||||
const res = await request(mountApp()).get('/api/content/slate/cricket');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/content/potd/:sport', () => {
|
||||
test('lines data → game of the day', async () => {
|
||||
const res = await request(mountApp()).get('/api/content/potd/mlb');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.subtype).toBe('game_of_the_day');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/content/recap/:sport', () => {
|
||||
test('no resolved grades → available:false', async () => {
|
||||
const res = await request(mountApp()).get('/api/content/recap/mlb');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.available).toBe(false);
|
||||
});
|
||||
|
||||
test('with cached results → record computed', async () => {
|
||||
const utc = new Date().toISOString().split('T')[0];
|
||||
mockStore[`results:mlb:${utc}`] = [
|
||||
{ player: 'A', grade: 'A', confidence: 80, result: 'win' },
|
||||
{ player: 'B', grade: 'B', confidence: 60, result: 'loss' },
|
||||
];
|
||||
const res = await request(mountApp()).get('/api/content/recap/mlb');
|
||||
expect(res.body.available).toBe(true);
|
||||
expect(res.body.record).toEqual({ wins: 1, losses: 1, pushes: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/content/preview/:sport/:gameId', () => {
|
||||
test('returns matchup data for a scheduled game', async () => {
|
||||
const res = await request(mountApp()).get('/api/content/preview/mlb/g1');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.type).toBe('matchup_preview');
|
||||
expect(res.body.home.abbreviation).toBe('CIN');
|
||||
expect(res.body.lines.bookCount).toBe(1);
|
||||
expect(res.body.playerStreaks.map((s) => s.player)).toEqual(['Acuna']);
|
||||
});
|
||||
|
||||
test('unknown game → 404', async () => {
|
||||
const res = await request(mountApp()).get('/api/content/preview/mlb/nope');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user