'use strict'; /** * proplineVerify — Order Zero Phase 1 unit tests. * * Hermetic: no network, no key. Locks the two properties that make this tool * trustworthy — (1) the key can never escape in any emitted string, and (2) * consensus eligibility counts only reference books posting BOTH sides at the * SAME line (a one-sided quote cannot be de-vigged, so it cannot rule). */ const { __internals } = require('../../src/services/proplineVerify'); const { analyseBreadth, scrubKeys, probe, summarise, detectRedaction, REFERENCE_CANDIDATES, REFERENCE_POLICIES, DFS_PLATFORMS } = __internals; const ALLOWED = new Set(['draftkings', 'fanduel', 'betmgm', 'betrivers', 'pinnacle']); /** Build a PropLine-shaped event. books = { bookKey: [{player, point, over, under}] } */ function event(id, books) { return { id, home_team: 'Home', away_team: 'Away', bookmakers: Object.entries(books).map(([key, quotes]) => ({ key, markets: [{ key: 'batter_hits', outcomes: quotes.flatMap((q) => [ ...(q.over != null ? [{ name: 'Over', description: q.player, point: q.point, price: q.over }] : []), ...(q.under != null ? [{ name: 'Under', description: q.player, point: q.point, price: q.under }] : []), ]), }], })), }; } describe('proplineVerify — key safety', () => { const realKeys = process.env.PROPLINE_API_KEY_1; afterEach(() => { process.env.PROPLINE_API_KEY_1 = realKeys; }); it('scrubs the configured key out of any string', () => { process.env.PROPLINE_API_KEY_1 = 'SUPERSECRETKEY123'; const leak = 'GET https://api.prop-line.com/v1/x?apiKey=SUPERSECRETKEY123&markets=a failed'; const out = scrubKeys(leak); expect(out).not.toContain('SUPERSECRETKEY123'); expect(out).toContain(''); }); it('scrubs an apiKey query value even when the key is not configured', () => { delete process.env.PROPLINE_API_KEY_1; expect(scrubKeys('?apiKey=abc123xyz&z=1')).toBe('?apiKey=&z=1'); }); it('probe never throws and never emits the key on a transport error', async () => { process.env.PROPLINE_API_KEY_1 = 'SUPERSECRETKEY123'; const httpGet = async () => { throw new Error('connect ECONNREFUSED apiKey=SUPERSECRETKEY123'); }; const r = await probe('/x', {}, 'note', httpGet); expect(r.verdict).toBe('no'); expect(JSON.stringify(r)).not.toContain('SUPERSECRETKEY123'); }); }); describe('proplineVerify — probe verdicts', () => { beforeEach(() => { process.env.PROPLINE_API_KEY_1 = 'k'; }); const withStatus = (status, data, headers = {}) => async () => ({ status, data, headers }); it('200 with data = works', async () => { expect((await probe('/x', {}, '', withStatus(200, [{ a: 1 }]))).verdict).toBe('works'); }); it('200 with an empty body = partial, not works', async () => { expect((await probe('/x', {}, '', withStatus(200, []))).verdict).toBe('partial'); }); it('403 = no (tier-gated for us)', async () => { expect((await probe('/x', {}, '', withStatus(403, { detail: 'upgrade' }))).verdict).toBe('no'); }); it('404 = partial (reachable, nothing on file)', async () => { expect((await probe('/x', {}, '', withStatus(404, { detail: 'none' }))).verdict).toBe('partial'); }); it('counts CSV rows excluding the header', async () => { const r = await probe('/x', {}, '', withStatus(200, 'h1,h2\na,b\nc,d\n')); expect(r.csv_rows).toBe(2); }); }); describe('proplineVerify — breadth', () => { it('reports the cost of our own allow-list, and props made invisible by it', () => { const raw = [event('e1', { draftkings: [{ player: 'A', point: 1.5, over: -110, under: -110 }], novig: [{ player: 'A', point: 1.5, over: -104, under: -104 }], kalshi: [{ player: 'A', point: 1.5, over: -103, under: -105 }], // B exists at three books, NONE of them admitted -> invisible to VYNDR smarkets: [{ player: 'B', point: 0.5, over: 120, under: -140 }], prizepicks: [{ player: 'B', point: 0.5, over: -119, under: -119 }], underdog: [{ player: 'B', point: 0.5, over: -118, under: -118 }], })]; const r = analyseBreadth(raw, ALLOWED); expect(r.props).toBe(2); expect(r.feed.mean_books_per_prop).toBe(3); expect(r.after_allow_list.mean_books_per_prop).toBe(0.5); expect(r.invisible_props).toBe(1); expect(r.invisible_pct).toBe(50); }); it('consensus eligibility requires TWO-SIDED quotes at the SAME line', () => { const raw = [event('e1', { // Prop A: two reference books, both two-sided, SAME line -> eligible novig: [{ player: 'A', point: 1.5, over: -104, under: -104 }], kalshi: [{ player: 'A', point: 1.5, over: -103, under: -105 }], // Prop B: two reference books but DIFFERENT lines -> NOT eligible smarkets: [{ player: 'B', point: 0.5, over: 100, under: -120 }], matchbook: [{ player: 'B', point: 1.5, over: 200, under: -240 }], // Prop C: two reference books, same line, but one is ONE-SIDED -> NOT eligible polymarket: [{ player: 'C', point: 2.5, over: 150, under: -180 }], bovada: [{ player: 'C', point: 2.5, over: 145 }], })]; const r = analyseBreadth(raw, ALLOWED); expect(r.props).toBe(3); expect(r.consensus_eligible_n2).toBe(1); expect(r.consensus_eligible_n3).toBe(0); }); it('DFS platforms are reported separately and are never reference candidates', () => { for (const dfs of DFS_PLATFORMS) expect(REFERENCE_CANDIDATES).not.toContain(dfs); const raw = [event('e1', { prizepicks: [{ player: 'A', point: 1.5, over: -119, under: -119 }], underdog: [{ player: 'A', point: 1.5, over: -118, under: -118 }], sleeper: [{ player: 'A', point: 1.5, over: -120, under: -120 }], })]; const r = analyseBreadth(raw, ALLOWED); // 100% DFS coverage must NOT create consensus eligibility. expect(r.dfs_presence_EXCLUDED_FROM_CONSENSUS.prizepicks.pct).toBe(100); expect(r.consensus_eligible_n2).toBe(0); }); it('ignores game-line outcomes that carry no player description', () => { const raw = [{ id: 'e1', bookmakers: [{ key: 'draftkings', markets: [{ key: 'h2h', outcomes: [{ name: 'Home', price: -130 }] }] }], }]; expect(analyseBreadth(raw, ALLOWED).props).toBe(0); }); it('summarise never returns object internals for a null body', () => { expect(summarise(null)).toBe('null'); expect(summarise([{ a: 1 }])).toContain('array(1)'); }); }); describe('proplineVerify — redaction detection', () => { beforeEach(() => { process.env.PROPLINE_API_KEY_1 = 'k'; }); const priced = { bookmakers: [{ key: 'dk', markets: [{ outcomes: [{ price: -110 }, { price: -110 }] }] }] }; const stripped = { bookmakers: [{ key: 'dk', markets: [{ outcomes: [{ price: null }, {}] }] }], upgrade_url: 'https://x' }; it('flags a structure-only body with an upgrade_url as redacted', () => { const r = detectRedaction(stripped); expect(r.outcomes).toBe(2); expect(r.priced).toBe(0); expect(r.redacted).toBe(true); }); it('does not flag a genuinely priced body', () => { const r = detectRedaction(priced); expect(r.priced).toBe(2); expect(r.redacted).toBe(false); }); it('DOWNGRADES a 200 redacted body from works to partial', async () => { const httpGet = async () => ({ status: 200, data: stripped, headers: {} }); const r = await probe('/x', {}, '', httpGet); expect(r.verdict).toBe('partial'); // a non-empty body is NOT proof of access expect(r.redaction.redacted).toBe(true); }); it('keeps works when the body carries real prices', async () => { const httpGet = async () => ({ status: 200, data: priced, headers: {} }); expect((await probe('/x', {}, '', httpGet)).verdict).toBe('works'); }); }); describe('proplineVerify — reference policies', () => { it('no policy admits a DFS platform', () => { for (const books of Object.values(REFERENCE_POLICIES)) { for (const dfs of DFS_PLATFORMS) expect(books).not.toContain(dfs); } }); it('the curve widens monotonically as the policy widens', () => { const raw = [event('e1', { novig: [{ player: 'A', point: 1.5, over: -104, under: -104 }], draftkings: [{ player: 'A', point: 1.5, over: -110, under: -110 }], betmgm: [{ player: 'A', point: 1.5, over: -112, under: -108 }], })]; const c = analyseBreadth(raw, ALLOWED).reference_policy_curve; expect(c.exchange_only.n2).toBe(0); // one exchange only expect(c.exchange_plus_us.n2).toBe(1); // exchange + two US books expect(c.takeable_only.n2).toBe(1); }); });