import { describe, expect, it } from 'vitest'; import { getConnectorMeta, missingRequirements } from '@rareindex/connectors'; import { loadFixture, runFixtureSuite } from '@rareindex/connectors/testing'; import createConnector, { buildUrl, redact, toItem } from './index.js'; const connector = createConnector(getConnectorMeta('rakuten-ichiba')); describe('rakuten-ichiba (gated)', () => { runFixtureSuite(connector, it, expect); it('is reported as gated on RAKUTEN_APP_ID', () => { expect(connector.meta.requires).toContain('RAKUTEN_APP_ID'); expect(missingRequirements(connector.meta, {})).toEqual(['RAKUTEN_APP_ID']); expect(missingRequirements(connector.meta, { RAKUTEN_APP_ID: 'x' })).toEqual([]); }); it('accepts formatVersion 1 ({ Item: {…} }, [{ imageUrl }]) and formatVersion 2 (flat, string images)', () => { const v1 = toItem({ Item: { itemCode: 'shop:1', itemName: 'A', itemPrice: 1200, itemUrl: 'https://item.rakuten.co.jp/shop/1/', mediumImageUrls: [{ imageUrl: 'https://img/1.jpg?_ex=128x128' }], availability: 1, shopName: 'S', genreId: 101905 } }); expect(v1).toMatchObject({ itemCode: 'shop:1', itemPrice: 1200, images: ['https://img/1.jpg'], availability: 1, genreId: '101905' }); const v2 = toItem({ itemCode: 'shop:2', itemName: 'B', itemPrice: '3000', itemUrl: 'https://item.rakuten.co.jp/shop/2/', mediumImageUrls: ['https://img/2.jpg'], availability: 0 }); expect(v2).toMatchObject({ itemCode: 'shop:2', itemPrice: 3000, images: ['https://img/2.jpg'], availability: 0 }); expect(toItem({ itemCode: 'x', itemName: 'no price', itemUrl: 'u' })).toBeNull(); }); it('builds the documented request (2026-07-01 endpoint with access key, legacy without) and redacts credentials', () => { const modern = buildUrl({ appId: 'APP', accessKey: 'KEY', keyword: 'ポケモンカード', page: 2 }); expect(modern.startsWith('https://openapi.rakuten.co.jp/ichibams/api/IchibaItem/Search/20260701?')).toBe(true); expect(modern).toContain('formatVersion=2'); expect(modern).toContain('hits=30'); expect(modern).toContain('page=2'); expect(modern).toContain('accessKey=KEY'); const legacy = buildUrl({ appId: 'APP', accessKey: null, keyword: 'x', page: 1 }); expect(legacy.startsWith('https://app.rakuten.co.jp/services/api/IchibaItem/Search/20220601?')).toBe(true); expect(legacy).not.toContain('accessKey'); expect(redact(modern)).not.toContain('KEY'); expect(redact(modern)).toContain('applicationId=***'); }); it('normalises the documented response shape into JPY listings (grade, condition, bundle flag, sale window)', async () => { const fx = loadFixture('rakuten-ichiba', 'docs-format-v2'); const out = await connector.normalize(fx.raw); expect(out).toHaveLength(2); const [a, b] = out; if (a?.kind !== 'listing' || b?.kind !== 'listing') throw new Error('expected listings'); expect(a.currency).toBe('JPY'); expect(a.grade).toMatchObject({ grader: 'psa', grade: '10' }); expect(a.condition.conditionRaw).toBe('Used'); expect(a.attributes.metadata.tax_included).toBe(true); expect(b.availability).toBe('ended'); expect(b.attributes.metadata.is_bundle).toBe(true); expect(b.listedAt?.toISOString()).toBe('2026-09-01T01:00:00.000Z'); expect(b.endsAt?.toISOString()).toBe('2026-09-30T14:59:00.000Z'); }); });