learning_ai_common_plat/packages/marketplace-client/src/client.test.ts
saravanakumardb1 be03efa111 feat(shared-packages): add 9 @bytelyst/* client packages with 100% API coverage
Packages added:
- @bytelyst/referral-client — referral API client + share helpers
- @bytelyst/subscription-client — subscription/plan API client + cache
- @bytelyst/celebrations — milestone triggers, confetti, positive messages
- @bytelyst/gentle-notifications — ND-friendly messaging, forbidden phrases
- @bytelyst/accessibility — VoiceOver/TalkBack label generators
- @bytelyst/quick-actions — progressive disclosure, smart defaults
- @bytelyst/time-references — familiar duration references
- @bytelyst/org-client — org/workspace/membership/license API client
- @bytelyst/marketplace-client — listing/review/install API client

All packages: pure TS, ESM, globalThis.fetch, no Node.js deps.
99 Vitest tests across 9 packages, 79/79 public methods covered.

Review fixes applied:
- time-references: fix module-level mutable state leak + add clearCustomReferences()
- accessibility: fix parameter reassignment in formatDurationForA11y/numberToWords
- subscription-client: fix flaky daysRemaining test (ms boundary race)
2026-03-19 13:10:09 -07:00

283 lines
8.0 KiB
TypeScript

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>): 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>): 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>): 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<typeof vi.fn>;
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<typeof vi.fn>;
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<typeof vi.fn>;
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<typeof vi.fn>;
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<typeof vi.fn>;
const callHeaders = fetchMock.mock.calls[0][1].headers as Record<string, string>;
expect(callHeaders['x-product-id']).toBe('testapp');
expect(callHeaders['Authorization']).toBe('Bearer test-token');
expect(callHeaders['x-request-id']).toBeDefined();
});
});