Skip to content
Merged
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
11 changes: 8 additions & 3 deletions packages/b2c-cli/test/commands/_test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,14 @@ describe('commands/_test', () => {

await command.run();

expect(logStub.calledWith('Using this.log() - goes through pino')).to.be.true;
expect(traceStub.calledWith('Trace level message')).to.be.true;
expect(infoStub.called).to.be.true;
// Structural assertions: each logger surface gets at least one non-empty
// string argument. The exact debug copy is not part of any contract.
const hadStringArg = (stub: sinon.SinonStub) =>
stub.getCalls().some((call) => typeof call.args[0] === 'string' && (call.args[0] as string).length > 0);

expect(hadStringArg(logStub), 'command.log called with non-empty string').to.equal(true);
expect(hadStringArg(traceStub), 'logger.trace called with non-empty string').to.equal(true);
expect(hadStringArg(infoStub), 'logger.info called with non-empty string').to.equal(true);
});

it('logs with context objects', async () => {
Expand Down
91 changes: 91 additions & 0 deletions packages/b2c-cli/test/commands/content/validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
* For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
*/

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {expect} from 'chai';
import {ux} from '@oclif/core';
import {afterEach, beforeEach} from 'mocha';
Expand Down Expand Up @@ -131,4 +134,92 @@ describe('content validate', () => {

expect(result.totalFiles).to.equal(2);
});

describe('with real validator (no SDK stub)', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'content-validate-'));
});

afterEach(() => {
fs.rmSync(tmpDir, {force: true, recursive: true});
});

it('reports valid pagetype file as valid', async () => {
const filePath = path.join(tmpDir, 'home.json');
// Minimal valid pagetype: only region_definitions is required by the schema
fs.writeFileSync(filePath, JSON.stringify({region_definitions: []}));

const command: any = await createCommand({type: 'pagetype'}, [filePath]);
// Use real glob - the file actually exists on disk
sinon.stub(command, 'jsonEnabled').returns(true);

const result = await command.run();

expect(result.totalFiles).to.equal(1);
expect(result.validFiles).to.equal(1);
expect(result.totalErrors).to.equal(0);
expect(result.results[0].valid).to.equal(true);
expect(result.results[0].schemaType).to.equal('pagetype');
});

it('reports invalid pagetype file (missing required region_definitions) with errors', async () => {
const filePath = path.join(tmpDir, 'broken.json');
// Pagetype requires region_definitions - omitting it triggers a real validator error
fs.writeFileSync(filePath, JSON.stringify({name: {default: 'Bad'}}));

const command: any = await createCommand({type: 'pagetype'}, [filePath]);
// Non-JSON mode is required to exercise the post-render `this.error('Validation failed')` branch
sinon.stub(command, 'jsonEnabled').returns(false);
const stdoutStub = sinon.stub(ux, 'stdout');
const errorStub = sinon.stub(command, 'error').throws(new Error('Validation failed'));

let caught: unknown;
try {
await command.run();
} catch (error) {
caught = error;
}

// The validator must have produced at least one error -> command.error called
expect(errorStub.called, 'command.error should be called for invalid file').to.equal(true);
expect(caught).to.exist;

const stdoutOutput = stdoutStub
.getCalls()
.map((c) => String(c.args[0] ?? ''))
.join('\n');
expect(stdoutOutput).to.include('FAIL');
// The real validator surfaces the missing required property
expect(stdoutOutput).to.include('region_definitions');
});

it('reports invalid JSON content with a JSON parse error', async () => {
const filePath = path.join(tmpDir, 'bad-json.json');
fs.writeFileSync(filePath, '{not valid json');

const command: any = await createCommand({type: 'pagetype'}, [filePath]);
sinon.stub(command, 'jsonEnabled').returns(false);
const stdoutStub = sinon.stub(ux, 'stdout');
const errorStub = sinon.stub(command, 'error').throws(new Error('Validation failed'));

let caught: unknown;
try {
await command.run();
} catch (error) {
caught = error;
}

expect(errorStub.called).to.equal(true);
expect(caught).to.exist;

const stdoutOutput = stdoutStub
.getCalls()
.map((c) => String(c.args[0] ?? ''))
.join('\n');
expect(stdoutOutput).to.include('FAIL');
expect(stdoutOutput.toLowerCase()).to.include('invalid json');
});
});
});
3 changes: 2 additions & 1 deletion packages/b2c-cli/test/commands/job/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('job run', () => {
sinon.stub(command, 'runAfterHooks').resolves(void 0);
const execStub = sinon.stub().resolves({id: 'e1', execution_status: 'running'});
command.operations = {...command.operations, executeJob: execStub};
sinon.stub(command, 'showJobLog').resolves(void 0);
const showJobLogStub = sinon.stub(command, 'showJobLog').resolves(void 0);

const exec: any = {execution_status: 'finished', exit_status: {code: 'ERROR'}};
const {JobExecutionError} = await import('@salesforce/b2c-tooling-sdk/operations/jobs');
Expand All @@ -143,6 +143,7 @@ describe('job run', () => {
// expected
}

expect(showJobLogStub.calledOnce).to.equal(true);
expect(errorStub.called).to.equal(true);
});
});
63 changes: 54 additions & 9 deletions packages/b2c-cli/test/commands/scapi/custom/status.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import {expect} from 'chai';
import sinon from 'sinon';
import {Config} from '@oclif/core';
import {Config, ux} from '@oclif/core';
import ScapiCustomStatus from '../../../../src/commands/scapi/custom/status.js';
import {isolateConfig, restoreConfig} from '@salesforce/b2c-tooling-sdk/test-utils';
import {stubParse} from '../../../helpers/stub-parse.js';
Expand Down Expand Up @@ -114,32 +114,77 @@ describe('scapi custom status', () => {
expect(fetchStub.called).to.equal(true);
});

it('does not block in non-JSON mode (renderEndpoints is stubbed)', async () => {
it('renders endpoints to stdout in non-JSON mode', async () => {
const command: any = new ScapiCustomStatus([], config);

stubParse(command, {'tenant-id': 'zzxy_prd'}, {});
await command.init();

sinon.stub(command, 'requireOAuthCredentials').returns(void 0);
sinon.stub(command, 'jsonEnabled').returns(false);
sinon.stub(command, 'log').returns(void 0);
sinon.stub(command, 'resolvedConfig').get(() => ({values: {shortCode: 'kv7kzm78', tenantId: 'zzxy_prd'}}));

sinon.stub(command, 'renderEndpoints').returns(void 0);
const logStub = sinon.stub(command, 'log');
const stdoutStub = sinon.stub(ux, 'stdout');

sinon.stub(command, 'getOAuthStrategy').returns({
getAuthorizationHeader: async () => 'Bearer test',
});

const fetchStub = sinon.stub(globalThis, 'fetch').resolves(
new Response(JSON.stringify({total: 1, data: []}), {
status: 200,
headers: {'content-type': 'application/json'},
}),
new Response(
JSON.stringify({
total: 2,
activeCodeVersion: 'version1',
data: [
{
apiName: 'OrdersApi',
apiVersion: 'v1',
cartridgeName: 'app_custom',
endpointPath: '/orders',
httpMethod: 'get',
status: 'active',
securityScheme: 'AmOAuth2',
siteId: 'RefArch',
},
{
apiName: 'ProductsApi',
apiVersion: 'v2',
cartridgeName: 'app_custom',
endpointPath: '/products',
httpMethod: 'post',
status: 'not_registered',
securityScheme: 'ShopperToken',
siteId: 'RefArchGlobal',
},
],
}),
{status: 200, headers: {'content-type': 'application/json'}},
),
);

const result = await command.run();
expect(fetchStub.called).to.equal(true);
expect(result.total).to.equal(1);
expect(result.total).to.equal(2);

const logOutput = logStub
.getCalls()
.map((c) => String(c.args[0] ?? ''))
.join('\n');
const stdoutOutput = stdoutStub
.getCalls()
.map((c) => String(c.args[0] ?? ''))
.join('\n');
const allOutput = `${logOutput}\n${stdoutOutput}`;

// Header info logged via command.log
expect(logOutput).to.include('version1');
expect(logOutput).to.match(/Found\s+2/);

// Table content rendered via ux.stdout (TableRenderer)
expect(allOutput).to.include('/orders');
expect(allOutput).to.include('/products');
expect(allOutput).to.include('active');
expect(allOutput).to.include('not_registered');
});
});
31 changes: 25 additions & 6 deletions packages/b2c-cli/test/commands/scapi/replications/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
*/
import {expect} from 'chai';
import sinon from 'sinon';
import {Config} from '@oclif/core';
import {Config, ux} from '@oclif/core';
import ReplicationsList from '../../../../src/commands/scapi/replications/list.js';
import {stubParse} from '../../../helpers/stub-parse.js';
import {createIsolatedEnvHooks, runSilent} from '../../../helpers/test-setup.js';
import {createIsolatedEnvHooks} from '../../../helpers/test-setup.js';

describe('scapi replications list', () => {
const hooks = createIsolatedEnvHooks();
Expand Down Expand Up @@ -80,26 +80,45 @@ describe('scapi replications list', () => {
sinon.stub(command, 'resolvedConfig').get(() => ({values: {shortCode: 'kv7kzm78', tenantId: 'zzxy_prd'}}));
sinon.stub(command, 'getOAuthStrategy').returns({getAuthorizationHeader: async () => 'Bearer test'});

const stdoutStub = sinon.stub(ux, 'stdout');

sinon.stub(globalThis, 'fetch').resolves(
new Response(
JSON.stringify({
total: 1,
total: 2,
data: [
{
id: 'proc-1',
id: 'proc-abc',
status: 'completed',
startTime: '2025-01-01T00:00:00Z',
initiatedBy: 'user@example.com',
productItem: {productId: 'PROD-1'},
},
{
id: 'proc-xyz',
status: 'in_progress',
startTime: '2025-01-01T01:00:00Z',
initiatedBy: 'user@example.com',
priceTableItem: {priceTableId: 'table-1'},
},
],
}),
{status: 200, headers: {'content-type': 'application/json'}},
),
);

const result = (await runSilent(() => command.run())) as {total: number};
expect(result.total).to.equal(1);
const result = (await command.run()) as {total: number};
expect(result.total).to.equal(2);

const stdoutOutput = stdoutStub
.getCalls()
.map((c) => String(c.args[0] ?? ''))
.join('');
expect(stdoutOutput).to.include('proc-abc');
expect(stdoutOutput).to.include('proc-xyz');
expect(stdoutOutput).to.include('completed');
expect(stdoutOutput).to.include('in_progress');
expect(stdoutOutput).to.match(/Total:\s*2/);
});

it('handles empty result set', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ describe('scapi replications publish', () => {
expect.fail('Should have thrown');
} catch {
expect(errorStub.calledOnce).to.equal(true);
const message = String(errorStub.firstCall.args[0]);
// 422 surfaces the staging-instance detail returned by the API
expect(message).to.include('staging instances');
}
});

Expand Down Expand Up @@ -236,6 +239,9 @@ describe('scapi replications publish', () => {
expect.fail('Should have thrown');
} catch {
expect(errorStub.calledOnce).to.equal(true);
const message = String(errorStub.firstCall.args[0]);
// 409 surfaces the full-replication-running detail
expect(message).to.include('full replication is running');
}
});
});
Expand Down
37 changes: 29 additions & 8 deletions packages/b2c-cli/test/commands/scapi/replications/wait.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import sinon from 'sinon';
import {Config} from '@oclif/core';
import ReplicationsWait from '../../../../src/commands/scapi/replications/wait.js';
import {stubParse} from '../../../helpers/stub-parse.js';
import {createIsolatedEnvHooks, runSilent} from '../../../helpers/test-setup.js';
import {createIsolatedEnvHooks} from '../../../helpers/test-setup.js';

describe('scapi replications wait', () => {
const hooks = createIsolatedEnvHooks();
Expand Down Expand Up @@ -100,7 +100,7 @@ describe('scapi replications wait', () => {
expect(result.status).to.equal('failed');
});

it('logs status updates in non-JSON mode', async () => {
it('logs status transitions in non-JSON mode', async () => {
const command: any = new ReplicationsWait([], config);
stubParse(command, {'tenant-id': 'zzxy_prd', timeout: 10, interval: 0}, {'process-id': 'proc-789'});
await command.init();
Expand All @@ -111,8 +111,22 @@ describe('scapi replications wait', () => {
sinon.stub(command, 'resolvedConfig').get(() => ({values: {shortCode: 'kv7kzm78', tenantId: 'zzxy_prd'}}));
sinon.stub(command, 'getOAuthStrategy').returns({getAuthorizationHeader: async () => 'Bearer test'});

sinon.stub(globalThis, 'fetch').resolves(
new Response(
let callCount = 0;
sinon.stub(globalThis, 'fetch').callsFake(async () => {
callCount++;
if (callCount === 1) {
return new Response(
JSON.stringify({
id: 'proc-789',
status: 'in_progress',
startTime: '2025-01-01T02:00:00Z',
initiatedBy: 'user@example.com',
productItem: {productId: 'PROD-3'},
}),
{status: 200, headers: {'content-type': 'application/json'}},
);
}
return new Response(
JSON.stringify({
id: 'proc-789',
status: 'completed',
Expand All @@ -122,11 +136,18 @@ describe('scapi replications wait', () => {
productItem: {productId: 'PROD-3'},
}),
{status: 200, headers: {'content-type': 'application/json'}},
),
);
);
});

await command.run();

await runSilent(() => command.run());
expect(logStub.called).to.equal(true);
const logMessages = logStub.getCalls().map((c) => String(c.args[0] ?? ''));
const allLogs = logMessages.join('\n');
// Should have logged the transition through both statuses
expect(allLogs).to.include('in_progress');
expect(allLogs).to.include('completed');
// And the explicit "Process completed successfully" final message
expect(logMessages.some((m) => /completed successfully/i.test(m))).to.equal(true);
});

it('times out if process does not complete', async () => {
Expand Down
Loading
Loading