Files
vyndr/tests/unit/snapshotGating.test.js
T
builtbykev aaa41134d4 Close the second leak path: /api/hero-prop bypassed the snapshot gate
The landing hero reads the snapshot from Redis DIRECTLY via heroPropService,
so it never passed through routes/snapshot.js and was still serving model_odds,
ev_pct, value and takeable to anonymous visitors after the first fix. Same
strip, same tier resolution, same private-cache rule for authenticated callers;
the Next proxy now forwards the bearer token.

PRODUCT CONSEQUENCE, FLAGGED RATHER THAN BURIED: the landing hero is served to
anonymous visitors, so it now renders BOOK and FAIR with the model leg LOCKED
instead of the full triplet it showed this morning. That follows the stated
free-tier rule exactly, but it does trade a strong marketing moment (VALUE
+21.1% VS FAIR on the shop window) for consistency of the gate. Reversing is
one line — add 'model_price' to the free tier in src/config/tiers.js, or
special-case the hero route — and is a product call, not a correctness one.

Tests 3557 passed / 291 suites, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
2026-07-20 19:52:00 -04:00

158 lines
6.9 KiB
JavaScript

/* ============================================================
Session 67 — the model price must never leave the server for an unentitled
viewer, and the FAIR leg must never be gated.
`GET /api/snapshot/:sport` is PUBLIC. Session 66 gated model_odds on
/api/analyze; this endpoint bypassed that gate and served the model price
(and p_win, and an invertible ev_pct) to anonymous callers on every graded
row. These tests lock the fix shut.
============================================================ */
const { stripModelPrice, MODEL_FIELDS, MARKET_FIELDS, entitledToModelPrice } = require('../../src/utils/snapshotGating');
const { resolveTierFromRequest } = require('../../src/utils/requestTier');
// A real shape, from a live /api/snapshot/mlb row.
const ROW = Object.freeze({
player_name: 'Josh Bell', stat_type: 'hits', line: 0.5, direction: 'over', grade: 'B',
book_odds: -210, fair_odds: -173, fair_prob: 0.633, overround: 0.068, devig_method: 'multiplicative',
model_odds: -311, p_win: 0.757, ev_pct: 11.7, value: false, takeable: false,
projection: 0.8, archetype: 'DRIVER',
});
describe('stripModelPrice — free/anonymous never receive the model price', () => {
it('removes EVERY model-derived field for free', () => {
const [out] = stripModelPrice([ROW], 'free');
for (const f of MODEL_FIELDS) expect(out[f]).toBeUndefined();
});
it('removes them for anonymous / unknown / garbage tiers (fails closed)', () => {
for (const tier of [undefined, null, '', 'anonymous', 'nonsense', 'FREE ']) {
const [out] = stripModelPrice([ROW], tier);
expect(out.model_odds).toBeUndefined();
expect(out.p_win).toBeUndefined();
}
});
it('strips ev_pct too — it is INVERTIBLE back to p_win', () => {
// ev is a function of (p_win, book_odds); book_odds is public, so leaving
// ev behind hands over the model price in a different base.
const [out] = stripModelPrice([ROW], 'free');
expect(out.ev_pct).toBeUndefined();
expect(MODEL_FIELDS).toContain('ev_pct');
});
it('KEEPS every market field — the fair leg is never the paywall', () => {
const [out] = stripModelPrice([ROW], 'free');
for (const f of MARKET_FIELDS) expect(out[f]).toBe(ROW[f]);
expect(out.fair_odds).toBe(-173);
});
it('flags the lock so a gated price is not mistaken for a missing one', () => {
const [out] = stripModelPrice([ROW], 'free');
expect(out.model_price_locked).toBe(true);
});
it('does NOT flag a lock on a row with no price story to lock onto', () => {
const [out] = stripModelPrice([{ ...ROW, book_odds: null, fair_odds: null }], 'free');
expect(out.model_price_locked).toBeUndefined();
});
it('analyst and desk receive the full triplet, untouched', () => {
for (const tier of ['analyst', 'desk']) {
const [out] = stripModelPrice([ROW], tier);
expect(out.model_odds).toBe(-311);
expect(out.p_win).toBe(0.757);
expect(out.ev_pct).toBe(11.7);
expect(out.model_price_locked).toBeUndefined();
}
expect(entitledToModelPrice('analyst')).toBe(true);
expect(entitledToModelPrice('free')).toBe(false);
});
it('never mutates the input rows', () => {
const input = [{ ...ROW }];
stripModelPrice(input, 'free');
expect(input[0].model_odds).toBe(-311);
});
it('survives junk rows without throwing', () => {
expect(() => stripModelPrice([null, undefined, 'x', 7], 'free')).not.toThrow();
expect(stripModelPrice(null, 'free')).toBeNull();
});
});
describe('resolveTierFromRequest — a public endpoint that still gates', () => {
const noSupabase = { getSupabaseServiceClient: () => null };
it('anonymous (no header) is free', async () => {
expect(await resolveTierFromRequest({ headers: {} }, noSupabase)).toBe('free');
});
it('a malformed or empty bearer is free', async () => {
expect(await resolveTierFromRequest({ headers: { authorization: 'Basic x' } }, noSupabase)).toBe('free');
expect(await resolveTierFromRequest({ headers: { authorization: 'Bearer ' } }, noSupabase)).toBe('free');
});
it('FAILS CLOSED — a thrown resolver yields free, never an entitled tier', async () => {
const boom = { getSupabaseServiceClient: () => { throw new Error('supabase down'); } };
expect(await resolveTierFromRequest({ headers: { authorization: 'Bearer abc' } }, boom)).toBe('free');
});
it('an invalid token is free', async () => {
const bad = {
getSupabaseServiceClient: () => ({
auth: { getUser: async () => ({ data: null, error: new Error('bad token') }) },
}),
};
expect(await resolveTierFromRequest({ headers: { authorization: 'Bearer abc' } }, bad)).toBe('free');
});
it('a valid token resolves the real tier', async () => {
const good = {
getSupabaseServiceClient: () => ({
auth: { getUser: async () => ({ data: { user: { id: 'u1' } }, error: null }) },
from: () => ({ select: () => ({ eq: () => ({ single: async () => ({ data: { tier: 'desk' } }) }) }) }),
}),
};
expect(await resolveTierFromRequest({ headers: { authorization: 'Bearer good' } }, good)).toBe('desk');
});
});
describe('the route wiring', () => {
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'routes', 'snapshot.js'), 'utf8');
it('gates BOTH response paths (snapshot and the grades fallback)', () => {
const gated = src.match(/grades: gate\(/g) || [];
expect(gated.length).toBe(2);
});
it('never shared-caches a response that varies by entitlement', () => {
// A CDN handing a paid payload to an anonymous viewer would defeat the gate.
expect(src).toMatch(/req\.headers\.authorization \? 'private, max-age=30' : 'public, max-age=30'/);
// Scoped to the /:sport handler — /summary carries counts only (no price
// data), so it stays legitimately public-cacheable.
const bySport = src.slice(src.indexOf("router.get('/:sport'"));
expect(bySport).not.toMatch(/res\.set\('Cache-Control', 'public, max-age=30'\)/);
});
it('the hero-prop route applies the SAME gate (it reads Redis directly)', () => {
const hero = fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'routes', 'heroProp.js'), 'utf8');
// The hero bypassed routes/snapshot.js entirely, so it needed its own strip
// or the landing page kept serving the model price to anonymous visitors.
expect(hero).toContain('stripModelPrice');
expect(hero).toContain('resolveTierFromRequest');
expect(hero).not.toMatch(/res\.set\('Cache-Control', 'public, max-age=300'\);\n\s*return res\.json\(hero\)/);
});
it('the browser proxy forwards the bearer token', () => {
const proxy = fs.readFileSync(
path.join(__dirname, '..', '..', 'web', 'src', 'app', 'api', 'snapshot', '[sport]', 'route.ts'),
'utf8',
);
expect(proxy).toMatch(/Authorization: auth/);
expect(proxy).toMatch(/private, max-age=30/);
});
});