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
29 changes: 27 additions & 2 deletions packages/b2c-cli/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import headerPlugin from 'eslint-plugin-header';
import path from 'node:path';
import {fileURLToPath} from 'node:url';

import {copyrightHeader, sharedRules, oclifRules, prettierPlugin} from '../../eslint.config.mjs';
import {copyrightHeader, sharedRules, oclifRules, chaiTestRules, prettierPlugin} from '../../eslint.config.mjs';

const gitignorePath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '.gitignore');
headerPlugin.rules.header.meta.schema = false;
Expand All @@ -19,7 +19,7 @@ export default [
// node_modules must be explicitly ignored because the .gitignore pattern only covers
// packages/b2c-cli/node_modules, not the monorepo root node_modules
{
ignores: ['**/node_modules/**', 'test/functional/fixtures/**/*.js'],
ignores: ['**/node_modules/**', 'test/functional/fixtures/**/*.js', '**/node_modules/marked-terminal/**'],
},
includeIgnoreFile(gitignorePath),
...oclif,
Expand All @@ -39,4 +39,29 @@ export default [
...oclifRules,
},
},
{
files: ['test/**/*.ts'],
rules: {
...chaiTestRules,
// Tests use stubbing patterns that intentionally return undefined
'unicorn/no-useless-undefined': 'off',
// Some tests use void 0 to satisfy TS stub typings; allow it in tests
'no-void': 'off',
// Command tests frequently use `any` to avoid over-typing oclif command internals
'@typescript-eslint/no-explicit-any': 'off',
// Helper functions in tests are commonly declared within suites for clarity
'unicorn/consistent-function-scoping': 'off',
// Sinon default import is intentional and idiomatic in tests
'import/no-named-as-default-member': 'off',
// import/namespace behaves inconsistently across environments when parsing CJS modules like marked-terminal
'import/namespace': 'off',
},
},
{
files: ['src/commands/docs/**/*.ts'],
rules: {
// marked-terminal is CJS and breaks import/namespace static analysis
'import/namespace': 'off',
},
},
];
1 change: 1 addition & 0 deletions packages/b2c-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"oclif": "^4",
"prettier": "^3.6.2",
"shx": "^0.3.3",
"sinon": "^21.0.1",
"tsx": "^4.20.6",
"typescript": "^5"
},
Expand Down
6 changes: 5 additions & 1 deletion packages/b2c-cli/src/commands/code/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ export default class CodeDelete extends InstanceCommand<typeof CodeDelete> {
}),
};

protected async confirm(message: string): Promise<boolean> {
return confirm(message);
}

async run(): Promise<void> {
this.requireOAuthCredentials();

Expand All @@ -59,7 +63,7 @@ export default class CodeDelete extends InstanceCommand<typeof CodeDelete> {

// Confirm deletion unless --force is used
if (!this.flags.force) {
const confirmed = await confirm(
const confirmed = await this.confirm(
t(
'commands.code.delete.confirm',
'Are you sure you want to delete code version "{{codeVersion}}" on {{hostname}}? (y/n)',
Expand Down
24 changes: 20 additions & 4 deletions packages/b2c-cli/src/commands/code/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ export default class CodeDeploy extends CartridgeCommand<typeof CodeDeploy> {
}),
};

protected async deleteCartridges(cartridges: Parameters<typeof deleteCartridges>[1]) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this might be to mock the underlying SDK with sinon. But it's adding quite a bit of lines and noise. Can we try simplifying the dependency injection pattern here. Maybe we can support the run method with default arguments set to the imported SDK methods? So the tests can pass in their mocks.

  async run(
    // we'll have to disable this eslint globally but that's ok
    // eslint-disable-next-line unicorn/no-object-as-default-parameter
    operations = {
      uploadCartridges,
      deleteCartridges,
      getActiveCodeVersion,
      reloadCodeVersion,
    },
  ): Promise<DeployResult> {
    const {uploadCartridges, deleteCartridges, getActiveCodeVersion, reloadCodeVersion} = operations;

That way only the run method needs to change and the rest of the code stays the same.

Or maybe using a protected member with the operations we want to mock and then mock this in the tests?

   protected operations = {
     uploadCartridges,
     deleteCartridges,
     getActiveCodeVersion,
     reloadCodeVersion,
   };

//

await operations.uploadCartridges

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, Thanks Charles! I’ll look at simplifying the dependency injection pattern for these commands and see how we can reduce the test scaffolding.

return deleteCartridges(this.instance, cartridges);
}

protected async getActiveCodeVersion() {
return getActiveCodeVersion(this.instance);
}

protected async reloadCodeVersion(codeVersion: string) {
return reloadCodeVersion(this.instance, codeVersion);
}

async run(): Promise<DeployResult> {
this.requireWebDavCredentials();
this.requireOAuthCredentials();
Expand All @@ -59,7 +71,7 @@ export default class CodeDeploy extends CartridgeCommand<typeof CodeDeploy> {
this.warn(
t('commands.code.deploy.noCodeVersion', 'No code version specified, discovering active code version...'),
);
const activeVersion = await getActiveCodeVersion(this.instance);
const activeVersion = await this.getActiveCodeVersion();
if (!activeVersion?.id) {
this.error(
t('commands.code.deploy.noActiveVersion', 'No active code version found. Specify one with --code-version.'),
Expand Down Expand Up @@ -119,17 +131,17 @@ export default class CodeDeploy extends CartridgeCommand<typeof CodeDeploy> {
try {
// Optionally delete existing cartridges first
if (this.flags.delete) {
await deleteCartridges(this.instance, cartridges);
await this.deleteCartridges(cartridges);
}

// Upload cartridges
await uploadCartridges(this.instance, cartridges);
await this.uploadCartridges(cartridges);

// Optionally reload code version
let reloaded = false;
if (this.flags.reload) {
try {
await reloadCodeVersion(this.instance, version);
await this.reloadCodeVersion(version);
reloaded = true;
} catch (error) {
this.logger?.debug(`Could not reload code version: ${error instanceof Error ? error.message : error}`);
Expand Down Expand Up @@ -176,4 +188,8 @@ export default class CodeDeploy extends CartridgeCommand<typeof CodeDeploy> {
throw error;
}
}

protected async uploadCartridges(cartridges: Parameters<typeof uploadCartridges>[1]) {
return uploadCartridges(this.instance, cartridges);
}
}
28 changes: 16 additions & 12 deletions packages/b2c-cli/src/commands/code/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,7 @@ export default class CodeWatch extends CartridgeCommand<typeof CodeWatch> {
}

try {
const result = await watchCartridges(this.instance, this.cartridgePath, {
...this.cartridgeOptions,
onUpload: (files) => {
this.log(t('commands.code.watch.uploaded', '[UPLOAD] {{count}} file(s)', {count: files.length}));
},
onDelete: (files) => {
this.log(t('commands.code.watch.deleted', '[DELETE] {{count}} file(s)', {count: files.length}));
},
onError: (error) => {
this.warn(t('commands.code.watch.error', 'Error: {{message}}', {message: error.message}));
},
});
const result = await this.watchCartridges();

this.log(
t('commands.code.watch.watching', 'Watching {{count}} cartridge(s)...', {count: result.cartridges.length}),
Expand All @@ -78,4 +67,19 @@ export default class CodeWatch extends CartridgeCommand<typeof CodeWatch> {
throw error;
}
}

protected async watchCartridges() {
return watchCartridges(this.instance, this.cartridgePath, {
...this.cartridgeOptions,
onUpload: (files) => {
this.log(t('commands.code.watch.uploaded', '[UPLOAD] {{count}} file(s)', {count: files.length}));
},
onDelete: (files) => {
this.log(t('commands.code.watch.deleted', '[DELETE] {{count}} file(s)', {count: files.length}));
},
onError: (error) => {
this.warn(t('commands.code.watch.error', 'Error: {{message}}', {message: error.message}));
},
});
}
}
6 changes: 5 additions & 1 deletion packages/b2c-cli/src/commands/docs/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export default class DocsDownload extends InstanceCommand<typeof DocsDownload> {
}),
};

protected async downloadDocs(input: Parameters<typeof downloadDocs>[1]) {
return downloadDocs(this.instance, input);
}

async run(): Promise<DownloadDocsResult> {
this.requireServer();
this.requireWebDavCredentials();
Expand All @@ -50,7 +54,7 @@ export default class DocsDownload extends InstanceCommand<typeof DocsDownload> {
}),
);

const result = await downloadDocs(this.instance, {
const result = await this.downloadDocs({
outputDir,
keepArchive,
});
Expand Down
8 changes: 6 additions & 2 deletions packages/b2c-cli/src/commands/docs/read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/
import {Args, Flags} from '@oclif/core';
import {marked} from 'marked';
// eslint-disable-next-line import/namespace

import {markedTerminal} from 'marked-terminal';
import {BaseCommand} from '@salesforce/b2c-tooling-sdk/cli';
import {readDocByQuery, type DocEntry} from '@salesforce/b2c-tooling-sdk/operations/docs';
Expand Down Expand Up @@ -57,11 +57,15 @@ export default class DocsRead extends BaseCommand<typeof DocsRead> {
}),
};

protected readDocByQuery(query: string) {
return readDocByQuery(query);
}

async run(): Promise<ReadDocsResult> {
const {query} = this.args;
const {raw} = this.flags;

const result = readDocByQuery(query);
const result = this.readDocByQuery(query);

if (!result) {
this.error(t('commands.docs.read.notFound', 'No documentation found matching: {{query}}', {query}), {
Expand Down
12 changes: 10 additions & 2 deletions packages/b2c-cli/src/commands/docs/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,21 @@ export default class DocsSchema extends BaseCommand<typeof DocsSchema> {
}),
};

protected listSchemas() {
return listSchemas();
}

protected readSchemaByQuery(query: string) {
return readSchemaByQuery(query);
}

async run(): Promise<ListResult | SchemaResult> {
const {query} = this.args;
const {list} = this.flags;

// List mode
if (list) {
const entries = listSchemas();
const entries = this.listSchemas();

if (this.jsonEnabled()) {
return {entries};
Expand All @@ -72,7 +80,7 @@ export default class DocsSchema extends BaseCommand<typeof DocsSchema> {
this.error(t('commands.docs.schema.queryRequired', 'Schema name is required. Use --list to see all schemas.'));
}

const result = readSchemaByQuery(query);
const result = this.readSchemaByQuery(query);

if (!result) {
this.error(t('commands.docs.schema.notFound', 'No schema found matching: {{query}}', {query}), {
Expand Down
12 changes: 10 additions & 2 deletions packages/b2c-cli/src/commands/docs/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,17 @@ export default class DocsSearch extends BaseCommand<typeof DocsSearch> {
}),
};

protected listDocs() {
return listDocs();
}

async run(): Promise<ListDocsResponse | SearchDocsResponse> {
const {query} = this.args;
const {limit, list} = this.flags;

// List mode
if (list) {
const entries = listDocs();
const entries = this.listDocs();

if (this.jsonEnabled()) {
return {entries};
Expand Down Expand Up @@ -109,7 +113,7 @@ export default class DocsSearch extends BaseCommand<typeof DocsSearch> {
);
}

const results = searchDocs(query, limit);
const results = this.searchDocs(query, limit);

const response: SearchDocsResponse = {
query,
Expand All @@ -136,4 +140,8 @@ export default class DocsSearch extends BaseCommand<typeof DocsSearch> {

return response;
}

protected searchDocs(query: string, limit: number) {
return searchDocs(query, limit);
}
}
12 changes: 10 additions & 2 deletions packages/b2c-cli/src/commands/job/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default class JobExport extends JobCommand<typeof JobExport> {
'no-download': noDownload,
'zip-only': zipOnly,
timeout,
'show-log': showLog,
'show-log': showLog = true,
} = this.flags;

const hostname = this.resolvedConfig.values.hostname!;
Expand Down Expand Up @@ -173,7 +173,7 @@ export default class JobExport extends JobCommand<typeof JobExport> {
this.log(t('commands.job.export.dataUnits', 'Data units: {{dataUnits}}', {dataUnits: JSON.stringify(dataUnits)}));

try {
const result = await siteArchiveExportToPath(this.instance, dataUnits, output, {
const result = await this.siteArchiveExportToPath(dataUnits, output, {
keepArchive: keepArchive || noDownload,
extractZip: !zipOnly,
waitOptions: {
Expand Down Expand Up @@ -254,6 +254,14 @@ export default class JobExport extends JobCommand<typeof JobExport> {
}
}

protected async siteArchiveExportToPath(
dataUnits: Parameters<typeof siteArchiveExportToPath>[1],
output: Parameters<typeof siteArchiveExportToPath>[2],
options: Parameters<typeof siteArchiveExportToPath>[3],
) {
return siteArchiveExportToPath(this.instance, dataUnits, output, options);
}

private buildDataUnits(params: {
dataUnitsJson?: string;
site?: string[];
Expand Down
11 changes: 9 additions & 2 deletions packages/b2c-cli/src/commands/job/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default class JobImport extends JobCommand<typeof JobImport> {
this.requireWebDavCredentials();

const {target} = this.args;
const {'keep-archive': keepArchive, remote, timeout, 'show-log': showLog} = this.flags;
const {'keep-archive': keepArchive, remote, timeout, 'show-log': showLog = true} = this.flags;

const hostname = this.resolvedConfig.values.hostname!;

Expand Down Expand Up @@ -107,7 +107,7 @@ export default class JobImport extends JobCommand<typeof JobImport> {
try {
const importTarget = remote ? {remoteFilename: target} : target;

const result = await siteArchiveImport(this.instance, importTarget, {
const result = await this.siteArchiveImport(importTarget, {
keepArchive,
waitOptions: {
timeout: timeout ? timeout * 1000 : undefined,
Expand Down Expand Up @@ -178,4 +178,11 @@ export default class JobImport extends JobCommand<typeof JobImport> {
throw error;
}
}

protected async siteArchiveImport(
target: Parameters<typeof siteArchiveImport>[1],
options: Parameters<typeof siteArchiveImport>[2],
) {
return siteArchiveImport(this.instance, target, options);
}
}
12 changes: 10 additions & 2 deletions packages/b2c-cli/src/commands/job/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export default class JobRun extends JobCommand<typeof JobRun> {
}),
};

protected async executeJob(jobId: string, options: Parameters<typeof executeJob>[2]) {
return executeJob(this.instance, jobId, options);
}

async run(): Promise<JobExecution> {
this.requireOAuthCredentials();

Expand Down Expand Up @@ -106,7 +110,7 @@ export default class JobRun extends JobCommand<typeof JobRun> {

let execution: JobExecution;
try {
execution = await executeJob(this.instance, jobId, {
execution = await this.executeJob(jobId, {
parameters: rawBody ? undefined : parameters,
body: rawBody,
waitForRunning: !noWaitRunning,
Expand Down Expand Up @@ -143,6 +147,10 @@ export default class JobRun extends JobCommand<typeof JobRun> {
return execution;
}

protected async waitForJob(jobId: string, executionId: string, options: Parameters<typeof waitForJob>[3]) {
return waitForJob(this.instance, jobId, executionId, options);
}

private handleExecutionError(error: unknown, context: B2COperationContext): never {
// Run afterOperation hooks with failure (fire-and-forget, errors ignored)
this.runAfterHooks(context, {
Expand Down Expand Up @@ -213,7 +221,7 @@ export default class JobRun extends JobCommand<typeof JobRun> {
this.log(t('commands.job.run.waiting', 'Waiting for job to complete...'));

try {
const execution = await waitForJob(this.instance, jobId, executionId, {
const execution = await this.waitForJob(jobId, executionId, {
timeout: timeout ? timeout * 1000 : undefined,
onProgress: (exec, elapsed) => {
if (!this.jsonEnabled()) {
Expand Down
Loading