Session 31: Code audit + security review — NFL MARKET_MAP gap fixed, npm audit 0 vulns (1695 tests)
- Add NFL keys to oddsNormalizer.MARKET_MAP (defensive; same silent-zero class as the Session 30 MLB bug) + NFL surface test - npm audit fix: ws/qs + Supabase transitives, 7 vulns -> 0 (semver-safe) - Audit findings documented in BUILD-STATE: grades cache has no writer, NFL/NHL not wired end-to-end, rate limiting only on /analyze, tests mutate a tracked jsonl, leaked GitHub PAT in origin remote (rotate) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,76 @@
|
|||||||
2026-06-14
|
2026-06-14
|
||||||
|
|
||||||
## Current Phase
|
## Current Phase
|
||||||
|
AUDIT v31.0 — Full code audit + security review + cleanup (Session 31)
|
||||||
|
|
||||||
|
## Session 31 (2026-06-14) — SHIPPED (audit)
|
||||||
|
|
||||||
|
Full-codebase audit, not a feature build. Validated 30 sessions of
|
||||||
|
assumptions across the API→normalizer→cache→route→proxy→frontend chain.
|
||||||
|
Backend 1694 → **1695 tests** (+1), 137 suites, zero regressions. Web
|
||||||
|
build clean. `npm audit`: 7 vulns (3 high, 4 moderate) → **0**.
|
||||||
|
|
||||||
|
### FIXES APPLIED
|
||||||
|
1. **MARKET_MAP NFL gap (silent-failure class, same family as the MLB
|
||||||
|
bug Session 30 fixed).** `oddsNormalizer.MARKET_MAP` had ZERO NFL
|
||||||
|
keys — when NFL wires up (season approaching) every NFL prop would
|
||||||
|
normalize to nothing. Added defensive mappings for both odds-api
|
||||||
|
`_yds` and `_yards` spellings → internal stat_types aligned with
|
||||||
|
`config/statFilters.js` (passing_yards/rushing_yards/receiving_yards/
|
||||||
|
interceptions + pass/rush/reception TDs, receptions, anytime_td,
|
||||||
|
kicking_points). Additive/zero-risk (only fires when those markets
|
||||||
|
are returned). +1 explicit NFL surface test.
|
||||||
|
2. **`npm audit fix`** — resolved ws (uninitialized memory disclosure)
|
||||||
|
+ qs (DoS) + transitive Supabase deps, all semver-safe. 0 vulns.
|
||||||
|
|
||||||
|
### SECURITY AUDIT (Ryan Montgomery)
|
||||||
|
- Stripe webhook: ✅ verified via `stripe.webhooks.constructEvent`
|
||||||
|
(`stripeService.constructWebhookEvent`, signature + 400 on failure).
|
||||||
|
- CORS: ✅ allowlist (localhost, vyndr.app, *.vercel.app preview),
|
||||||
|
`FRONTEND_ORIGINS` override — NOT open `*`.
|
||||||
|
- Internal routes: ✅ `requireInternalAuth` via `router.use` on the whole
|
||||||
|
`/api/internal` router (VYNDR_INTERNAL_KEY).
|
||||||
|
- Hardcoded secrets in SOURCE: ✅ none (only doc-comment references).
|
||||||
|
- npm audit: ✅ 0 after fix.
|
||||||
|
- Supabase service-role: scoped to `src/utils/supabase.js` (backend-only;
|
||||||
|
Express enforces its own auth) — acceptable.
|
||||||
|
- 🔴 **CRITICAL (operator action): live GitHub PAT (`ghp_…`) embedded in
|
||||||
|
the `origin` remote URL in `.git/config`.** Not in tracked source (not
|
||||||
|
committed/pushed) but exposed on disk. ROTATE the token and scrub the
|
||||||
|
URL (use a credential helper). Not auto-fixed — needs rotation.
|
||||||
|
|
||||||
|
### FINDINGS DOCUMENTED (deliberately left)
|
||||||
|
- **`grades:{sport}` cache has NO writer.** `contentTemplateService`
|
||||||
|
collector reads it ("when present"), so content slate/POTD never reach
|
||||||
|
`dataLevel: 'full'` in prod — degrades to lines/schedule. Graceful by
|
||||||
|
design; wiring a grades-cache writer is feature work. Severity: medium.
|
||||||
|
- **NFL/NHL props not wired end-to-end.** `oddsService.SPORT_KEYS` has no
|
||||||
|
nfl/nhl; `proplineAdapter.MARKETS.nfl/nhl` are empty. Not a current
|
||||||
|
silent failure (nothing expects them yet). NFL MARKET_MAP now ready;
|
||||||
|
full wiring is feature work. NHL: no product support anywhere (absent
|
||||||
|
from statFilters/streaks) — left entirely.
|
||||||
|
- **Inbound rate limiting only on `/api/analyze`.** Public cached
|
||||||
|
endpoints (/odds, /schedule, /gamelines, /streaks) have no inbound
|
||||||
|
throttle. Real risk bounded (Redis-cached, no per-hit upstream cost).
|
||||||
|
Recommend adding the existing `middleware/rateLimit` to public routers.
|
||||||
|
- **Tests mutate a tracked file** (`data/training/resolutions-2026-06.jsonl`
|
||||||
|
gets resolution rows appended on every run → spurious git diff).
|
||||||
|
Reverted the artifact; logging path should write to a temp/ignored
|
||||||
|
location under test. Severity: low (hygiene).
|
||||||
|
- Cache keys VERIFIED aligned: oddsService writes `odds:{sport}:{UTC}`;
|
||||||
|
content `getBestLines` reads the same. No mismatches found.
|
||||||
|
- Edge cases already covered: PropLine unsupported-sport→null,
|
||||||
|
error→null, all-3-keys-exhausted→null, no-keys→null, and
|
||||||
|
PropLine-error→odds-api fallback all have tests.
|
||||||
|
|
||||||
|
### Files modified
|
||||||
|
- `src/utils/oddsNormalizer.js` (NFL MARKET_MAP keys)
|
||||||
|
- `tests/unit/oddsNormalizer.test.js` (NFL surface test)
|
||||||
|
- `package-lock.json` (npm audit fix)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previous Phase
|
||||||
SHIP BUILD v30.0 — Provider backbone: PropLine 3-key adapter, MLB Stats API, ESPN summary (Session 30)
|
SHIP BUILD v30.0 — Provider backbone: PropLine 3-key adapter, MLB Stats API, ESPN summary (Session 30)
|
||||||
|
|
||||||
## Session 30 (2026-06-14) — SHIPPED
|
## Session 30 (2026-06-14) — SHIPPED
|
||||||
|
|||||||
Generated
+62
-33
@@ -2337,9 +2337,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/anymatch/node_modules/picomatch": {
|
"node_modules/anymatch/node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2385,14 +2385,40 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.6",
|
"version": "1.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
|
||||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
"integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.11",
|
"follow-redirects": "^1.16.0",
|
||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^1.1.0"
|
"https-proxy-agent": "^5.0.1",
|
||||||
|
"proxy-from-env": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/agent-base": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios/node_modules/https-proxy-agent": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "6",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/babel-jest": {
|
"node_modules/babel-jest": {
|
||||||
@@ -2551,9 +2577,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3592,9 +3618,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.11",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -5505,9 +5531,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "8.3.0",
|
"version": "8.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -5522,9 +5548,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -5612,10 +5638,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pure-rand": {
|
"node_modules/pure-rand": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
@@ -5635,9 +5664,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||||
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
|
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.1.0"
|
"side-channel": "^1.1.0"
|
||||||
@@ -6352,9 +6381,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -6765,9 +6794,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.19.0",
|
"version": "8.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
|
|||||||
@@ -35,6 +35,29 @@ const MARKET_MAP = {
|
|||||||
pitcher_earned_runs: 'earned_runs',
|
pitcher_earned_runs: 'earned_runs',
|
||||||
pitcher_hits_allowed: 'hits_allowed',
|
pitcher_hits_allowed: 'hits_allowed',
|
||||||
pitcher_outs: 'outs',
|
pitcher_outs: 'outs',
|
||||||
|
// NFL props (Session 31 audit) — defensive mapping added BEFORE NFL is
|
||||||
|
// fully wired into the props flow, so it can't repeat the MLB silent-zero
|
||||||
|
// bug (MARKET_MAP dropping every market → props normalize to nothing).
|
||||||
|
// The Odds API + PropLine use the abbreviated `_yds` keys; the spec's
|
||||||
|
// `_yards` spellings are mapped too so either form survives. Internal
|
||||||
|
// stat_type names align with config/statFilters.js (passing_yards,
|
||||||
|
// rushing_yards, receiving_yards, interceptions).
|
||||||
|
player_pass_yds: 'passing_yards',
|
||||||
|
player_pass_yards: 'passing_yards',
|
||||||
|
player_pass_tds: 'pass_tds',
|
||||||
|
player_pass_completions: 'pass_completions',
|
||||||
|
player_pass_attempts: 'pass_attempts',
|
||||||
|
player_pass_interceptions: 'interceptions',
|
||||||
|
player_rush_yds: 'rushing_yards',
|
||||||
|
player_rush_yards: 'rushing_yards',
|
||||||
|
player_rush_attempts: 'rush_attempts',
|
||||||
|
player_rush_tds: 'rush_tds',
|
||||||
|
player_receptions: 'receptions',
|
||||||
|
player_reception_yds: 'receiving_yards',
|
||||||
|
player_receiving_yards: 'receiving_yards',
|
||||||
|
player_reception_tds: 'reception_tds',
|
||||||
|
player_anytime_td: 'anytime_td',
|
||||||
|
player_kicking_points: 'kicking_points',
|
||||||
// Soccer props — World Cup 2026 + permanent league support.
|
// Soccer props — World Cup 2026 + permanent league support.
|
||||||
// odds-api keys verified against soccer_fifa_world_cup market list.
|
// odds-api keys verified against soccer_fifa_world_cup market list.
|
||||||
// 'assists' is shared with NBA — sport context discriminates downstream.
|
// 'assists' is shared with NBA — sport context discriminates downstream.
|
||||||
|
|||||||
@@ -121,6 +121,37 @@ describe('oddsNormalizer', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('exposes the NFL market keys added in the Session 31 audit', () => {
|
||||||
|
// Defensive mapping landed before NFL is fully wired so it can't
|
||||||
|
// repeat the MLB silent-zero bug. Both odds-api `_yds` and the
|
||||||
|
// `_yards` spellings must resolve, and internal names align with
|
||||||
|
// config/statFilters.js (passing/rushing/receiving_yards, interceptions).
|
||||||
|
expect(MARKET_MAP.player_pass_yds).toBe('passing_yards');
|
||||||
|
expect(MARKET_MAP.player_pass_yards).toBe('passing_yards');
|
||||||
|
expect(MARKET_MAP.player_rush_yds).toBe('rushing_yards');
|
||||||
|
expect(MARKET_MAP.player_reception_yds).toBe('receiving_yards');
|
||||||
|
expect(MARKET_MAP.player_receiving_yards).toBe('receiving_yards');
|
||||||
|
expect(MARKET_MAP.player_receptions).toBe('receptions');
|
||||||
|
expect(MARKET_MAP.player_pass_interceptions).toBe('interceptions');
|
||||||
|
expect(MARKET_MAP.player_anytime_td).toBe('anytime_td');
|
||||||
|
|
||||||
|
// End-to-end: an NFL market normalizes to a real prop, not zero.
|
||||||
|
const event = makeEvent({
|
||||||
|
bookmakers: [
|
||||||
|
makeBookmaker('draftkings', [
|
||||||
|
makeMarket('player_pass_yds', [
|
||||||
|
makeOutcome('Over', 'Patrick Mahomes', -110, 275.5),
|
||||||
|
makeOutcome('Under', 'Patrick Mahomes', -110, 275.5),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const result = normalizeProps([event]);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].stat_type).toBe('passing_yards');
|
||||||
|
expect(result[0].player).toBe('Patrick Mahomes');
|
||||||
|
});
|
||||||
|
|
||||||
it('handles missing/null odds gracefully (skips incomplete outcomes)', () => {
|
it('handles missing/null odds gracefully (skips incomplete outcomes)', () => {
|
||||||
const event = makeEvent({
|
const event = makeEvent({
|
||||||
bookmakers: [
|
bookmakers: [
|
||||||
|
|||||||
Reference in New Issue
Block a user