140 lines
5.3 KiB
JavaScript
140 lines
5.3 KiB
JavaScript
// Integration: /api/internal/prefetch/tank01 (Session 18).
|
|
//
|
|
// Verifies (a) the endpoint is gated by the shared internal key,
|
|
// (b) the request body is translated into the prefetch module's
|
|
// argv shape correctly, and (c) the module's summary is returned
|
|
// verbatim. The prefetch module itself is mocked — it has its own
|
|
// unit coverage in tests/unit/tank01Prefetch.test.js.
|
|
|
|
const express = require('express');
|
|
const request = require('supertest');
|
|
|
|
jest.mock('../../scripts/tank01-prefetch', () => ({
|
|
main: jest.fn(),
|
|
}));
|
|
const tank01Prefetch = require('../../scripts/tank01-prefetch');
|
|
|
|
jest.mock('../../src/services/quotaTracker', () => ({
|
|
getAllQuotaStatuses: jest.fn(),
|
|
}));
|
|
const quotaTracker = require('../../src/services/quotaTracker');
|
|
|
|
beforeEach(() => {
|
|
jest.resetAllMocks();
|
|
process.env.VYNDR_INTERNAL_KEY = 'test-internal-key-9999';
|
|
});
|
|
|
|
function mountApp() {
|
|
// Reset the require cache so the route picks up the mocked
|
|
// tank01-prefetch module fresh between tests.
|
|
delete require.cache[require.resolve('../../src/routes/internal')];
|
|
const internalRoutes = require('../../src/routes/internal');
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use('/api/internal', internalRoutes);
|
|
return app;
|
|
}
|
|
|
|
describe('POST /api/internal/prefetch/tank01', () => {
|
|
test('rejects requests without the internal key', async () => {
|
|
const app = mountApp();
|
|
const res = await request(app)
|
|
.post('/api/internal/prefetch/tank01')
|
|
.send({ dryRun: true });
|
|
expect(res.status).toBe(401);
|
|
expect(tank01Prefetch.main).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('translates body into argv and returns the summary', async () => {
|
|
const fakeSummary = {
|
|
nba: { games: 3, boxscores: 8, odds: true, skipped: null },
|
|
mlb: { games: 5, boxscores: 12, bvp: 0, skipped: null, bvp_skipped_reason: null },
|
|
requestsSpent: 4,
|
|
dryRun: false,
|
|
};
|
|
tank01Prefetch.main.mockResolvedValueOnce(fakeSummary);
|
|
|
|
const app = mountApp();
|
|
const res = await request(app)
|
|
.post('/api/internal/prefetch/tank01')
|
|
.set('x-internal-key', 'test-internal-key-9999')
|
|
.send({ max: 25, sports: ['nba', 'mlb'], dryRun: false });
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual({ ok: true, summary: fakeSummary });
|
|
expect(tank01Prefetch.main).toHaveBeenCalledTimes(1);
|
|
const argv = tank01Prefetch.main.mock.calls[0][0];
|
|
expect(argv).toContain('--max=25');
|
|
expect(argv).toContain('--sports=nba,mlb');
|
|
expect(argv).not.toContain('--dry-run');
|
|
});
|
|
|
|
test('forwards --dry-run when requested', async () => {
|
|
tank01Prefetch.main.mockResolvedValueOnce({ dryRun: true });
|
|
const app = mountApp();
|
|
const res = await request(app)
|
|
.post('/api/internal/prefetch/tank01')
|
|
.set('x-internal-key', 'test-internal-key-9999')
|
|
.send({ dryRun: true });
|
|
expect(res.status).toBe(200);
|
|
const argv = tank01Prefetch.main.mock.calls[0][0];
|
|
expect(argv).toContain('--dry-run');
|
|
});
|
|
|
|
test('returns 500 with the underlying error when the module rejects', async () => {
|
|
tank01Prefetch.main.mockRejectedValueOnce(new Error('budget exhausted'));
|
|
const app = mountApp();
|
|
const res = await request(app)
|
|
.post('/api/internal/prefetch/tank01')
|
|
.set('x-internal-key', 'test-internal-key-9999')
|
|
.send({});
|
|
expect(res.status).toBe(500);
|
|
expect(res.body).toEqual({ ok: false, error: 'budget exhausted' });
|
|
});
|
|
|
|
test('accepts string sports (single sport) and forwards it', async () => {
|
|
tank01Prefetch.main.mockResolvedValueOnce({ ok: true });
|
|
const app = mountApp();
|
|
await request(app)
|
|
.post('/api/internal/prefetch/tank01')
|
|
.set('x-internal-key', 'test-internal-key-9999')
|
|
.send({ sports: 'mlb' });
|
|
const argv = tank01Prefetch.main.mock.calls[0][0];
|
|
expect(argv).toContain('--sports=mlb');
|
|
});
|
|
});
|
|
|
|
describe('GET /api/internal/quota (Session 20)', () => {
|
|
test('rejects without the internal key', async () => {
|
|
const app = mountApp();
|
|
const res = await request(app).get('/api/internal/quota');
|
|
expect(res.status).toBe(401);
|
|
expect(quotaTracker.getAllQuotaStatuses).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('returns the per-provider snapshot when keyed', async () => {
|
|
const fakeSnapshot = [
|
|
{ provider: 'odds-api', name: 'The Odds API', used: 487, limit: 500, remaining: 13, pct: 0.974, allowed: false, period: '2026-06', quotaType: 'monthly' },
|
|
{ provider: 'tank01', name: 'Tank01', used: 30, limit: 1000, remaining: 970, pct: 0.03, allowed: true, period: '2026-06', quotaType: 'monthly' },
|
|
];
|
|
quotaTracker.getAllQuotaStatuses.mockResolvedValueOnce(fakeSnapshot);
|
|
|
|
const app = mountApp();
|
|
const res = await request(app)
|
|
.get('/api/internal/quota')
|
|
.set('x-internal-key', 'test-internal-key-9999');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual({ ok: true, providers: fakeSnapshot });
|
|
});
|
|
|
|
test('returns 500 with the underlying error when the tracker throws', async () => {
|
|
quotaTracker.getAllQuotaStatuses.mockRejectedValueOnce(new Error('redis down'));
|
|
const app = mountApp();
|
|
const res = await request(app)
|
|
.get('/api/internal/quota')
|
|
.set('x-internal-key', 'test-internal-key-9999');
|
|
expect(res.status).toBe(500);
|
|
expect(res.body).toEqual({ ok: false, error: 'redis down' });
|
|
});
|
|
});
|