import { describe, it, expect, vi, afterEach } from 'vitest'; import { createMarketplaceClient } from './client.js'; import type { MarketplaceListingDoc, MarketplaceReviewDoc, MarketplaceInstallDoc, } from './types.js'; const baseConfig = { baseUrl: 'http://localhost:4003/api', productId: 'testapp', getAccessToken: () => 'test-token', }; function mockListing(overrides?: Partial): MarketplaceListingDoc { return { id: 'lst_1', productId: 'testapp', templateType: 'fasting_protocol', authorId: 'user-1', authorName: 'Alice', title: '16:8 Protocol', shortDescription: 'Classic intermittent fasting', description: 'A detailed 16:8 fasting protocol.', tags: ['fasting', 'beginner'], category: 'protocols', payload: { hours: 16, breakHours: 8 }, pricingModel: 'free', priceInCents: 0, certificationStatus: 'approved', installCount: 100, reviewCount: 10, averageRating: 4.5, visibility: 'public', featured: false, version: '1.0.0', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', ...overrides, }; } function mockReview(overrides?: Partial): MarketplaceReviewDoc { return { id: 'rev_1', listingId: 'lst_1', productId: 'testapp', authorId: 'user-2', rating: 5, title: 'Great protocol!', body: 'Really works well for me.', verified: true, createdAt: '2026-01-01T00:00:00Z', ...overrides, }; } function mockInstall(overrides?: Partial): MarketplaceInstallDoc { return { id: 'inst_1', listingId: 'lst_1', productId: 'testapp', userId: 'user-2', version: '1.0.0', installedAt: '2026-01-01T00:00:00Z', uninstalledAt: null, ...overrides, }; } describe('createMarketplaceClient', () => { afterEach(() => { vi.restoreAllMocks(); }); it('should list listings', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ listings: [mockListing()], total: 1 }), }) ); const client = createMarketplaceClient(baseConfig); const result = await client.listListings({ category: 'protocols' }); expect(result.listings).toHaveLength(1); expect(result.total).toBe(1); const fetchMock = globalThis.fetch as ReturnType; const url = fetchMock.mock.calls[0][0] as string; expect(url).toContain('category=protocols'); }); it('should get a listing by id', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(mockListing()), }) ); const client = createMarketplaceClient(baseConfig); const result = await client.getListing('lst_1'); expect(result.title).toBe('16:8 Protocol'); }); it('should create a listing', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(mockListing()), }) ); const client = createMarketplaceClient(baseConfig); const result = await client.createListing({ templateType: 'fasting_protocol', title: '16:8 Protocol', shortDescription: 'Classic', description: 'Detailed', category: 'protocols', payload: { hours: 16 }, }); expect(result.id).toBe('lst_1'); const fetchMock = globalThis.fetch as ReturnType; expect(fetchMock).toHaveBeenCalledWith( expect.stringContaining('/marketplace/listings'), expect.objectContaining({ method: 'POST' }) ); }); it('should submit for certification', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(mockListing({ certificationStatus: 'submitted' })), }) ); const client = createMarketplaceClient(baseConfig); const result = await client.submitForCertification('lst_1', 'Ready for review'); expect(result.certificationStatus).toBe('submitted'); }); it('should install a listing', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(mockInstall()), }) ); const client = createMarketplaceClient(baseConfig); const result = await client.installListing('lst_1'); expect(result.listingId).toBe('lst_1'); }); it('should list my installs', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve([mockInstall()]), }) ); const client = createMarketplaceClient(baseConfig); const result = await client.listMyInstalls(); expect(result).toHaveLength(1); }); it('should list reviews', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve([mockReview()]), }) ); const client = createMarketplaceClient(baseConfig); const result = await client.listReviews('lst_1'); expect(result).toHaveLength(1); expect(result[0].rating).toBe(5); }); it('should create a review', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(mockReview()), }) ); const client = createMarketplaceClient(baseConfig); const result = await client.createReview('lst_1', { rating: 5, title: 'Great!', body: 'Love it.', }); expect(result.rating).toBe(5); }); it('should update a listing', async () => { const updated = mockListing({ title: 'Updated Title' }); vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(updated), }) ); const client = createMarketplaceClient(baseConfig); const result = await client.updateListing('lst_1', { title: 'Updated Title' }); expect(result.title).toBe('Updated Title'); const fetchMock = globalThis.fetch as ReturnType; expect(fetchMock).toHaveBeenCalledWith( 'http://localhost:4003/api/marketplace/listings/lst_1', expect.objectContaining({ method: 'PATCH' }) ); }); it('should uninstall a listing', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, }) ); const client = createMarketplaceClient(baseConfig); await expect(client.uninstallListing('lst_1')).resolves.toBeUndefined(); const fetchMock = globalThis.fetch as ReturnType; expect(fetchMock).toHaveBeenCalledWith( 'http://localhost:4003/api/marketplace/listings/lst_1/uninstall', expect.objectContaining({ method: 'POST' }) ); }); it('should report a listing', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, }) ); const client = createMarketplaceClient(baseConfig); await expect( client.reportListing('lst_1', { reason: 'spam', details: 'Looks like spam' }) ).resolves.toBeUndefined(); }); it('should throw on non-ok response', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: false, status: 500, }) ); const client = createMarketplaceClient(baseConfig); await expect(client.getListing('lst_1')).rejects.toThrow('getListing failed: 500'); }); it('should send correct headers', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ listings: [], total: 0 }), }) ); const client = createMarketplaceClient(baseConfig); await client.listListings(); const fetchMock = globalThis.fetch as ReturnType; const callHeaders = fetchMock.mock.calls[0][1].headers as Record; expect(callHeaders['x-product-id']).toBe('testapp'); expect(callHeaders['Authorization']).toBe('Bearer test-token'); expect(callHeaders['x-request-id']).toBeDefined(); }); });