Skip to content
Open
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
4 changes: 2 additions & 2 deletions packages/contentstack-audit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
"version": "oclif readme && git add README.md",
"clean": "rm -rf ./lib ./node_modules tsconfig.tsbuildinfo oclif.manifest.json",
"test:unit:report": "nyc --extension .ts mocha --forbid-only \"test/unit/**/*.test.ts\"",
"test:unit": "mocha --timeout 10000 --forbid-only \"test/unit/**/*.test.ts\""
"test:unit:report": "nyc --extension .ts mocha --forbid-only --file test/unit/logger-config.js \"test/unit/**/*.test.ts\"",
"test:unit": "mocha --timeout 10000 --forbid-only --file test/unit/logger-config.js \"test/unit/**/*.test.ts\""
},
"engines": {
"node": ">=16"
Expand Down
22 changes: 16 additions & 6 deletions packages/contentstack-audit/src/modules/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default class Assets extends BaseClass {
protected schema: ContentTypeStruct[] = [];
protected missingEnvLocales: Record<string, any> = {};
public moduleName: keyof typeof auditConfig.moduleConfig;
private fixOverwriteConfirmed: boolean | null = null;

constructor({ fix, config, moduleName }: ModuleConstructorParam & CtConstructorParam) {
super({ config });
Expand Down Expand Up @@ -161,11 +162,17 @@ export default class Assets extends BaseClass {

if (this.fix) {
log.debug('Fix mode enabled, checking write permissions', this.config.auditContext);
if (!this.config.flags['copy-dir'] && !this.config.flags['external-config']?.skipConfirm) {
log.debug(`Asking user for confirmation to write fix content (--yes flag: ${this.config.flags.yes})`, this.config.auditContext);
canWrite = this.config.flags.yes || (await cliux.confirm(commonMsg.FIX_CONFIRMATION));
if (this.config.flags['copy-dir'] || this.config.flags['external-config']?.skipConfirm || this.config.flags.yes) {
this.fixOverwriteConfirmed = true;
log.debug('Skipping confirmation due to copy-dir, external-config, or yes flags', this.config.auditContext);
} else if (this.fixOverwriteConfirmed !== null) {
canWrite = this.fixOverwriteConfirmed;
log.debug(`Using cached overwrite confirmation: ${canWrite}`, this.config.auditContext);
} else {
log.debug('Skipping confirmation due to copy-dir or external-config flags', this.config.auditContext);
log.debug(`Asking user for confirmation to write fix content (--yes flag: ${this.config.flags.yes})`, this.config.auditContext);
this.completeProgress(true);
canWrite = await cliux.confirm(commonMsg.FIX_CONFIRMATION);
this.fixOverwriteConfirmed = canWrite;
}

if (canWrite) {
Expand Down Expand Up @@ -248,13 +255,16 @@ export default class Assets extends BaseClass {
if (this.progressManager) {
this.progressManager.tick(true, `asset: ${assetUid}`, null);
}

if (this.fix) {
log.debug(`Fixing asset ${assetUid}`, this.config.auditContext);
log.info($t(auditFixMsg.ASSET_FIX, { uid: assetUid }), this.config.auditContext);
await this.writeFixContent(`${basePath}/${indexer[fileIndex]}`, this.assets);
}
}

if (this.fix) {
await this.writeFixContent(`${basePath}/${indexer[fileIndex]}`, this.assets);
}
}

log.debug(`Asset reference validation completed. Processed ${Object.keys(this.missingEnvLocales).length} assets with issues`, this.config.auditContext);
Expand Down
1 change: 1 addition & 0 deletions packages/contentstack-audit/src/modules/content-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export default class ContentType extends BaseClass {
log.debug('Fix mode enabled, checking write permissions', this.config.auditContext);
if (!this.config.flags['copy-dir'] && !this.config.flags['external-config']?.skipConfirm) {
log.debug('Asking user for confirmation to write fix content', this.config.auditContext);
this.completeProgress(true);
canWrite = this.config.flags.yes ?? (await cliux.confirm(commonMsg.FIX_CONFIRMATION));
} else {
log.debug('Skipping confirmation due to copy-dir or external-config flags', this.config.auditContext);
Expand Down
1 change: 1 addition & 0 deletions packages/contentstack-audit/src/modules/custom-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ export default class CustomRoles extends BaseClass {
log.debug('Skipping confirmation due to copy-dir, external-config, or yes flags', this.config.auditContext);
} else {
log.debug('Asking user for confirmation to write fix content', this.config.auditContext);
this.completeProgress(true);
}

const canWrite = skipConfirm || (await cliux.confirm(commonMsg.FIX_CONFIRMATION));
Expand Down
16 changes: 12 additions & 4 deletions packages/contentstack-audit/src/modules/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default class Entries extends BaseClass {
public environments: string[] = [];
public entryMetaData: Record<string, any>[] = [];
public moduleName: keyof typeof auditConfig.moduleConfig = 'entries';
private fixOverwriteConfirmed: boolean | null = null;

constructor({ fix, config, moduleName, ctSchema, gfSchema }: ModuleConstructorParam & CtConstructorParam) {
super({ config });
Expand Down Expand Up @@ -541,14 +542,21 @@ export default class Entries extends BaseClass {

const skipConfirm = this.config.flags['copy-dir'] || this.config.flags['external-config']?.skipConfirm;

if (skipConfirm) {
log.debug('Skipping confirmation due to copy-dir or external-config flags', this.config.auditContext);
let canWrite: boolean;
if (skipConfirm || this.config.flags.yes) {
canWrite = true;
this.fixOverwriteConfirmed = true;
log.debug('Skipping confirmation due to copy-dir, external-config, or yes flags', this.config.auditContext);
} else if (this.fixOverwriteConfirmed !== null) {
canWrite = this.fixOverwriteConfirmed;
log.debug(`Using cached overwrite confirmation: ${canWrite}`, this.config.auditContext);
} else {
log.debug('Asking user for confirmation to write fix content', this.config.auditContext);
this.completeProgress(true);
canWrite = await cliux.confirm(commonMsg.FIX_CONFIRMATION);
this.fixOverwriteConfirmed = canWrite;
}

const canWrite = skipConfirm || this.config.flags.yes || (await cliux.confirm(commonMsg.FIX_CONFIRMATION));

if (canWrite) {
log.debug(`Writing fixed entries to: ${filePath}`, this.config.auditContext);
writeFileSync(filePath, JSON.stringify(schema));
Expand Down
45 changes: 34 additions & 11 deletions packages/contentstack-audit/src/modules/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,19 @@ export default class Extensions extends BaseClass {
? JSON.parse(readFileSync(this.extensionsPath, 'utf8'))
: {};
log.debug(`Loaded ${Object.keys(newExtensionSchema).length} existing extensions`, this.config.auditContext);


let userConfirm: boolean;
if (
this.config.flags['copy-dir'] ||
this.config.flags['external-config']?.skipConfirm ||
this.config.flags.yes
) {
userConfirm = true;
} else {
this.completeProgress(true);
userConfirm = await cliux.confirm(commonMsg.FIX_CONFIRMATION);
}

for (const ext of missingCtInExtensions) {
const { uid, title } = ext;
log.debug(`Fixing extension: ${title} (${uid})`, this.config.auditContext);
Expand All @@ -187,8 +199,7 @@ export default class Extensions extends BaseClass {
} else {
log.debug(`Extension ${title} has no valid content types or scope not found`, this.config.auditContext);
cliux.print($t(commonMsg.EXTENSION_FIX_WARN, { title: title, uid }), { color: 'yellow' });
const shouldDelete = this.config.flags.yes || (await cliux.confirm(commonMsg.EXTENSION_FIX_CONFIRMATION));
if (shouldDelete) {
if (userConfirm) {
log.debug(`Deleting extension: ${title} (${uid})`, this.config.auditContext);
delete newExtensionSchema[uid];
} else {
Expand All @@ -198,23 +209,35 @@ export default class Extensions extends BaseClass {
}

log.debug(`Extensions scope fix completed, writing updated schema`, this.config.auditContext);
await this.writeFixContent(newExtensionSchema);
await this.writeFixContent(newExtensionSchema, userConfirm);
}

async writeFixContent(fixedExtensions: Record<string, Extension>) {
async writeFixContent(fixedExtensions: Record<string, Extension>, preConfirmed?: boolean) {
log.debug(`Writing fix content for ${Object.keys(fixedExtensions).length} extensions`, this.config.auditContext);
log.debug(`Fix mode: ${this.fix}`, this.config.auditContext);
log.debug(`Copy directory flag: ${this.config.flags['copy-dir']}`, this.config.auditContext);
log.debug(`External config skip confirm: ${this.config.flags['external-config']?.skipConfirm}`, this.config.auditContext);
log.debug(`Yes flag: ${this.config.flags.yes}`, this.config.auditContext);

if (
this.fix &&
(this.config.flags['copy-dir'] ||
this.config.flags['external-config']?.skipConfirm ||
this.config.flags.yes ||
(await cliux.confirm(commonMsg.FIX_CONFIRMATION)))
let shouldWrite: boolean;
if (!this.fix) {
shouldWrite = false;
} else if (preConfirmed === true) {
shouldWrite = true;
} else if (preConfirmed === false) {
shouldWrite = false;
} else if (
this.config.flags['copy-dir'] ||
this.config.flags['external-config']?.skipConfirm ||
this.config.flags.yes
) {
shouldWrite = true;
} else {
this.completeProgress(true);
shouldWrite = await cliux.confirm(commonMsg.FIX_CONFIRMATION);
}

if (shouldWrite) {
const outputPath = join(this.folderPath, this.config.moduleConfig[this.moduleName].fileName);
log.debug(`Writing fixed extensions to: ${outputPath}`, this.config.auditContext);
log.debug(`Extensions to write: ${Object.keys(fixedExtensions).join(', ')}`, this.config.auditContext);
Expand Down
1 change: 1 addition & 0 deletions packages/contentstack-audit/src/modules/field_rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ export default class FieldRule extends BaseClass {
if (this.fix) {
if (!this.config.flags['copy-dir'] && !this.config.flags['external-config']?.skipConfirm) {
log.debug(`Asking user for confirmation to write fix content`, this.config.auditContext);
this.completeProgress(true);
canWrite = this.config.flags.yes ?? (await cliux.confirm(commonMsg.FIX_CONFIRMATION));
log.debug(`User confirmation: ${canWrite}`, this.config.auditContext);
} else {
Expand Down
47 changes: 37 additions & 10 deletions packages/contentstack-audit/src/modules/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,22 @@ export default class Workflows extends BaseClass {

log.debug(`Loaded ${Object.keys(newWorkflowSchema).length} workflows for fixing`, this.config.auditContext);

if (Object.keys(newWorkflowSchema).length !== 0) {
const hasWorkflowsToFix = Object.keys(newWorkflowSchema).length !== 0;
let userConfirm: boolean;
if (!hasWorkflowsToFix) {
userConfirm = true;
} else if (
this.config.flags['copy-dir'] ||
this.config.flags['external-config']?.skipConfirm ||
this.config.flags.yes
) {
userConfirm = true;
} else {
this.completeProgress(true);
userConfirm = await cliux.confirm(commonMsg.FIX_CONFIRMATION);
}

if (hasWorkflowsToFix) {
log.debug(`Processing ${this.workflowSchema.length} workflows for fixes`, this.config.auditContext);

for (const workflow of this.workflowSchema) {
Expand Down Expand Up @@ -237,7 +252,7 @@ export default class Workflows extends BaseClass {

cliux.print(warningMessage, { color: 'yellow' });

if (this.config.flags.yes || (await cliux.confirm(commonMsg.WORKFLOW_FIX_CONFIRMATION))) {
if (userConfirm) {
log.debug(`Deleting workflow ${name} (${uid})`, this.config.auditContext);
delete newWorkflowSchema[workflow.uid];
} else {
Expand All @@ -250,24 +265,36 @@ export default class Workflows extends BaseClass {
}

log.debug(`Workflow schema fix completed`, this.config.auditContext);
await this.writeFixContent(newWorkflowSchema);
await this.writeFixContent(newWorkflowSchema, userConfirm);
}

async writeFixContent(newWorkflowSchema: Record<string, Workflow>) {
async writeFixContent(newWorkflowSchema: Record<string, Workflow>, preConfirmed?: boolean) {
log.debug(`Writing fix content`, this.config.auditContext);
log.debug(`Fix mode: ${this.fix}`, this.config.auditContext);
log.debug(`Copy directory flag: ${this.config.flags['copy-dir']}`, this.config.auditContext);
log.debug(`External config skip confirm: ${this.config.flags['external-config']?.skipConfirm}`, this.config.auditContext);
log.debug(`Yes flag: ${this.config.flags.yes}`, this.config.auditContext);
log.debug(`Workflows to write: ${Object.keys(newWorkflowSchema).length}`, this.config.auditContext);

if (
this.fix &&
(this.config.flags['copy-dir'] ||
this.config.flags['external-config']?.skipConfirm ||
this.config.flags.yes ||
(await cliux.confirm(commonMsg.FIX_CONFIRMATION)))
let shouldWrite: boolean;
if (!this.fix) {
shouldWrite = false;
} else if (preConfirmed === true) {
shouldWrite = true;
} else if (preConfirmed === false) {
shouldWrite = false;
} else if (
this.config.flags['copy-dir'] ||
this.config.flags['external-config']?.skipConfirm ||
this.config.flags.yes
) {
shouldWrite = true;
} else {
this.completeProgress(true);
shouldWrite = await cliux.confirm(commonMsg.FIX_CONFIRMATION);
}

if (shouldWrite) {
const outputPath = join(this.folderPath, this.config.moduleConfig[this.moduleName].fileName);
log.debug(`Writing fixed workflows to: ${outputPath}`, this.config.auditContext);

Expand Down
49 changes: 48 additions & 1 deletion packages/contentstack-audit/test/unit/base-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { resolve } from 'path';
import { fancy } from 'fancy-test';
import { expect } from 'chai';
import { FileTransportInstance } from 'winston/lib/winston/transports';

import { BaseCommand } from '../../src/base-command';
import { mockLogger } from './mock-logger';

Expand Down Expand Up @@ -168,4 +167,52 @@ describe('BaseCommand class', () => {
}
});
});

describe('init with external-config', () => {
class CMDCheckConfig extends BaseCommand<typeof CMDCheckConfig> {
async run() {
const sc = this.sharedConfig as Record<string, unknown>;
if (sc.testMergeKey !== undefined) this.log(String(sc.testMergeKey));
if (this.flags['external-config']?.noLog) this.log('noLog');
}
}

fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.stub(winston.transports, 'File', () => fsTransport)
.stub(winston, 'createLogger', createMockWinstonLogger)
.stub(BaseCommand.prototype, 'parse', () =>
Promise.resolve({
args: {},
flags: { 'external-config': { config: { testMergeKey: 'merged' } } },
} as any)
)
.do(() => CMDCheckConfig.run([]))
.do((output: { stdout: string }) => expect(output.stdout).to.include('merged'))
.it('merges external-config.config into sharedConfig when present');

fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.stub(winston.transports, 'File', () => fsTransport)
.stub(winston, 'createLogger', createMockWinstonLogger)
.stub(BaseCommand.prototype, 'parse', () =>
Promise.resolve({
args: {},
flags: { 'external-config': { noLog: true } },
} as any)
)
.do(() => CMDCheckConfig.run([]))
.do((output: { stdout: string }) => expect(output.stdout).to.include('noLog'))
.it('hits noLog branch when external-config.noLog is true');

fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.stub(winston.transports, 'File', () => fsTransport)
.stub(winston, 'createLogger', createMockWinstonLogger)
.stub(BaseCommand.prototype, 'parse', () =>
Promise.resolve({ args: {}, flags: { 'external-config': {} } } as any)
)
.do(() => CMDCheckConfig.run([]))
.it('completes when external-config is empty (no merge, no noLog)');
});
});
12 changes: 12 additions & 0 deletions packages/contentstack-audit/test/unit/logger-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Loaded by Mocha via --file before any test. Forces log config to non-debug
* so the real Logger never enables the debug path and unit tests don't throw
* when user has run: csdx config:set:log --level debug
*/
const cliUtils = require('@contentstack/cli-utilities');
const configHandler = cliUtils.configHandler;
const originalGet = configHandler.get.bind(configHandler);
configHandler.get = function (key) {
if (key === 'log') return { level: 'info' };
return originalGet(key);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"entry-empty-title": { "title": "" },
"entry-no-title": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"1":"empty-entries.json"}
23 changes: 23 additions & 0 deletions packages/contentstack-audit/test/unit/modules/assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,29 @@ describe('Assets module', () => {
}
});

fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('when fix true and multiple assets in chunk, confirm is called only once', async () => {
const instance = new Assets({ ...constructorParam, fix: true });
await instance.prerequisiteData();
const confirmStub = Sinon.stub(cliux, 'confirm').resolves(true);
const writeStub = Sinon.stub(fs, 'writeFileSync');
await instance.lookForReference();
expect(confirmStub.callCount).to.equal(1);
confirmStub.restore();
writeStub.restore();
});

fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('calls writeFixContent once per chunk file when fix is true (not per asset)', async () => {
const instance = new Assets({ ...constructorParam, fix: true });
await instance.prerequisiteData();
const writeFixSpy = Sinon.stub(Assets.prototype, 'writeFixContent').resolves();
await instance.lookForReference();
expect(writeFixSpy.callCount).to.equal(1);
});

fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should log scan success message exactly once per asset', async () => {
Expand Down
Loading
Loading