import { afterEach, describe, expect, it } from 'vitest'; import metaJson from './meta.json' with { type: 'json' }; import { loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; import { missingRequirements } from '@rareindex/connectors'; import { localMeta } from '../_lib/local-meta.js'; import { clearTokenCache } from '../_g10-lib/index.js'; import createConnector, { apiBase, getAppToken, parseSearchResponse, searchUrl } from './index.js'; const meta = localMeta(metaJson); const connector = createConnector(meta); describe('ebay-browse (gated)', () => { runFixtureSuite(connector, it, expect); afterEach(() => clearTokenCache()); it('is reported as gated until EBAY_CLIENT_ID / EBAY_CLIENT_SECRET exist', () => { expect(meta.requires).toEqual(['EBAY_CLIENT_ID', 'EBAY_CLIENT_SECRET']); expect(missingRequirements(meta, {})).toEqual(['EBAY_CLIENT_ID', 'EBAY_CLIENT_SECRET']); expect(missingRequirements(meta, { EBAY_CLIENT_ID: 'a', EBAY_CLIENT_SECRET: 'b' })).toEqual([]); expect(apiBase(undefined).token).toBe('https://api.ebay.com/identity/v1/oauth2/token'); expect(apiBase('sandbox').api).toBe('https://api.sandbox.ebay.com'); }); it('builds search URLs with marketplace-independent params and filters', () => { const url = searchUrl('https://api.ebay.com', { q: 'pokemon psa 10', categoryIds: ['183454'], categorySlug: 'pokemon', filter: 'price:[100..],priceCurrency:USD' }, { limit: 200, offset: 400, filter: 'buyingOptions:{FIXED_PRICE|AUCTION}', sort: 'newlyListed' }); const u = new URL(url); expect(u.pathname).toBe('/buy/browse/v1/item_summary/search'); expect(u.searchParams.get('q')).toBe('pokemon psa 10'); expect(u.searchParams.get('category_ids')).toBe('183454'); expect(u.searchParams.get('filter')).toBe('buyingOptions:{FIXED_PRICE|AUCTION},price:[100..],priceCurrency:USD'); expect(u.searchParams.get('limit')).toBe('200'); expect(u.searchParams.get('offset')).toBe('400'); }); it('requests and caches the client-credentials token (Basic auth, form body, api_scope)', async () => { let calls = 0; const fakeFetch = (async (url: string | URL | Request, init?: RequestInit) => { calls++; expect(String(url)).toBe('https://api.ebay.com/identity/v1/oauth2/token'); expect((init?.headers as Record).authorization).toBe(`Basic ${Buffer.from('id:secret').toString('base64')}`); expect((init?.headers as Record)['content-type']).toBe('application/x-www-form-urlencoded'); expect(String(init?.body)).toBe('grant_type=client_credentials&scope=https%3A%2F%2Fapi.ebay.com%2Foauth%2Fapi_scope'); return new Response(JSON.stringify({ access_token: 'v^1.1#tok', expires_in: 7200, token_type: 'Application Access Token' }), { status: 200 }); }) as typeof fetch; const t1 = await getAppToken('id', 'secret', 'https://api.ebay.com/identity/v1/oauth2/token', fakeFetch); const t2 = await getAppToken('id', 'secret', 'https://api.ebay.com/identity/v1/oauth2/token', fakeFetch); expect(t1).toBe('v^1.1#tok'); expect(t2).toBe(t1); expect(calls).toBe(1); }); it('parses the documented search response shape and normalises listings in native currency', async () => { const fx = loadFixture('ebay-browse', 'docs-search-ebay-us'); const payload = fx.raw.payload as { items: unknown[] }; const parsed = parseSearchResponse({ itemSummaries: payload.items, total: 12345, next: 'https://api.ebay.com/buy/browse/v1/item_summary/search?q=x&limit=2&offset=2' }); expect(parsed?.items.length).toBe(payload.items.length); expect(parsed?.next).toContain('offset=2'); expect(parseSearchResponse({ errors: [{ errorId: 12000 }] })).toBeNull(); const out = await connector.normalize(fx.raw); expect(out.length).toBeGreaterThan(0); const first = out[0]!; if (first.kind !== 'listing') throw new Error('expected listing'); expect(first.currency).toBe('USD'); expect(first.attributes.identifiers.ebay_item_id).toMatch(/^v1\|\d+\|\d+$/); expect(first.attributes.identifiers.ebay_epid).toBeTruthy(); expect(first.seller).toBeTruthy(); expect(first.sellerReputation).toMatch(/%/); expect(first.shippingCost).not.toBeNull(); expect(first.listingType).toBe('best_offer'); // FIXED_PRICE + BEST_OFFER → best_offer const auction = out.find((r) => r.kind === 'listing' && r.listingType === 'auction'); expect(auction && auction.kind === 'listing' ? auction.bidCount : null).not.toBeNull(); const gb = loadFixture('ebay-browse', 'docs-search-ebay-gb-converted'); const gbOut = await connector.normalize(gb.raw); expect(gbOut.length).toBe(1); const g = gbOut[0]!; if (g.kind !== 'listing') throw new Error('expected listing'); expect(g.currency).toBe('GBP'); expect(g.attributes.metadata.price_converted_by_ebay).toBe(true); expect(g.grade.grader).toBe('psa'); expect(g.grade.grade).toBe('10'); }); });