Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions packages/cli/src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2682,4 +2682,155 @@ describe('production mode', () => {
expect(output).toMatch(/networkId/i);
});
});

describe('ucp', () => {
describe('catalog search', () => {
it('GETs /ucp/catalog/search with the query and array filters', async () => {
setNextResponse(200, {
data: [{ sku_id: 'sku_1', name: 'Sneaker', price: 2500 }],
total_count: 1,
has_more: false,
});

const result = await runProdCli(
'ucp',
'catalog',
'search',
'--query',
'sneakers',
'--brand',
'Acme',
'--limit',
'5',
'--json',
);

expect(result.exitCode).toBe(0);
expect(lastRequest.method).toBe('GET');
expect(lastRequest.url).toContain('/ucp/catalog/search');
expect(lastRequest.url).toContain('query=sneakers');
expect(lastRequest.url).toContain('brand%5B%5D=Acme');
expect(lastRequest.headers.authorization).toBe(
'Bearer prod_test_access_token',
);

const output = parseJson(result.stdout) as Record<string, unknown>;
const data = output.data as Record<string, unknown>[];
expect(data[0].sku_id).toBe('sku_1');
});

it('errors without a query or filter, without hitting the API', async () => {
const result = await runProdCli('ucp', 'catalog', 'search', '--json');

expect(result.exitCode).toBe(1);
expect(requests).toHaveLength(0);
const output = parseJson(result.stdout) as Record<string, unknown>;
expect(output.code).toBe('INVALID_INPUT');
});
});

describe('checkout create', () => {
it('POSTs profile_id and parsed line items to /ucp/checkout', async () => {
setNextResponse(200, {
id: 'dcs_1',
status: 'requires_payment',
currency: 'usd',
amount_total: 5500,
});

const result = await runProdCli(
'ucp',
'checkout',
'create',
'--network-id',
'np_1',
'--line-item',
'sku_id:sku_1,quantity:2',
'--json',
);

expect(result.exitCode).toBe(0);
expect(lastRequest.method).toBe('POST');
expect(lastRequest.url).toBe('/ucp/checkout');
const body = JSON.parse(lastRequest.body);
// Flag is --network-id; the wire field stays profile_id (UCP API contract).
expect(body.profile_id).toBe('np_1');
expect(body.line_items).toEqual([{ sku_id: 'sku_1', quantity: 2 }]);
expect(body.currency).toBe('usd');

const output = parseJson(result.stdout) as Record<string, unknown>;
expect(output.id).toBe('dcs_1');
// Agent mode includes a _next hint to complete the checkout.
expect((output._next as Record<string, unknown>).command).toContain(
'ucp checkout complete dcs_1',
);
});

it('rejects a line item with a non-positive quantity', async () => {
const result = await runProdCli(
'ucp',
'checkout',
'create',
'--network-id',
'np_1',
'--line-item',
'sku_id:sku_1,quantity:0',
'--json',
);

expect(result.exitCode).toBe(1);
expect(requests).toHaveLength(0);
const output = parseJson(result.stdout) as Record<string, unknown>;
expect(output.code).toBe('INVALID_INPUT');
});
});

describe('checkout complete', () => {
it('POSTs the shared payment token to the confirm path', async () => {
setNextResponse(200, {
id: 'dcs_1',
status: 'completed',
order_details: { status: 'confirmed' },
});

const result = await runProdCli(
'ucp',
'checkout',
'complete',
'dcs_1',
'--shared-payment-token',
'spt_1',
'--json',
);

expect(result.exitCode).toBe(0);
expect(lastRequest.method).toBe('POST');
expect(lastRequest.url).toBe('/ucp/checkout/dcs_1/complete');
expect(JSON.parse(lastRequest.body).shared_payment_token).toBe('spt_1');

const output = parseJson(result.stdout) as Record<string, unknown>;
expect(output.status).toBe('completed');
});

it('surfaces an upstream card error', async () => {
setNextResponse(402, {
error: { code: 'card_declined', message: 'Your card was declined.' },
});

const result = await runProdCli(
'ucp',
'checkout',
'complete',
'dcs_1',
'--shared-payment-token',
'spt_1',
'--json',
);

expect(result.exitCode).toBe(1);
const combined = result.stdout + result.stderr;
expect(combined).toContain('Your card was declined.');
});
});
});
});
8 changes: 8 additions & 0 deletions packages/cli/src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createShippingAddressCli } from './commands/shipping-address';
import { createSourcesCli } from './commands/sources';
import { createSpendRequestCli } from './commands/spend-request';
import { createTransactionsCli } from './commands/transactions';
import { createUcpCli } from './commands/ucp';
import { createUserInfoCli } from './commands/user-info';
import { createWebBotAuthCli } from './commands/web-bot-auth';
import { ResourceFactory } from './utils/resource-factory';
Expand Down Expand Up @@ -158,6 +159,13 @@ if (!hiddenCli) {
envAccessToken,
),
);
cli.command(
createUcpCli(
() => factory.createUcpResource(),
authStorage,
envAccessToken,
),
);
cli.command(
createDemoCli(
authRepo,
Expand Down
Loading