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
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>
120 lines
2.9 KiB
JavaScript
120 lines
2.9 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
buildTcgdexPriceIndex,
|
|
getTcgdexBaseUrl,
|
|
matchTcgdexCard,
|
|
parseTcgdexPricing,
|
|
} from '../../../lib/pricing/tcgdex-client.js';
|
|
|
|
describe('tcgdex-client', () => {
|
|
beforeEach(() => {
|
|
delete process.env.TCGDEX_BASE_URL;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('uses default TCGdex base URL', () => {
|
|
expect(getTcgdexBaseUrl()).toBe('https://api.tcgdex.net/v2/en');
|
|
});
|
|
|
|
it('parses TCGPlayer USD pricing into current and market prices', () => {
|
|
const parsed = parseTcgdexPricing({
|
|
pricing: {
|
|
tcgplayer: {
|
|
normal: {
|
|
lowPrice: 0.5,
|
|
marketPrice: 1.25,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed).toEqual({
|
|
currentPrice: 0.5,
|
|
marketPrice: 1.25,
|
|
currency: 'USD',
|
|
});
|
|
});
|
|
|
|
it('falls back to Cardmarket EUR for market_price when USD is missing', () => {
|
|
const parsed = parseTcgdexPricing({
|
|
pricing: {
|
|
cardmarket: {
|
|
trend: 2.5,
|
|
unit: 'EUR',
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(parsed.currentPrice).toBeNull();
|
|
expect(parsed.marketPrice).toBe(2.5);
|
|
expect(parsed.currency).toBe('EUR');
|
|
});
|
|
|
|
it('matches cards by set card number with leading zero normalization', () => {
|
|
const index = buildTcgdexPriceIndex([
|
|
{
|
|
id: 'me3-18',
|
|
localId: '18',
|
|
name: 'Seel',
|
|
set: { cardCount: { official: 88 } },
|
|
},
|
|
]);
|
|
|
|
const match = matchTcgdexCard(index, {
|
|
name: 'Seel',
|
|
cardNumber: '018/88',
|
|
});
|
|
|
|
expect(match?.id).toBe('me3-18');
|
|
});
|
|
|
|
it('falls back to unique name match within a set index', () => {
|
|
const index = buildTcgdexPriceIndex([
|
|
{ id: 'sv1-001', localId: '1', name: 'Sprigatito' },
|
|
]);
|
|
|
|
const match = matchTcgdexCard(index, {
|
|
name: 'Sprigatito',
|
|
cardNumber: null,
|
|
});
|
|
|
|
expect(match?.id).toBe('sv1-001');
|
|
});
|
|
|
|
it('fetchSetCards loads set summaries and full card details', async () => {
|
|
const fetchMock = vi.fn(async (url) => {
|
|
if (url.endsWith('/sets/me3')) {
|
|
return {
|
|
ok: true,
|
|
json: async () => ({
|
|
cards: [{ id: 'me3-18', localId: '18', name: 'Seel' }],
|
|
}),
|
|
};
|
|
}
|
|
if (url.endsWith('/cards/me3-18')) {
|
|
return {
|
|
ok: true,
|
|
json: async () => ({
|
|
id: 'me3-18',
|
|
localId: '18',
|
|
name: 'Seel',
|
|
pricing: {
|
|
tcgplayer: { normal: { lowPrice: 0.1, marketPrice: 0.2 } },
|
|
},
|
|
}),
|
|
};
|
|
}
|
|
throw new Error(`unexpected url ${url}`);
|
|
});
|
|
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
const { fetchSetCards } = await import('../../../lib/pricing/tcgdex-client.js');
|
|
const cards = await fetchSetCards('me3');
|
|
|
|
expect(cards).toHaveLength(1);
|
|
expect(cards[0].pricing.tcgplayer.normal.marketPrice).toBe(0.2);
|
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|