deckhearth/test/api/user-stats-valuation.test.js
Randall Stillwell d0468015eb
Some checks failed
CI / Lint (pull_request) Has been cancelled
CI / Schema map up to date (pull_request) Has been cancelled
CI / Forbidden patterns (9 checks) (pull_request) Has been cancelled
CI / Migrations apply (node-pg-migrate) (pull_request) Has been cancelled
Visual diff / Should run? (pull_request) Has been cancelled
CI / Unit tests (vitest) (pull_request) Has been cancelled
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Has been cancelled
PR Health rollup / Aggregate gate status (pull_request) Has been cancelled
Preview smoke / Should run? (pull_request) Has been cancelled
Preview smoke / Playwright smoke (pull_request) Has been cancelled
Visual diff / Screenshot diff (pull_request) Has been cancelled
feat(pricing): convoy for online prices and local stores
Add pricing schema, free API integrations (MTG bulk, TCGdex, Foolio),
valuation and sort-by-price, card detail pricing UI, and nearby store
discovery via home zip and OSM geo clients.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-08 22:47:46 -05:00

152 lines
4.6 KiB
JavaScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('../../lib/sql.js', () => ({
sql: vi.fn((strings, ...values) => {
if (values.length === 0) {
return { __sqlFragment: strings.join('') };
}
return Promise.resolve({ rows: [] });
}),
}));
vi.mock('../../lib/permission-middleware.js', () => ({
getUserFromRequest: vi.fn(),
}));
import { sql } from '../../lib/sql.js';
import { getUserFromRequest } from '../../lib/permission-middleware.js';
import { effectiveUnitPriceExpr } from '../../lib/pricing/collection-valuation.js';
import handler from '../../pages/api/user/stats.js';
const UNIT_PRICE_SQL = effectiveUnitPriceExpr({ cardAlias: 'cards', userCardAlias: 'user_cards' });
function createRes() {
const res = {
statusCode: 200,
body: null,
status(code) {
res.statusCode = code;
return res;
},
json(data) {
res.body = data;
return res;
},
};
return res;
}
function queryText(call) {
const [strings, ...values] = call;
const fragments = values.map((value) =>
value && typeof value === 'object' && value.__sqlFragment ? value.__sqlFragment : '?'
);
return strings.reduce((text, part, index) => text + part + (fragments[index] ?? ''), '');
}
describe('GET /api/user/stats foil-aware valuation', () => {
beforeEach(() => {
vi.clearAllMocks();
getUserFromRequest.mockResolvedValue({ userId: 1, email: 'a@b.c', role: 'user' });
sql.mockImplementation((strings, ...values) => {
if (values.length === 0) {
return { __sqlFragment: strings.join('') };
}
return Promise.resolve({ rows: [] });
});
});
it('uses effective unit price SQL for totals and breakdowns', async () => {
sql.mockImplementation((strings, ...values) => {
if (values.length === 0) {
return { __sqlFragment: strings.join('') };
}
const text = queryText([strings, ...values]);
if (text.includes('SUM') && text.includes('total_value') && !text.includes('GROUP BY')) {
return Promise.resolve({ rows: [{ total_value: '6.50' }] });
}
if (text.includes('GROUP BY cards.game')) {
return Promise.resolve({
rows: [{ game: 'MTG', unique_cards: '2', total_quantity: '3', total_value: '6.50' }],
});
}
if (text.includes('GROUP BY cards.rarity')) {
return Promise.resolve({
rows: [{ rarity: 'rare', unique_cards: '2', total_quantity: '3', total_value: '6.50' }],
});
}
if (text.includes('ORDER BY user_cards.created_at DESC')) {
return Promise.resolve({
rows: [
{
name: 'Lightning Bolt',
game: 'MTG',
rarity: 'common',
image_url: null,
market_price: '1.00',
quantity: 2,
created_at: '2026-01-01T00:00:00.000Z',
},
{
name: 'Lightning Bolt',
game: 'MTG',
rarity: 'common',
image_url: null,
market_price: '4.50',
quantity: 1,
created_at: '2026-01-02T00:00:00.000Z',
},
],
});
}
if (text.includes('total_cards')) {
return Promise.resolve({ rows: [{ total_cards: '3' }] });
}
if (text.includes('total_collections')) {
return Promise.resolve({ rows: [{ total_collections: '1' }] });
}
if (text.includes('total_decks')) {
return Promise.resolve({ rows: [{ total_decks: '0' }] });
}
return Promise.resolve({ rows: [] });
});
const req = { method: 'GET' };
const res = createRes();
await handler(req, res);
expect(res.statusCode).toBe(200);
expect(res.body.totalValue).toBe(6.5);
expect(res.body.recentActivity[0].marketPrice).toBe(1);
expect(res.body.recentActivity[1].marketPrice).toBe(4.5);
const valuationCalls = sql.mock.calls.filter((call) => queryText(call).includes('total_value'));
for (const call of valuationCalls) {
const text = queryText(call);
expect(text).toContain(UNIT_PRICE_SQL);
expect(text).not.toContain('cards.market_price * user_cards.quantity');
}
const recentActivityCall = sql.mock.calls.find((call) =>
queryText(call).includes('ORDER BY user_cards.created_at DESC')
);
expect(queryText(recentActivityCall)).toContain(`${UNIT_PRICE_SQL} as market_price`);
});
it('returns 401 when unauthenticated', async () => {
getUserFromRequest.mockResolvedValue(null);
const req = { method: 'GET' };
const res = createRes();
await handler(req, res);
expect(res.statusCode).toBe(401);
expect(sql).not.toHaveBeenCalled();
});
});