d242b11b4b
PROMISE-AUDIT.md: every /pricing claim → verified/built/reworded. BUILT (was vapor): alt line ladder + edge ranking (same-features regrade at shifted lines, Desk-gated at the API), quarter-Kelly (engine quantile P(win) x real captured odds — either missing → no sizing), free-tier kill-condition locked previews. FIXED (was false): analyst 15/day cap vs the Founder 'Unlimited reads' promise → analyst unlimited; every '40+ factors' claim (real count: 22 named features) reworded truthfully in 7 files. VERIFIED: cascade alerts (real, wired), phi correlation, leg history, cross-book comparison, WC soccer, real-time feed. Locked by tests/unit/promiseAudit.test.js. Jest now ignores .claude/worktrees (parallel agents' suites no longer leak into runs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
125 lines
4.1 KiB
JavaScript
125 lines
4.1 KiB
JavaScript
const { scanLimit, __internals } = require('../../src/middleware/scanLimit');
|
|
|
|
function mockReqRes({ ip = '1.1.1.1', tier, userId } = {}) {
|
|
const req = {
|
|
ip,
|
|
socket: { remoteAddress: ip },
|
|
headers: {},
|
|
user: userId ? { id: userId, tier } : (tier ? { tier } : null),
|
|
};
|
|
const headers = {};
|
|
const res = {
|
|
statusCode: 200,
|
|
body: null,
|
|
set(k, v) { headers[k] = v; return res; },
|
|
status(c) { res.statusCode = c; return res; },
|
|
json(b) { res.body = b; return res; },
|
|
};
|
|
return { req, res, headers };
|
|
}
|
|
|
|
beforeEach(() => __internals.resetForTests());
|
|
|
|
describe('scanLimit middleware', () => {
|
|
const mw = scanLimit();
|
|
|
|
test('free user allowed up to 3 scans per day', () => {
|
|
const next = jest.fn();
|
|
for (let i = 0; i < 3; i += 1) {
|
|
const { req, res } = mockReqRes({ tier: 'free', userId: 'u-free' });
|
|
mw(req, res, next);
|
|
}
|
|
expect(next).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
test('free user 4th scan in the window returns 429', () => {
|
|
const next = jest.fn();
|
|
let lastRes;
|
|
for (let i = 0; i < 4; i += 1) {
|
|
const { req, res } = mockReqRes({ tier: 'free', userId: 'u-free-block' });
|
|
mw(req, res, next);
|
|
lastRes = res;
|
|
}
|
|
expect(next).toHaveBeenCalledTimes(3);
|
|
expect(lastRes.statusCode).toBe(429);
|
|
expect(lastRes.body.error).toMatch(/scan limit/i);
|
|
expect(lastRes.body.scans_used).toBe(3);
|
|
expect(lastRes.body.scans_limit).toBe(3);
|
|
expect(lastRes.body.tier).toBe('free');
|
|
});
|
|
|
|
test('429 response includes Retry-After + X-Scans-* headers', () => {
|
|
const next = jest.fn();
|
|
let lastHeaders;
|
|
for (let i = 0; i < 4; i += 1) {
|
|
const { req, res, headers } = mockReqRes({ tier: 'free', userId: 'u-hdr' });
|
|
mw(req, res, next);
|
|
lastHeaders = headers;
|
|
}
|
|
expect(parseInt(lastHeaders['Retry-After'], 10)).toBeGreaterThan(0);
|
|
expect(lastHeaders['X-Scans-Used']).toBe('3');
|
|
expect(lastHeaders['X-Scans-Limit']).toBe('3');
|
|
});
|
|
|
|
// Session 62 (A1-S1) — analyst is UNLIMITED now (the Founder card promises
|
|
// "Unlimited reads"; the 15/day cap made the pricing page false).
|
|
test('analyst tier is unlimited (Founder promise)', () => {
|
|
const next = jest.fn();
|
|
let blockedAt = null;
|
|
for (let i = 0; i < 30; i += 1) {
|
|
const { req, res } = mockReqRes({ tier: 'analyst', userId: 'u-analyst' });
|
|
mw(req, res, next);
|
|
if (res.statusCode === 429 && blockedAt == null) blockedAt = i;
|
|
}
|
|
expect(blockedAt).toBeNull();
|
|
expect(next).toHaveBeenCalledTimes(30);
|
|
});
|
|
|
|
test('desk tier is unlimited — never blocks', () => {
|
|
const next = jest.fn();
|
|
for (let i = 0; i < 100; i += 1) {
|
|
const { req, res } = mockReqRes({ tier: 'desk', userId: 'u-desk' });
|
|
mw(req, res, next);
|
|
expect(res.statusCode).toBe(200);
|
|
}
|
|
expect(next).toHaveBeenCalledTimes(100);
|
|
});
|
|
|
|
test('anonymous user (no req.user) falls back to free quota keyed by IP', () => {
|
|
const next = jest.fn();
|
|
let lastRes;
|
|
for (let i = 0; i < 4; i += 1) {
|
|
const { req, res } = mockReqRes({ ip: '7.7.7.7' });
|
|
mw(req, res, next);
|
|
lastRes = res;
|
|
}
|
|
expect(next).toHaveBeenCalledTimes(3);
|
|
expect(lastRes.statusCode).toBe(429);
|
|
});
|
|
|
|
test('different IPs are independent (anonymous bucket)', () => {
|
|
const next = jest.fn();
|
|
const a = mockReqRes({ ip: '1.1.1.1' });
|
|
const b = mockReqRes({ ip: '2.2.2.2' });
|
|
mw(a.req, a.res, next);
|
|
mw(b.req, b.res, next);
|
|
expect(next).toHaveBeenCalledTimes(2);
|
|
expect(a.res.statusCode).toBe(200);
|
|
expect(b.res.statusCode).toBe(200);
|
|
});
|
|
|
|
test('authenticated users in the same IP get independent quotas', () => {
|
|
const next = jest.fn();
|
|
// Same IP, different user IDs — should not interfere.
|
|
for (let i = 0; i < 3; i += 1) {
|
|
const { req, res } = mockReqRes({ tier: 'free', userId: 'u-A', ip: '5.5.5.5' });
|
|
mw(req, res, next);
|
|
}
|
|
// User A exhausted; user B starts clean.
|
|
const { req, res } = mockReqRes({ tier: 'free', userId: 'u-B', ip: '5.5.5.5' });
|
|
mw(req, res, next);
|
|
expect(res.statusCode).toBe(200);
|
|
expect(next).toHaveBeenCalledTimes(4);
|
|
});
|
|
});
|