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(); }); });