From 66db3e156ca6400ac315df3eaabc849dd447a4b6 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Sun, 15 Mar 2026 00:56:26 +0530 Subject: [PATCH 1/8] fix: Audit fix progress bar and overwrite confirmation UX --- packages/contentstack-audit/package.json | 4 +- .../contentstack-audit/src/modules/assets.ts | 22 +- .../src/modules/content-types.ts | 1 + .../src/modules/custom-roles.ts | 1 + .../contentstack-audit/src/modules/entries.ts | 16 +- .../src/modules/extensions.ts | 45 +- .../src/modules/field_rules.ts | 1 + .../src/modules/workflows.ts | 47 +- .../test/unit/base-command.test.ts | 49 +- .../test/unit/logger-config.js | 12 + .../empty_title_ct/en-us/empty-entries.json | 4 + .../entries/empty_title_ct/en-us/index.json | 1 + .../test/unit/modules/assets.test.ts | 23 + .../unit/modules/composable-studio.test.ts | 302 ++++++- .../test/unit/modules/content-types.test.ts | 329 +++++++ .../test/unit/modules/custom-roles.test.ts | 242 +++++- .../test/unit/modules/entries.test.ts | 805 +++++++++++++++++- .../test/unit/modules/extensions.test.ts | 56 ++ .../test/unit/modules/field-rules.test.ts | 205 ++++- .../test/unit/modules/workflow.test.ts | 266 +++++- 20 files changed, 2360 insertions(+), 71 deletions(-) create mode 100644 packages/contentstack-audit/test/unit/logger-config.js create mode 100644 packages/contentstack-audit/test/unit/mock/contents/entries/empty_title_ct/en-us/empty-entries.json create mode 100644 packages/contentstack-audit/test/unit/mock/contents/entries/empty_title_ct/en-us/index.json diff --git a/packages/contentstack-audit/package.json b/packages/contentstack-audit/package.json index 36724da19..98bb445c1 100644 --- a/packages/contentstack-audit/package.json +++ b/packages/contentstack-audit/package.json @@ -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" diff --git a/packages/contentstack-audit/src/modules/assets.ts b/packages/contentstack-audit/src/modules/assets.ts index 85dbf38cf..6e84abc78 100644 --- a/packages/contentstack-audit/src/modules/assets.ts +++ b/packages/contentstack-audit/src/modules/assets.ts @@ -27,6 +27,7 @@ export default class Assets extends BaseClass { protected schema: ContentTypeStruct[] = []; protected missingEnvLocales: Record = {}; public moduleName: keyof typeof auditConfig.moduleConfig; + private fixOverwriteConfirmed: boolean | null = null; constructor({ fix, config, moduleName }: ModuleConstructorParam & CtConstructorParam) { super({ config }); @@ -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) { @@ -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); diff --git a/packages/contentstack-audit/src/modules/content-types.ts b/packages/contentstack-audit/src/modules/content-types.ts index f6fc23bc4..79f23dfdd 100644 --- a/packages/contentstack-audit/src/modules/content-types.ts +++ b/packages/contentstack-audit/src/modules/content-types.ts @@ -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); diff --git a/packages/contentstack-audit/src/modules/custom-roles.ts b/packages/contentstack-audit/src/modules/custom-roles.ts index 8ae7a3cbf..88485181e 100644 --- a/packages/contentstack-audit/src/modules/custom-roles.ts +++ b/packages/contentstack-audit/src/modules/custom-roles.ts @@ -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)); diff --git a/packages/contentstack-audit/src/modules/entries.ts b/packages/contentstack-audit/src/modules/entries.ts index 71b4ceefd..5e17a9f83 100644 --- a/packages/contentstack-audit/src/modules/entries.ts +++ b/packages/contentstack-audit/src/modules/entries.ts @@ -60,6 +60,7 @@ export default class Entries extends BaseClass { public environments: string[] = []; public entryMetaData: Record[] = []; public moduleName: keyof typeof auditConfig.moduleConfig = 'entries'; + private fixOverwriteConfirmed: boolean | null = null; constructor({ fix, config, moduleName, ctSchema, gfSchema }: ModuleConstructorParam & CtConstructorParam) { super({ config }); @@ -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)); diff --git a/packages/contentstack-audit/src/modules/extensions.ts b/packages/contentstack-audit/src/modules/extensions.ts index 3d25e8581..7651eaeaa 100644 --- a/packages/contentstack-audit/src/modules/extensions.ts +++ b/packages/contentstack-audit/src/modules/extensions.ts @@ -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); @@ -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 { @@ -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) { + async writeFixContent(fixedExtensions: Record, 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); diff --git a/packages/contentstack-audit/src/modules/field_rules.ts b/packages/contentstack-audit/src/modules/field_rules.ts index 7e2dc935e..6c788a71e 100644 --- a/packages/contentstack-audit/src/modules/field_rules.ts +++ b/packages/contentstack-audit/src/modules/field_rules.ts @@ -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 { diff --git a/packages/contentstack-audit/src/modules/workflows.ts b/packages/contentstack-audit/src/modules/workflows.ts index 69ebda0af..4cc1fcef9 100644 --- a/packages/contentstack-audit/src/modules/workflows.ts +++ b/packages/contentstack-audit/src/modules/workflows.ts @@ -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) { @@ -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 { @@ -250,10 +265,10 @@ 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) { + async writeFixContent(newWorkflowSchema: Record, 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); @@ -261,13 +276,25 @@ export default class Workflows extends BaseClass { 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); diff --git a/packages/contentstack-audit/test/unit/base-command.test.ts b/packages/contentstack-audit/test/unit/base-command.test.ts index 20cf7b8ea..2aa639eaf 100644 --- a/packages/contentstack-audit/test/unit/base-command.test.ts +++ b/packages/contentstack-audit/test/unit/base-command.test.ts @@ -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'; @@ -168,4 +167,52 @@ describe('BaseCommand class', () => { } }); }); + + describe('init with external-config', () => { + class CMDCheckConfig extends BaseCommand { + async run() { + const sc = this.sharedConfig as Record; + 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)'); + }); }); diff --git a/packages/contentstack-audit/test/unit/logger-config.js b/packages/contentstack-audit/test/unit/logger-config.js new file mode 100644 index 000000000..4e434c48b --- /dev/null +++ b/packages/contentstack-audit/test/unit/logger-config.js @@ -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); +}; diff --git a/packages/contentstack-audit/test/unit/mock/contents/entries/empty_title_ct/en-us/empty-entries.json b/packages/contentstack-audit/test/unit/mock/contents/entries/empty_title_ct/en-us/empty-entries.json new file mode 100644 index 000000000..c4c2b64bf --- /dev/null +++ b/packages/contentstack-audit/test/unit/mock/contents/entries/empty_title_ct/en-us/empty-entries.json @@ -0,0 +1,4 @@ +{ + "entry-empty-title": { "title": "" }, + "entry-no-title": {} +} diff --git a/packages/contentstack-audit/test/unit/mock/contents/entries/empty_title_ct/en-us/index.json b/packages/contentstack-audit/test/unit/mock/contents/entries/empty_title_ct/en-us/index.json new file mode 100644 index 000000000..f0fcf8606 --- /dev/null +++ b/packages/contentstack-audit/test/unit/mock/contents/entries/empty_title_ct/en-us/index.json @@ -0,0 +1 @@ +{"1":"empty-entries.json"} diff --git a/packages/contentstack-audit/test/unit/modules/assets.test.ts b/packages/contentstack-audit/test/unit/modules/assets.test.ts index 31c09c98a..9f83806ac 100644 --- a/packages/contentstack-audit/test/unit/modules/assets.test.ts +++ b/packages/contentstack-audit/test/unit/modules/assets.test.ts @@ -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 () => { diff --git a/packages/contentstack-audit/test/unit/modules/composable-studio.test.ts b/packages/contentstack-audit/test/unit/modules/composable-studio.test.ts index a5830a3dd..7131fa408 100644 --- a/packages/contentstack-audit/test/unit/modules/composable-studio.test.ts +++ b/packages/contentstack-audit/test/unit/modules/composable-studio.test.ts @@ -1,8 +1,9 @@ +import fs from 'fs'; import { resolve } from 'path'; import { fancy } from 'fancy-test'; import { expect } from 'chai'; import cloneDeep from 'lodash/cloneDeep'; -import { ux } from '@contentstack/cli-utilities'; +import { ux, cliux } from '@contentstack/cli-utilities'; import sinon from 'sinon'; import config from '../../../src/config'; @@ -135,6 +136,21 @@ describe('ComposableStudio', () => { expect(cs.environmentUidSet.has('blt_env_dev')).to.be.true; expect(cs.environmentUidSet.has('blt_env_prod')).to.be.true; }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('does not load when environments file does not exist', async () => { + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'invalid_path'), + flags: {}, + }), + }); + await cs.loadEnvironments(); + expect(cs.environmentUidSet.size).to.equal(0); + }); }); describe('loadLocales method', () => { @@ -155,6 +171,42 @@ describe('ComposableStudio', () => { expect(cs.localeCodeSet.has('fr-fr')).to.be.true; expect(cs.localeCodeSet.has('de-de')).to.be.true; }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('does not load when master locale file does not exist', async () => { + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'invalid_path'), + flags: {}, + }), + }); + await cs.loadLocales(); + expect(cs.localeCodeSet.size).to.equal(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('loads only master locales when additional locales file does not exist', async () => { + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(`./test/unit/mock/contents/composable_studio`), + flags: {}, + }), + }); + const localesPath = resolve(cs.config.basePath, 'locales', 'locales.json'); + const origExists = fs.existsSync; + sinon.stub(fs, 'existsSync').callsFake((p: fs.PathLike) => { + if (String(p) === localesPath) return false; + return origExists.call(fs, p); + }); + await cs.loadLocales(); + expect(cs.localeCodeSet.size).to.be.greaterThan(0); + }); }); describe('run method with audit fix for composable-studio', () => { @@ -295,6 +347,38 @@ describe('ComposableStudio', () => { expect(projectWithCTIssue.issues).to.include('contentTypeUid'); } }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('reportEntry uses undefined for missing issue types (branch coverage)', async () => { + const onlyInvalidEnv = [ + { uid: 'e1', name: 'EnvOnly', contentTypeUid: 'page_1', settings: { configuration: { environment: 'bad_env', locale: 'en-us' } } }, + ]; + const origRead = fs.readFileSync; + const origExists = fs.existsSync; + sinon.stub(fs, 'readFileSync').callsFake((p: fs.PathOrFileDescriptor) => { + if (String(p).includes('composable_studio.json')) return JSON.stringify(onlyInvalidEnv); + if (String(p).includes('environments.json')) return JSON.stringify([{ uid: 'blt_env_dev' }]); + if (String(p).includes('master-locale') || String(p).includes('locales.json')) return JSON.stringify({ 'en-us': { code: 'en-us' } }); + return origRead.call(fs, p); + }); + sinon.stub(fs, 'existsSync').callsFake((p: fs.PathLike) => { + const s = String(p); + if (s.includes('composable_studio') || s.includes('environments') || s.includes('locales') || s.includes('master-locale')) return true; + return origExists.call(fs, p); + }); + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: cloneDeep(require('./../mock/contents/composable_studio/ctSchema.json')), + config: Object.assign(config, { basePath: resolve(`./test/unit/mock/contents/`), flags: {} }), + }); + const result: any = await cs.run(); + const envOnly = result.find((r: any) => r.uid === 'e1'); + expect(envOnly).to.exist; + expect(envOnly.content_types).to.be.undefined; + expect(envOnly.environment).to.deep.equal(['bad_env']); + expect(envOnly.locale).to.be.undefined; + }); }); describe('Empty and edge cases', () => { @@ -326,5 +410,221 @@ describe('ComposableStudio', () => { // When the file exists and has projects with validation issues, it returns an array expect(result).to.exist; }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns {} when composable studio file does not exist', async () => { + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: cloneDeep(require('./../mock/contents/composable_studio/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents', 'content_types'), + flags: {}, + }), + }); + const result = await cs.run(); + expect(result).to.eql({}); + }); + }); + + describe('run with valid project (no issues)', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('logs when project has no validation issues', async () => { + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: cloneDeep(require('./../mock/contents/composable_studio/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(`./test/unit/mock/contents/`), + flags: {}, + }), + }); + const result = await cs.run(); + expect(Array.isArray(result)).to.be.true; + expect(cs.composableStudioProjects.length).to.be.greaterThan(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('loads single project object (non-array) and normalizes to array', async () => { + const singleProject = { uid: 'only', name: 'Only', contentTypeUid: 'page_1', settings: { configuration: { environment: 'blt_env_dev', locale: 'en-us' } } }; + const origRead = fs.readFileSync; + sinon.stub(fs, 'readFileSync').callsFake((p: fs.PathOrFileDescriptor) => { + if (String(p).includes('composable_studio.json')) return JSON.stringify(singleProject); + return origRead.call(fs, p); + }); + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: cloneDeep(require('./../mock/contents/composable_studio/ctSchema.json')), + config: Object.assign(config, { basePath: resolve(`./test/unit/mock/contents/`), flags: {} }), + }); + await cs.run(); + expect(cs.composableStudioProjects).to.have.lengthOf(1); + expect(cs.composableStudioProjects[0].uid).to.equal('only'); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('when fix true but no issues returns empty array', async () => { + const validProject = { uid: 'v1', name: 'Valid', contentTypeUid: 'page_1', settings: { configuration: { environment: 'blt_env_dev', locale: 'en-us' } } }; + const origRead = fs.readFileSync; + const origExists = fs.existsSync; + sinon.stub(fs, 'readFileSync').callsFake((p: fs.PathOrFileDescriptor) => { + const pathStr = String(p); + if (pathStr.includes('composable_studio.json')) return JSON.stringify([validProject]); + if (pathStr.includes('environments.json')) return JSON.stringify([{ uid: 'blt_env_dev' }, { uid: 'blt_env_prod' }]); + if (pathStr.includes('master-locale') || pathStr.includes('locales.json')) return JSON.stringify({ 'en-us': { code: 'en-us' } }); + return origRead.call(fs, p); + }); + sinon.stub(fs, 'existsSync').callsFake((p: fs.PathLike) => { + const pathStr = String(p); + if (pathStr.includes('composable_studio') || pathStr.includes('environments') || pathStr.includes('locales') || pathStr.includes('master-locale')) return true; + return origExists.call(fs, p); + }); + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: cloneDeep(require('./../mock/contents/composable_studio/ctSchema.json')), + config: Object.assign(config, { basePath: resolve(`./test/unit/mock/contents/`), flags: {} }), + fix: true, + }); + const result: any = await cs.run(); + expect(Array.isArray(result)).to.be.true; + expect(result).to.have.lengthOf(0); + }); + }); + + describe('fixComposableStudioProjects', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns early when readFileSync throws', async () => { + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(`./test/unit/mock/contents/`), + flags: {}, + }), + fix: true, + }); + cs.composableStudioPath = resolve(__dirname, '..', 'mock', 'contents', 'composable_studio', 'composable_studio.json'); + cs.projectsWithIssues = [{ uid: 'p1', name: 'P1' }]; + sinon.stub(fs, 'readFileSync').callsFake(() => { + throw new Error('read failed'); + }); + await cs.fixComposableStudioProjects(); + expect(cs.projectsWithIssues).to.have.lengthOf(1); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(cliux, 'confirm', async () => true) + .stub(fs, 'writeFileSync', () => {}) + .it('hits needsFix true and logs project was fixed when project has invalid env', async () => { + const projectWithInvalidEnv = { + uid: 'inv_env', + name: 'Invalid Env', + contentTypeUid: 'page_1', + settings: { configuration: { environment: 'bad_env', locale: 'en-us' } }, + }; + sinon.stub(fs, 'readFileSync').callsFake(() => JSON.stringify([projectWithInvalidEnv])); + const writeSpy = sinon.stub(fs, 'writeFileSync'); + sinon.stub(cliux, 'confirm').resolves(true); + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: [{ uid: 'page_1', title: 'P1' }] as any, + config: Object.assign(config, { basePath: resolve(`./test/unit/mock/contents/`), flags: {} }), + fix: true, + }); + cs.ctUidSet = new Set(['page_1']); + cs.environmentUidSet = new Set(['blt_env_dev']); + cs.localeCodeSet = new Set(['en-us']); + cs.composableStudioPath = resolve(__dirname, '..', 'mock', 'contents', 'composable_studio', 'composable_studio.json'); + await cs.fixComposableStudioProjects(); + const written = JSON.parse(String(writeSpy.firstCall.args[1])); + expect(written[0].settings.configuration.environment).to.be.undefined; + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(cliux, 'confirm', async () => true) + .stub(fs, 'writeFileSync', () => {}) + .it('logs when project did not need fixing', async () => { + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: [{ uid: 'page_1', title: 'Page 1' }] as any, + config: Object.assign(config, { + basePath: resolve(`./test/unit/mock/contents/`), + flags: {}, + }), + fix: true, + }); + cs.ctUidSet = new Set(['page_1']); + cs.environmentUidSet = new Set(['blt_env_dev']); + cs.localeCodeSet = new Set(['en-us']); + cs.composableStudioPath = resolve(__dirname, '..', 'mock', 'contents', 'composable_studio', 'composable_studio.json'); + cs.projectsWithIssues = [ + { + uid: 'test_project_uid_1', + name: 'Test Project 1', + contentTypeUid: 'page_1', + settings: { configuration: { environment: 'blt_env_dev', locale: 'en-us' } }, + }, + ]; + await cs.fixComposableStudioProjects(); + expect(cs.projectsWithIssues.length).to.equal(1); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(cliux, 'confirm', async () => true) + .stub(fs, 'writeFileSync', () => {}) + .it('handles single project object (non-array) from file', async () => { + const singleProject = { uid: 's1', name: 'Single', contentTypeUid: 'page_1', settings: { configuration: { environment: 'blt_env_dev', locale: 'en-us' } } }; + sinon.stub(fs, 'readFileSync').callsFake(() => JSON.stringify(singleProject)); + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: [{ uid: 'page_1', title: 'P1' }] as any, + config: Object.assign(config, { basePath: resolve(`./test/unit/mock/contents/`), flags: {} }), + fix: true, + }); + cs.ctUidSet = new Set(['page_1']); + cs.environmentUidSet = new Set(['blt_env_dev']); + cs.localeCodeSet = new Set(['en-us']); + cs.composableStudioPath = resolve(__dirname, '..', 'mock', 'contents', 'composable_studio', 'composable_studio.json'); + cs.projectsWithIssues = [singleProject]; + await cs.fixComposableStudioProjects(); + expect(cs.projectsWithIssues).to.have.lengthOf(1); + }); + }); + + describe('writeFixContent', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(cliux, 'confirm', async () => false) + .it('skips write when user declines confirmation', async () => { + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: [], + config: Object.assign(config, { basePath: resolve(__dirname, '..', 'mock', 'contents'), flags: {} }), + fix: true, + }); + const writeSpy = sinon.stub(fs, 'writeFileSync'); + await cs.writeFixContent([{ uid: 'p1', name: 'P1' }]); + expect(writeSpy.callCount).to.equal(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('skips write when fix mode disabled', async () => { + const cs = new ComposableStudio({ + moduleName: 'composable-studio', + ctSchema: [], + config: Object.assign(config, { basePath: resolve(__dirname, '..', 'mock', 'contents'), flags: {} }), + fix: false, + }); + const writeSpy = sinon.stub(fs, 'writeFileSync'); + await cs.writeFixContent([{ uid: 'p1', name: 'P1' }]); + expect(writeSpy.callCount).to.equal(0); + }); }); }); diff --git a/packages/contentstack-audit/test/unit/modules/content-types.test.ts b/packages/contentstack-audit/test/unit/modules/content-types.test.ts index 5dfff7259..b85dae300 100644 --- a/packages/contentstack-audit/test/unit/modules/content-types.test.ts +++ b/packages/contentstack-audit/test/unit/modules/content-types.test.ts @@ -209,6 +209,57 @@ describe('Content types', () => { expect(validateGroupFieldSpy.callCount).to.be.equals(1); }); + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(ContentType.prototype, 'runFixOnSchema', () => []) + .it('skips json extension field when not in fix types', async () => { + const ctInstance = new (class TempClass extends ContentType { + constructor() { + super({ + ...constructorParam, + config: { ...constructorParam.config, 'fix-fields': ['reference'], flags: {} } as any, + }); + this.currentUid = 'test'; + (this as any).missingRefs['test'] = []; + } + })(); + sinon.stub(ContentType.prototype, 'validateReferenceField').returns([]); + sinon.stub(ContentType.prototype, 'validateGlobalField').resolves(); + sinon.stub(ContentType.prototype, 'validateJsonRTEFields').returns([]); + sinon.stub(ContentType.prototype, 'validateGroupField').resolves(); + sinon.stub(ContentType.prototype, 'validateModularBlocksField').resolves(); + const schema = [ + { data_type: 'json', uid: 'j1', display_name: 'Json', field_metadata: { extension: true } }, + ]; + await ctInstance.lookForReference([], { schema } as unknown as CtType); + expect((ctInstance as any).missingRefs['test']).to.have.lengthOf(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('skips json RTE field when not in fix types', async () => { + const ctInstance = new (class TempClass extends ContentType { + constructor() { + super({ + ...constructorParam, + config: { ...constructorParam.config, 'fix-fields': ['reference'], flags: {} } as any, + }); + this.currentUid = 'test'; + (this as any).missingRefs['test'] = []; + } + })(); + sinon.stub(ContentType.prototype, 'validateReferenceField').returns([]); + sinon.stub(ContentType.prototype, 'validateGlobalField').resolves(); + sinon.stub(ContentType.prototype, 'validateJsonRTEFields').returns([]); + sinon.stub(ContentType.prototype, 'validateGroupField').resolves(); + sinon.stub(ContentType.prototype, 'validateModularBlocksField').resolves(); + const schema = [ + { data_type: 'json', uid: 'j1', display_name: 'RTE', field_metadata: { allow_json_rte: true } }, + ]; + await ctInstance.lookForReference([], { schema } as unknown as CtType); + expect((ctInstance as any).missingRefs['test']).to.have.lengthOf(0); + }); + fancy .stdout({ print: process.env.PRINT === 'true' || false }) .stub(ContentType.prototype, 'runFixOnSchema', () => []) @@ -221,6 +272,82 @@ describe('Content types', () => { }); }); + describe('validateExtensionAndAppField method', () => { + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns [] in fix mode', () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).currentTitle = 'Test'; + const field = { + uid: 'ext_f', + extension_uid: 'ext_123', + display_name: 'Ext Field', + data_type: 'json', + } as any; + const result = ctInstance.validateExtensionAndAppField([], field); + expect(result).to.deep.equal([]); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns [] when extension found in loaded extensions', () => { + const ctInstance = new ContentType(constructorParam); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).currentTitle = 'Test'; + (ctInstance as any).extensions = ['ext_123']; + const field = { + uid: 'ext_f', + extension_uid: 'ext_123', + display_name: 'Ext Field', + data_type: 'json', + } as any; + const result = ctInstance.validateExtensionAndAppField([{ uid: 'ext_f', name: 'Ext Field' }], field); + expect(result).to.deep.equal([]); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns issue when extension not in loaded extensions', () => { + const ctInstance = new ContentType(constructorParam); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).currentTitle = 'Test'; + (ctInstance as any).extensions = []; + const field = { + uid: 'ext_f', + extension_uid: 'missing_ext', + display_name: 'Ext Field', + data_type: 'json', + } as any; + const result = ctInstance.validateExtensionAndAppField([{ uid: 'ext_f', name: 'Ext Field' }], field); + expect(result).to.have.lengthOf(1); + expect(result[0].missingRefs).to.deep.include({ uid: 'ext_f', extension_uid: 'missing_ext', type: 'Extension or Apps' }); + }); + }); + + describe('validateReferenceToValues method', () => { + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns empty when single reference exists in ctSchema', () => { + const ctInstance = new ContentType(constructorParam); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).currentTitle = 'Test'; + const field = { uid: 'ref_f', reference_to: 'page_1', display_name: 'Ref', data_type: 'reference' } as any; + const result = ctInstance.validateReferenceToValues([], field); + expect(result).to.have.lengthOf(0); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('skips ref in skipRefs in array path', () => { + const ctInstance = new ContentType(constructorParam); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).currentTitle = 'Test'; + const field = { uid: 'ref_f', reference_to: ['page_1', 'sys_assets'], display_name: 'Ref', data_type: 'reference' } as any; + const result = ctInstance.validateReferenceToValues([], field); + expect(result).to.have.lengthOf(0); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns empty when array references all exist in ctSchema', () => { + const ctInstance = new ContentType(constructorParam); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).currentTitle = 'Test'; + const field = { uid: 'ref_f', reference_to: ['page_1', 'page_2'], display_name: 'Ref', data_type: 'reference' } as any; + const result = ctInstance.validateReferenceToValues([], field); + expect(result).to.have.lengthOf(0); + }); + }); + describe('validateReferenceField method', () => { fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('should return missing reference', async () => { const ctInstance = new ContentType(constructorParam); @@ -286,6 +413,158 @@ describe('Content types', () => { }); }); + describe('fixMissingExtensionOrApp method', () => { + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns field when extension found in loaded extensions', () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).currentTitle = 'Test'; + (ctInstance as any).missingRefs['test'] = []; + (ctInstance as any).extensions = ['ext_123']; + const field = { + uid: 'ext_f', + extension_uid: 'ext_123', + display_name: 'Ext Field', + data_type: 'json', + } as any; + const result = ctInstance.fixMissingExtensionOrApp([], field); + expect(result).to.equal(field); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns null and pushes to missingRefs when extension missing and fix mode', () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).currentTitle = 'Test'; + (ctInstance as any).missingRefs['test'] = []; + (ctInstance as any).extensions = []; + const field = { + uid: 'ext_f', + extension_uid: 'missing_ext', + display_name: 'Ext Field', + data_type: 'json', + } as any; + const result = ctInstance.fixMissingExtensionOrApp([{ uid: 'ext_f', name: 'Ext' }], field); + expect(result).to.be.null; + expect((ctInstance as any).missingRefs['test']).to.have.lengthOf(1); + expect((ctInstance as any).missingRefs['test'][0].fixStatus).to.equal('Fixed'); + }); + }); + + describe('runFixOnSchema method', () => { + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('filters out field with empty schema when in schema-fields-data-type', () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).missingRefs['test'] = []; + const schema = [ + { data_type: 'blocks', uid: 'b1', display_name: 'Blocks', schema: [], blocks: [] }, + { data_type: 'text', uid: 't1', display_name: 'Title' }, + ] as any; + const result = ctInstance.runFixOnSchema([], schema); + expect(result.some((f: any) => f?.uid === 't1')).to.be.true; + expect(result.filter((f: any) => f?.uid === 'b1')).to.have.lengthOf(0); + }); + }); + + describe('fixModularBlocksReferences method', () => { + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns false for block with no schema in content-types', () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).missingRefs['test'] = []; + const blocks = [ + { uid: 'blk1', title: 'Block1', reference_to: 'gf_0', schema: undefined }, + ] as any; + const result = ctInstance.fixModularBlocksReferences([], blocks); + expect(result).to.have.lengthOf(0); + expect((ctInstance as any).missingRefs['test']).to.have.lengthOf(1); + }); + }); + + describe('fixMissingReferences method', () => { + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('skips reference in skipRefs (single reference)', () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).missingRefs['test'] = []; + const field = { + uid: 'ref_f', + reference_to: 'sys_assets', + display_name: 'Ref', + data_type: 'reference', + field_metadata: {}, + } as any; + const result = ctInstance.fixMissingReferences([], field); + expect(result).to.equal(field); + expect(field.reference_to).to.deep.equal(['sys_assets']); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('keeps single reference when it exists in ctSchema (branch: found)', () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).missingRefs['test'] = []; + const field = { + uid: 'ref_f', + reference_to: 'page_1', + display_name: 'Ref', + data_type: 'reference', + field_metadata: {}, + } as any; + const result = ctInstance.fixMissingReferences([], field); + expect(result).to.equal(field); + expect(field.reference_to).to.deep.equal(['page_1']); + expect((ctInstance as any).missingRefs['test']).to.have.lengthOf(0); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('removes missing refs from array and pushes to missingRefs when fix mode', () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).missingRefs['test'] = []; + const field = { + uid: 'ref_f', + reference_to: ['page_1', 'nonexistent_ct'], + display_name: 'Ref', + data_type: 'reference', + field_metadata: {}, + } as any; + const result = ctInstance.fixMissingReferences([], field); + expect(result).to.equal(field); + expect(field.reference_to).to.deep.equal(['page_1']); + expect((ctInstance as any).missingRefs['test']).to.have.lengthOf(1); + expect((ctInstance as any).missingRefs['test'][0].fixStatus).to.equal('Fixed'); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('skips references in skipRefs when processing array', () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).missingRefs['test'] = []; + const field = { + uid: 'ref_f', + reference_to: ['page_1', 'sys_assets', 'page_2'], + display_name: 'Ref', + data_type: 'reference', + field_metadata: {}, + } as any; + ctInstance.fixMissingReferences([], field); + expect(field.reference_to).to.include('page_1'); + expect(field.reference_to).to.include('page_2'); + expect(field.reference_to).to.include('sys_assets'); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('keeps refs when all references exist in ctSchema (array path)', () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).missingRefs['test'] = []; + const field = { + uid: 'ref_f', + reference_to: ['page_1', 'page_2'], + display_name: 'Ref', + data_type: 'reference', + field_metadata: {}, + } as any; + const result = ctInstance.fixMissingReferences([], field); + expect(result).to.equal(field); + expect(field.reference_to).to.deep.equal(['page_1', 'page_2']); + expect((ctInstance as any).missingRefs['test']).to.have.lengthOf(0); + }); + }); + describe('fixGlobalFieldReferences method', () => { fancy .stdout({ print: process.env.PRINT === 'true' || false }) @@ -309,6 +588,56 @@ describe('Content types', () => { const actual = ctInstance.missingRefs; expect(actual).to.deep.equals({'audit-fix': []}); expect(fixField?.schema).is.undefined; + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('pushes missingRefs when global-fields module and referred GF has no schema', () => { + const ctInstance = new ContentType({ + ...constructorParam, + moduleName: 'global-fields', + gfSchema: [{ uid: 'ref_gf', title: 'Ref GF', schema: undefined }] as any, + ctSchema: [], + }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).currentTitle = 'Test'; + (ctInstance as any).missingRefs['test'] = []; + const field = { + data_type: 'global_field', + display_name: 'Global', + reference_to: 'ref_gf', + uid: 'global_field', + schema: undefined, + } as any; + const result = ctInstance.fixGlobalFieldReferences([], field); + expect(result).to.equal(field); + expect((ctInstance as any).missingRefs['test']).to.have.lengthOf(1); + expect((ctInstance as any).missingRefs['test'][0].missingRefs).to.equal('Referred Global Field Does not exist'); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('pushes Empty schema found when content-types module and GF has no schema', () => { + const ctInstance = new ContentType({ + ...constructorParam, + moduleName: 'content-types', + fix: true, + }); + (ctInstance as any).currentUid = 'test'; + (ctInstance as any).currentTitle = 'Test'; + (ctInstance as any).missingRefs['test'] = []; + (ctInstance as any).gfSchema = [{ uid: 'gf_empty', title: 'GF', schema: undefined }] as any; + const field = { + data_type: 'global_field', + display_name: 'Global', + reference_to: 'gf_empty', + uid: 'global_field', + schema: undefined, + } as any; + const result = ctInstance.fixGlobalFieldReferences([], field); + expect(result).to.equal(field); + expect((ctInstance as any).missingRefs['test']).to.have.lengthOf(1); + expect((ctInstance as any).missingRefs['test'][0].missingRefs).to.equal('Empty schema found'); // NOTE: TO DO // expect(actual).to.deep.equals(expected); // expect(fixField?.schema).is.not.empty; diff --git a/packages/contentstack-audit/test/unit/modules/custom-roles.test.ts b/packages/contentstack-audit/test/unit/modules/custom-roles.test.ts index a41fb4af7..3046dc071 100644 --- a/packages/contentstack-audit/test/unit/modules/custom-roles.test.ts +++ b/packages/contentstack-audit/test/unit/modules/custom-roles.test.ts @@ -1,8 +1,10 @@ -import { resolve } from 'path'; +import { join, resolve } from 'path'; import { expect } from 'chai'; import cloneDeep from 'lodash/cloneDeep'; import fancy from 'fancy-test'; import Sinon from 'sinon'; +import fs from 'fs'; +import { cliux } from '@contentstack/cli-utilities'; import config from '../../../src/config'; import { CustomRoles } from '../../../src/modules'; import { CtConstructorParam, ModuleConstructorParam } from '../../../src/types'; @@ -22,6 +24,14 @@ describe('Custom roles module', () => { Sinon.stub(require('@contentstack/cli-utilities'), 'log').value(mockLogger); }); + describe('validateModules', () => { + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns custom-roles when module not in config', () => { + const cr = new CustomRoles(constructorParam); + const result = (cr as any).validateModules('invalid-module', config.moduleConfig); + expect(result).to.equal('custom-roles'); + }); + }); + describe('run method', () => { fancy .stdout({ print: process.env.PRINT === 'true' || false }) @@ -31,10 +41,87 @@ describe('Custom roles module', () => { config: { ...constructorParam.config, branch: 'test' }, }); await customRoleInstance.run(); - expect(customRoleInstance.missingFieldsInCustomRoles).length(2); + expect(customRoleInstance.missingFieldsInCustomRoles).to.have.lengthOf(2); expect(JSON.stringify(customRoleInstance.missingFieldsInCustomRoles)).includes('"branches":["main"]'); }); + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('creates progress when totalCount > 0', async () => { + const cr = new CustomRoles({ ...constructorParam, config: { ...constructorParam.config, branch: 'test' } }); + (cr as any).createSimpleProgress = Sinon.stub().callsFake(function (this: any) { + const progress = { updateStatus: Sinon.stub(), tick: Sinon.stub(), complete: Sinon.stub() }; + this.progressManager = progress; + return progress; + }); + await cr.run(5); + expect((cr as any).createSimpleProgress.calledWith('custom-roles', 5)).to.be.true; + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('skips branch validation when config.branch is not set', async () => { + const cr = new CustomRoles({ + ...constructorParam, + config: { ...constructorParam.config, branch: undefined }, + }); + await cr.run(); + expect(cr.missingFieldsInCustomRoles).to.be.an('array'); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('does not create progressManager when totalCount is 0 or undefined', async () => { + const cr = new CustomRoles({ ...constructorParam, config: { ...constructorParam.config, branch: 'main' } }); + await cr.run(); + expect((cr as any).progressManager).to.be.null; + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('hits no-fix branch when fix false and no issues', async () => { + const cr = new CustomRoles({ + ...constructorParam, + config: { ...constructorParam.config, branch: 'main' }, + fix: false, + }); + await cr.run(); + expect(cr.missingFieldsInCustomRoles).to.be.an('array'); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('hits has no branch issues when role has no branch rules', async () => { + Sinon.stub(fs, 'existsSync').callsFake((p: fs.PathLike) => String(p).includes('custom-roles')); + Sinon.stub(fs, 'readFileSync').callsFake(() => + JSON.stringify({ + noBranchRule: { + uid: 'noBranchRule', + name: 'No Branch Rule', + rules: [{ module: 'environment', environments: [] }], + }, + }) + ); + const cr = new CustomRoles({ + ...constructorParam, + config: { ...constructorParam.config, branch: 'test' }, + }); + await cr.run(); + expect(cr.missingFieldsInCustomRoles.some((r: any) => r.uid === 'noBranchRule')).to.be.false; + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('logs no fixes needed when fix disabled or no issues', async () => { + const cr = new CustomRoles({ + ...constructorParam, + config: { ...constructorParam.config, branch: 'main' }, + fix: false, + }); + await cr.run(); + expect(cr.missingFieldsInCustomRoles).to.be.an('array'); + }); + fancy .stdout({ print: process.env.PRINT === 'true' || false }) .stub(CustomRoles.prototype, 'fixCustomRoleSchema', async () => {}) @@ -49,6 +136,17 @@ describe('Custom roles module', () => { expect(logSpy.callCount).to.be.equals(1); }); + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns {} when folder path does not exist', async () => { + const cr = new CustomRoles({ + ...constructorParam, + config: { ...constructorParam.config, basePath: resolve(__dirname, '..', 'mock', 'invalid_path') }, + }); + const result = await cr.run(); + expect(result).to.eql({}); + }); + fancy .stdout({ print: process.env.PRINT === 'true' || false }) .stub(CustomRoles.prototype, 'writeFixContent', async () => {}) @@ -64,6 +162,146 @@ describe('Custom roles module', () => { }); }); + describe('fixCustomRoleSchema', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(CustomRoles.prototype, 'writeFixContent', async () => {}) + .stub(fs, 'existsSync', () => true) + .stub(fs, 'readFileSync', () => JSON.stringify({ uid1: { uid: 'uid1', name: 'R1', rules: [{ module: 'branch', branches: ['main'] }] } })) + .it('skips fix for each role when config.branch is not set', async () => { + const cr = new CustomRoles({ + ...constructorParam, + fix: true, + config: { ...constructorParam.config, branch: undefined }, + }); + cr.customRolePath = join(cr.folderPath, cr.fileName); + cr.customRoleSchema = [{ uid: 'uid1', name: 'R1', rules: [{ module: 'branch', branches: ['main'] }] }] as any; + await cr.fixCustomRoleSchema(); + expect(cr.customRoleSchema.length).to.equal(1); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'existsSync', () => false) + .it('loads empty schema when custom role path does not exist', async () => { + const cr = new CustomRoles({ + ...constructorParam, + fix: true, + config: { ...constructorParam.config, branch: 'test' }, + }); + cr.customRolePath = join(cr.folderPath, cr.fileName); + cr.customRoleSchema = [{ uid: 'u1', name: 'R1', rules: [] }] as any; + const writeSpy = Sinon.stub(CustomRoles.prototype, 'writeFixContent').resolves(); + await cr.fixCustomRoleSchema(); + expect(writeSpy.callCount).to.equal(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'existsSync', () => true) + .stub(fs, 'readFileSync', () => JSON.stringify({})) + .it('returns early when no custom roles to fix', async () => { + const cr = new CustomRoles({ + ...constructorParam, + fix: true, + config: { ...constructorParam.config, branch: 'test' }, + }); + cr.customRolePath = join(cr.folderPath, cr.fileName); + cr.customRoleSchema = []; + const writeSpy = Sinon.stub(CustomRoles.prototype, 'writeFixContent').resolves(); + await cr.fixCustomRoleSchema(); + expect(writeSpy.callCount).to.equal(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'existsSync', () => true) + .stub(fs, 'readFileSync', () => + JSON.stringify({ + uid1: { + uid: 'uid1', + name: 'R1', + rules: [{ module: 'branch', branches: ['main', 'test'] }], + }, + }) + ) + .stub(cliux, 'confirm', async () => true) + .it('keeps config branch and removes others in branch rules', async () => { + const cr = new CustomRoles({ + ...constructorParam, + fix: true, + config: { ...constructorParam.config, branch: 'test' }, + }); + cr.customRolePath = join(cr.folderPath, cr.fileName); + cr.customRoleSchema = [ + { uid: 'uid1', name: 'R1', rules: [{ module: 'branch', branches: ['main', 'test'] }] }, + ] as any; + const writeFixStub = Sinon.stub(CustomRoles.prototype, 'writeFixContent').callsFake(async (schema: Record) => { + const rule = schema?.uid1?.rules?.find((r: any) => r.module === 'branch'); + expect(rule).to.exist; + expect(rule.branches).to.eql(['test']); + }); + await cr.fixCustomRoleSchema(); + expect(writeFixStub.called).to.be.true; + }); + }); + + describe('writeFixContent', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'writeFileSync', Sinon.stub()) + .it('writes file when fix is true and skipConfirm (copy-dir)', async () => { + const cr = new CustomRoles({ + ...constructorParam, + fix: true, + config: { ...constructorParam.config, branch: 'test', flags: { 'copy-dir': true } }, + }); + await cr.writeFixContent({ uid123: {} } as any); + expect((fs.writeFileSync as Sinon.SinonStub).called).to.be.true; + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'writeFileSync', Sinon.stub()) + .stub(cliux, 'confirm', async () => true) + .it('writes file when fix is true and user confirms', async () => { + const cr = new CustomRoles({ + ...constructorParam, + fix: true, + config: { ...constructorParam.config, branch: 'test', flags: {} }, + }); + await cr.writeFixContent({ uid123: {} } as any); + expect((fs.writeFileSync as Sinon.SinonStub).called).to.be.true; + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'writeFileSync', Sinon.stub()) + .stub(cliux, 'confirm', async () => false) + .it('does not write file when user declines confirmation', async () => { + const cr = new CustomRoles({ + ...constructorParam, + fix: true, + config: { ...constructorParam.config, branch: 'test', flags: {} }, + }); + await cr.writeFixContent({ uid123: {} } as any); + expect((fs.writeFileSync as Sinon.SinonStub).called).to.be.false; + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('skips write when not in fix mode', async () => { + const cr = new CustomRoles({ + ...constructorParam, + fix: false, + config: { ...constructorParam.config, branch: 'test', flags: {} }, + }); + const writeSpy = Sinon.stub(fs, 'writeFileSync'); + await cr.writeFixContent({ uid123: {} } as any); + expect(writeSpy.called).to.be.false; + }); + }); + afterEach(() => { Sinon.restore(); // Clears Sinon spies/stubs/mocks }); diff --git a/packages/contentstack-audit/test/unit/modules/entries.test.ts b/packages/contentstack-audit/test/unit/modules/entries.test.ts index 911ba3316..c10f8c824 100644 --- a/packages/contentstack-audit/test/unit/modules/entries.test.ts +++ b/packages/contentstack-audit/test/unit/modules/entries.test.ts @@ -104,6 +104,30 @@ describe('Entries module', () => { expect(fixPrerequisiteData.callCount).to.be.equals(1); expect(prepareEntryMetaData.callCount).to.be.equals(1); }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('run with real folder runs main loop and removeEmptyVal', async () => { + const realCtSchema = cloneDeep(require('../mock/contents/content_types/schema.json')); + const realGfSchema = cloneDeep(require('../mock/contents/global_fields/globalfields.json')); + ctStub.resolves(realCtSchema); + gfStub.resolves(realGfSchema); + try { + const ctInstance = new Entries(constructorParam); + const result = (await ctInstance.run()) as any; + expect(result).to.have.property('missingEntryRefs'); + expect(result).to.have.property('missingSelectFeild'); + expect(result).to.have.property('missingMandatoryFields'); + expect(result).to.have.property('missingTitleFields'); + expect(result).to.have.property('missingEnvLocale'); + expect(result).to.have.property('missingMultipleFields'); + } finally { + ctStub.resetHistory(); + gfStub.resetHistory(); + ctStub.resolves({ ct1: [{}] }); + gfStub.resolves({ gf1: [{}] }); + } + }); }); describe('fixPrerequisiteData method', () => { @@ -121,6 +145,44 @@ describe('Entries module', () => { expect(ctInstance.ctSchema).deep.contain({ ct1: [{}] }); expect(ctInstance.gfSchema).deep.contain({ gf1: [{}] }); }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('loads extensions when extensions.json exists', async () => { + Sinon.stub(fs, 'existsSync').callsFake((path: any) => String(path).includes('extensions.json')); + Sinon.stub(fs, 'readFileSync').callsFake(() => JSON.stringify({ ext_uid_1: {}, ext_uid_2: {} })); + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).missingRefs = { 'test-entry': [] }; + (ctInstance as any).missingSelectFeild = { 'test-entry': [] }; + (ctInstance as any).missingMandatoryFields = { 'test-entry': [] }; + await ctInstance.fixPrerequisiteData(); + expect(ctInstance.extensions).to.include('ext_uid_1'); + expect(ctInstance.extensions).to.include('ext_uid_2'); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('loads extension UIDs from marketplace apps when file exists', async () => { + if ((fs.existsSync as any).restore) (fs.existsSync as any).restore(); + if ((fs.readFileSync as any).restore) (fs.readFileSync as any).restore(); + Sinon.stub(fs, 'existsSync').callsFake((path: any) => String(path).includes('marketplace_apps.json')); + Sinon.stub(fs, 'readFileSync').callsFake((path: any) => { + if (String(path).includes('marketplace')) { + return JSON.stringify([ + { uid: 'app1', manifest: { name: 'App1' }, ui_location: { locations: [{ meta: { extension_uid: 'market_ext_1' } }] } }, + ]); + } + return '{}'; + }); + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).missingRefs = { 'test-entry': [] }; + (ctInstance as any).missingSelectFeild = { 'test-entry': [] }; + (ctInstance as any).missingMandatoryFields = { 'test-entry': [] }; + await ctInstance.fixPrerequisiteData(); + expect(ctInstance.extensions).to.include('market_ext_1'); + }); }); describe('writeFixContent method', () => { @@ -149,6 +211,39 @@ describe('Entries module', () => { expect(writeFileSync.calledWithExactly(resolve(__dirname, '..', 'mock', 'contents'), JSON.stringify({}))).to.be .true; }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'writeFileSync', () => {}) + .it("should skip confirmation when copy-dir flag passed", async () => { + const writeFileSync = Sinon.spy(fs, 'writeFileSync'); + const ctInstance = new Entries({ ...constructorParam, fix: true }); + ctInstance.config.flags['copy-dir'] = true; + await ctInstance.writeFixContent(resolve(__dirname, '..', 'mock', 'contents'), { e1: {} as EntryStruct }); + expect(writeFileSync.callCount).to.be.equals(1); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'writeFileSync', () => {}) + .stub(cliux, 'confirm', async () => false) + .it('should not write when user declines confirmation', async () => { + const writeFileSync = Sinon.spy(fs, 'writeFileSync'); + const ctInstance = new Entries({ ...constructorParam, fix: true }); + await ctInstance.writeFixContent(resolve(__dirname, '..', 'mock', 'contents'), {}); + expect(writeFileSync.callCount).to.be.equals(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'writeFileSync', () => {}) + .it('when fix true and writeFixContent called multiple times, confirm is called only once', async () => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + const confirmStub = Sinon.stub(cliux, 'confirm').resolves(true); + await ctInstance.writeFixContent(resolve(__dirname, '..', 'mock', 'contents', 'chunk1.json'), { e1: {} as EntryStruct }); + await ctInstance.writeFixContent(resolve(__dirname, '..', 'mock', 'contents', 'chunk2.json'), { e2: {} as EntryStruct }); + expect(confirmStub.callCount).to.equal(1); + }); }); describe('lookForReference method', () => { @@ -219,10 +314,12 @@ describe('Entries module', () => { fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('should return missing reference', async () => { const ctInstance = new Class(); + // Reference that is missing from entryMetaData so it appears in missingRefs + const entryData = [{ uid: 'test-uid-1', _content_type_uid: 'page_0' }]; const missingRefs = await ctInstance.validateReferenceField( [{ uid: 'test-uid', name: 'reference', field: 'reference' }], ctInstance.ctSchema[3].schema as any, - ctInstance.entries['reference'] as any, + entryData as any, ); expect(missingRefs).deep.equal([ @@ -250,34 +347,24 @@ describe('Entries module', () => { }); }); - // describe('validateGlobalField method', () => { - // let lookForReferenceSpy; - // let ctInstance; - - // beforeEach(() => { - // // Restore original methods before each test - // Sinon.restore(); - - // // Spy on the lookForReference method - // lookForReferenceSpy = Sinon.spy(Entries.prototype, 'lookForReference'); - - // // Create a new instance of Entries for each test - // ctInstance = new (class extends Entries { - // public entries: Record = ( - // require('../mock/contents/entries/page_1/en-us/e7f6e3cc-64ca-4226-afb3-7794242ae5f5-entries.json') as any - // )['test-uid-2']; - // })(constructorParam); - // }); - - // it('should call lookForReference method', async () => { - // // Call the method under test - // await ctInstance.validateGlobalField([], ctInstance.ctSchema as any, ctInstance.entries); - - // // Assertions - // expect(lookForReferenceSpy.callCount).to.be.equals(1); - // expect(lookForReferenceSpy.calledWithExactly([], ctInstance.ctSchema, ctInstance.entries)).to.be.true; - // }); - // }); + describe('validateGlobalField method', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('calls lookForReference and completes validation', () => { + const lookForReference = Sinon.spy(Entries.prototype, 'lookForReference'); + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).missingRefs = { 'test-entry': [] }; + (ctInstance as any).missingSelectFeild = { 'test-entry': [] }; + (ctInstance as any).missingMandatoryFields = { 'test-entry': [] }; + const tree: Record[] = []; + const fieldStructure = { uid: 'gf_1', display_name: 'Global Field 1', schema: [{ uid: 'ref', data_type: 'reference' }] }; + const field = { ref: [] }; + ctInstance.validateGlobalField(tree, fieldStructure as any, field as any); + expect(lookForReference.callCount).to.equal(1); + expect(lookForReference.calledWith(tree, fieldStructure, field)).to.be.true; + }); + }); describe('validateJsonRTEFields method', () => { fancy @@ -406,6 +493,154 @@ describe('Entries module', () => { }); }); + describe('removeMissingKeysOnEntry', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('removes entry keys not in schema and not in systemKeys', () => { + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'e1'; + const schema = [{ uid: 'title' }, { uid: 'body' }]; + const entry: Record = { title: 'T', body: 'B', invalid_key: 'remove me', uid: 'keep-uid' }; + (ctInstance as any).removeMissingKeysOnEntry(schema, entry); + expect(entry.invalid_key).to.be.undefined; + expect(entry.title).to.equal('T'); + expect(entry.uid).to.equal('keep-uid'); + }); + }); + + describe('runFixOnSchema', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('skips field when not present in entry', () => { + if ((Entries.prototype.fixGlobalFieldReferences as any).restore) (Entries.prototype.fixGlobalFieldReferences as any).restore(); + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).currentTitle = 'E1'; + (ctInstance as any).missingRefs = { e1: [] }; + (ctInstance as any).missingMultipleField = { e1: [] }; + const schema = [{ uid: 'only_in_schema', data_type: 'text', display_name: 'Only' }]; + const entry = { other_key: 'v' }; + const result = (ctInstance as any).runFixOnSchema([], schema, entry); + expect((entry as any).only_in_schema).to.be.undefined; + expect(result).to.equal(entry); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('converts non-array to array when field is multiple', () => { + if ((Entries.prototype.fixGlobalFieldReferences as any).restore) (Entries.prototype.fixGlobalFieldReferences as any).restore(); + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).currentTitle = 'E1'; + (ctInstance as any).missingRefs = { e1: [] }; + (ctInstance as any).missingMultipleField = { e1: [] }; + Sinon.stub(Entries.prototype, 'fixGlobalFieldReferences').callsFake((_t: any, _f: any, e: any) => e); + const schema = [{ uid: 'multi', data_type: 'global_field', multiple: true, display_name: 'M', schema: [] }]; + const entry = { multi: 'single value' as any }; + (ctInstance as any).runFixOnSchema([], schema, entry); + expect(entry.multi).to.eql(['single value']); + (Entries.prototype.fixGlobalFieldReferences as any).restore?.(); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('deletes reference field when fixMissingReferences returns falsy', () => { + if ((Entries.prototype.fixMissingReferences as any).restore) (Entries.prototype.fixMissingReferences as any).restore(); + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).entryMetaData = []; + Sinon.stub(Entries.prototype, 'fixMissingReferences').returns(undefined as any); + const schema = [{ uid: 'ref', data_type: 'reference', display_name: 'Ref', reference_to: ['ct1'] }]; + const entry = { ref: [{ uid: 'missing' }] }; + (ctInstance as any).runFixOnSchema([], schema, entry); + expect(entry.ref).to.be.undefined; + (Entries.prototype.fixMissingReferences as any).restore?.(); + }); + }); + + describe('validateMandatoryFields', () => { + const initInstance = (ctInstance: Entries) => { + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).currentTitle = 'Test Entry'; + (ctInstance as any).missingMandatoryFields = { 'test-entry': [] }; + }; + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns missing field when mandatory JSON RTE is empty', () => { + const ctInstance = new Entries(constructorParam); + initInstance(ctInstance); + const tree = [{ uid: 'test-entry', name: 'Test Entry' }]; + const fieldStructure = { + uid: 'body', + display_name: 'Body', + data_type: 'json', + mandatory: true, + multiple: false, + field_metadata: { allow_json_rte: true }, + }; + const entry = { + body: { + children: [{ children: [{ text: '' }] }], + }, + }; + const result = (ctInstance as any).validateMandatoryFields(tree, fieldStructure, entry); + expect(result).to.have.length(1); + expect(result[0]).to.include({ display_name: 'Body', missingFieldUid: 'body' }); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns missing field when mandatory number is empty', () => { + const ctInstance = new Entries(constructorParam); + initInstance(ctInstance); + const tree = [{ uid: 'test-entry', name: 'Test Entry' }]; + const fieldStructure = { uid: 'num', display_name: 'Number', data_type: 'number', mandatory: true, multiple: false, field_metadata: {} }; + const entry = {}; + const result = (ctInstance as any).validateMandatoryFields(tree, fieldStructure, entry); + expect(result).to.have.length(1); + expect(result[0].missingFieldUid).to.equal('num'); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns missing field when mandatory text is empty', () => { + const ctInstance = new Entries(constructorParam); + initInstance(ctInstance); + const tree = [{ uid: 'test-entry', name: 'Test Entry' }]; + const fieldStructure = { uid: 'title', display_name: 'Title', data_type: 'text', mandatory: true, multiple: false, field_metadata: {} }; + const entry = { title: '' }; + const result = (ctInstance as any).validateMandatoryFields(tree, fieldStructure, entry); + expect(result).to.have.length(1); + expect(result[0].missingFieldUid).to.equal('title'); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns missing field when mandatory reference array is empty', () => { + const ctInstance = new Entries(constructorParam); + initInstance(ctInstance); + const tree = [{ uid: 'test-entry', name: 'Test Entry' }]; + const fieldStructure = { uid: 'ref', display_name: 'Reference', data_type: 'reference', mandatory: true, multiple: false, field_metadata: {} }; + const entry = { ref: [] }; + const result = (ctInstance as any).validateMandatoryFields(tree, fieldStructure, entry); + expect(result).to.have.length(1); + expect(result[0].missingFieldUid).to.equal('ref'); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns empty array when mandatory field has value', () => { + const ctInstance = new Entries(constructorParam); + initInstance(ctInstance); + const tree = [{ uid: 'test-entry', name: 'Test Entry' }]; + const fieldStructure = { uid: 'title', display_name: 'Title', data_type: 'text', mandatory: true, multiple: false, field_metadata: {} }; + const entry = { title: 'Has value' }; + const result = (ctInstance as any).validateMandatoryFields(tree, fieldStructure, entry); + expect(result).to.eql([]); + }); + + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('returns empty array when field is not mandatory', () => { + const ctInstance = new Entries(constructorParam); + initInstance(ctInstance); + const tree = [{ uid: 'test-entry', name: 'Test Entry' }]; + const fieldStructure = { uid: 'opt', display_name: 'Optional', data_type: 'text', mandatory: false, multiple: false, field_metadata: {} }; + const entry = {}; + const result = (ctInstance as any).validateMandatoryFields(tree, fieldStructure, entry); + expect(result).to.eql([]); + }); + }); + describe('validateSelectField method', () => { fancy .stdout({ print: process.env.PRINT === 'true' || false }) @@ -1090,6 +1325,31 @@ describe('Entries module', () => { expect(result).to.have.length(0); }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns empty array when fix mode is enabled', () => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).entryMetaData = []; + const referenceFieldSchema = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1'] }; + const entryData = [{ uid: 'blt1', _content_type_uid: 'ct1' }]; + const result = ctInstance.validateReferenceValues([], referenceFieldSchema as any, entryData); + expect(result).to.eql([]); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('does not flag blt reference when found in entryMetaData and content type allowed', () => { + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).entryMetaData = [{ uid: 'blt999', ctUid: 'ct1' }]; + const referenceFieldSchema = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1'] }; + const entryData = ['blt999']; + const tree = [{ uid: 'test-entry', name: 'Test Entry' }]; + const result = ctInstance.validateReferenceValues(tree, referenceFieldSchema as any, entryData as any); + expect(result).to.have.length(0); + }); }); describe('validateModularBlocksField method', () => { @@ -1309,6 +1569,61 @@ describe('Entries module', () => { expect(result).to.be.an('array'); // Should return an array of missing references }); + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('reports extension as valid when extension_uid is in extensions list', async ({}) => { + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).missingRefs = { 'test-entry': [] }; + ctInstance.extensions = ['valid-ext-uid']; + const field = { uid: 'ext_f', display_name: 'Ext Field', data_type: 'json', field_metadata: { extension: true } }; + const entry = { ext_f: { metadata: { extension_uid: 'valid-ext-uid' } } }; + const tree: Record[] = []; + const result = ctInstance.validateExtensionAndAppField(tree, field as any, entry as any); + expect(result).to.be.an('array'); + expect(result).to.have.length(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns empty array when fix mode is enabled', async ({}) => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'test-entry'; + ctInstance.extensions = []; + const field = { uid: 'ext_f', display_name: 'Ext', data_type: 'json' }; + const entry = { ext_f: { metadata: { extension_uid: 'any' } } }; + const result = ctInstance.validateExtensionAndAppField([], field as any, entry as any); + expect(result).to.eql([]); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns empty array when field has no extension data (no entry[uid])', async ({}) => { + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'test-entry'; + ctInstance.extensions = []; + const field = { uid: 'ext_f', display_name: 'Ext Field', data_type: 'json' }; + const entry = {} as any; + const result = ctInstance.validateExtensionAndAppField([], field as any, entry); + expect(result).to.eql([]); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns result with treeStr when extension UID is missing', async ({}) => { + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).missingRefs = { 'test-entry': [] }; + ctInstance.extensions = ['other-ext']; + const field = { uid: 'ext_f', display_name: 'Ext Field', data_type: 'json' }; + const entry = { ext_f: { metadata: { extension_uid: 'missing-ext' } } }; + const tree = [{ uid: 'e1', name: 'Entry 1' }]; + const result = ctInstance.validateExtensionAndAppField(tree, field as any, entry as any); + expect(result).to.have.length(1); + expect(result[0]).to.have.property('treeStr'); + expect(result[0].missingRefs).to.deep.include({ uid: 'ext_f', extension_uid: 'missing-ext', type: 'Extension or Apps' }); + }); + fancy .stdout({ print: process.env.PRINT === 'true' || false }) .it('should flag file field with invalid asset UID', async ({}) => { @@ -1540,4 +1855,436 @@ describe('Entries module', () => { expect(callHelper('sys_assets', ['ct1'])).to.be.true; }); }); + + describe('jsonRefCheck entry ref and no-entry-uid branches', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('pushes to missingRefs and returns null when entry UID is not in entryMetaData', () => { + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).currentTitle = 'Test Entry'; + (ctInstance as any).missingRefs = { 'test-entry': [] }; + (ctInstance as any).entryMetaData = []; // entry not present + + const schema = { + uid: 'json_rte', + display_name: 'JSON RTE', + data_type: 'richtext', + reference_to: ['ct1'], + }; + const child = { + type: 'embed', + uid: 'child-uid', + attrs: { 'entry-uid': 'missing-uid', 'content-type-uid': 'ct1' }, + children: [], + }; + const tree: Record[] = []; + + const result = (ctInstance as any).jsonRefCheck(tree, schema, child); + + expect(result).to.be.null; + expect((ctInstance as any).missingRefs['test-entry']).to.have.length(1); + expect((ctInstance as any).missingRefs['test-entry'][0].missingRefs).to.deep.include({ + uid: 'missing-uid', + 'content-type-uid': 'ct1', + }); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns true when entry UID is in entryMetaData and isRefContentTypeAllowed (valid ref)', () => { + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).missingRefs = { 'test-entry': [] }; + (ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct1' }]; + + const schema = { + uid: 'json_rte', + display_name: 'JSON RTE', + data_type: 'richtext', + reference_to: ['ct1'], + }; + const child = { + type: 'embed', + uid: 'child-uid', + attrs: { 'entry-uid': 'blt123', 'content-type-uid': 'ct1' }, + children: [], + }; + const tree: Record[] = []; + + const result = (ctInstance as any).jsonRefCheck(tree, schema, child); + + expect(result).to.be.true; + expect((ctInstance as any).missingRefs['test-entry']).to.have.length(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns true when child has no entry-uid (no entry UID in JSON child)', () => { + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'test-entry'; + (ctInstance as any).missingRefs = { 'test-entry': [] }; + (ctInstance as any).entryMetaData = []; + + const schema = { + uid: 'json_rte', + display_name: 'JSON RTE', + data_type: 'richtext', + }; + const child = { + type: 'embed', + uid: 'child-uid', + attrs: {}, // no entry-uid + children: [], + }; + const tree: Record[] = []; + + const result = (ctInstance as any).jsonRefCheck(tree, schema, child); + + expect(result).to.be.true; + expect((ctInstance as any).missingRefs['test-entry']).to.have.length(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns true and does not push when entry ref is valid (covers Entry reference is valid log)', () => { + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).missingRefs = { e1: [] }; + (ctInstance as any).entryMetaData = [{ uid: 'valid-uid', ctUid: 'page_0' }]; + const schema = { uid: 'rte', display_name: 'RTE', data_type: 'richtext', reference_to: ['page_0'] }; + const child = { + type: 'reference', + uid: 'c1', + attrs: { 'entry-uid': 'valid-uid', 'content-type-uid': 'page_0' }, + children: [], + }; + const result = (ctInstance as any).jsonRefCheck([], schema, child); + expect(result).to.be.true; + expect((ctInstance as any).missingRefs.e1).to.have.length(0); + }); + }); + + describe('prepareEntryMetaData', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('loads locales, environments, and entry metadata from mock contents', async () => { + const ctInstance = new Entries(constructorParam); + await ctInstance.prepareEntryMetaData(); + + expect(ctInstance.entryMetaData).to.be.an('array'); + expect(ctInstance.environments).to.be.an('array'); + expect(ctInstance.locales).to.be.an('array'); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('loads only master locales when additional locales file is missing', async () => { + if ((fs.existsSync as any).restore) (fs.existsSync as any).restore(); + const realExists = fs.existsSync.bind(fs); + Sinon.stub(fs, 'existsSync').callsFake((path: fs.PathLike) => { + const p = String(path); + if (p.includes('locales.json') && !p.includes('master-locale')) return false; + return realExists(path); + }); + try { + const ctInstance = new Entries(constructorParam); + await ctInstance.prepareEntryMetaData(); + expect(ctInstance.locales).to.be.an('array'); + expect(ctInstance.entryMetaData).to.be.an('array'); + } finally { + (fs.existsSync as any).restore?.(); + } + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('records empty title and no-title entries and pushes to entryMetaData', async () => { + if ((fs.existsSync as any).restore) (fs.existsSync as any).restore(); + if ((fs.readFileSync as any).restore) (fs.readFileSync as any).restore(); + const fullSchema = cloneDeep(require('../mock/contents/content_types/schema.json')); + const page1 = fullSchema.find((c: any) => c.uid === 'page_1'); + const emptyTitleCt = page1 ? { ...page1, uid: 'empty_title_ct' } : fullSchema[0]; + const param = { + ...constructorParam, + ctSchema: [emptyTitleCt], + config: { ...constructorParam.config }, + }; + const ctInstance = new Entries(param); + await ctInstance.prepareEntryMetaData(); + const missingTitleFields = (ctInstance as any).missingTitleFields; + expect(missingTitleFields).to.be.an('object'); + expect(missingTitleFields['entry-empty-title']).to.deep.include({ + 'Entry UID': 'entry-empty-title', + 'Content Type UID': 'empty_title_ct', + Locale: 'en-us', + }); + const metaNoTitle = ctInstance.entryMetaData.find((m: any) => m.uid === 'entry-no-title'); + expect(metaNoTitle).to.be.ok; + expect(metaNoTitle!.title).to.be.undefined; + const metaEmpty = ctInstance.entryMetaData.find((m: any) => m.uid === 'entry-empty-title'); + expect(metaEmpty).to.be.ok; + expect(ctInstance.entryMetaData.length).to.be.at.least(2); + }); + }); + + describe('findNotPresentSelectField', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('initializes field as empty array when field is null or undefined', () => { + const ctInstance = new Entries(constructorParam); + const choices = { choices: [{ value: 'a' }, { value: 'b' }] }; + const result = (ctInstance as any).findNotPresentSelectField(null, choices); + expect(result.filteredFeild).to.eql([]); + expect(result.notPresent).to.eql([]); + }); + }); + + describe('fixMissingReferences uncovered branches', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('parses entry when entry is string (JSON)', () => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).currentTitle = 'Entry 1'; + (ctInstance as any).missingRefs = { e1: [] }; + (ctInstance as any).entryMetaData = [{ uid: 'blt123', ctUid: 'ct1' }]; + const field = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1'] }; + const entry = '[{"uid":"blt123","_content_type_uid":"ct1"}]'; + const tree: Record[] = []; + const result = ctInstance.fixMissingReferences(tree, field as any, entry as any); + expect(result).to.be.an('array'); + expect(result.length).to.equal(1); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('handles blt reference when ref missing and reference_to single', () => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).currentTitle = 'E1'; + (ctInstance as any).missingRefs = { e1: [] }; + (ctInstance as any).entryMetaData = []; + const field = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1'] }; + const entry = ['blt999']; + const tree: Record[] = []; + const result = ctInstance.fixMissingReferences(tree, field as any, entry as any); + expect((ctInstance as any).missingRefs.e1).to.have.length(1); + expect((ctInstance as any).missingRefs.e1[0].missingRefs).to.deep.include({ uid: 'blt999', _content_type_uid: 'ct1' }); + expect(result.filter((r: any) => r != null)).to.have.length(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('records no missing references when all refs valid', () => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).currentTitle = 'E1'; + (ctInstance as any).missingRefs = { e1: [] }; + (ctInstance as any).entryMetaData = [{ uid: 'blt1', ctUid: 'ct1' }]; + const field = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1'] }; + const entry = [{ uid: 'blt1', _content_type_uid: 'ct1' }]; + const tree: Record[] = []; + const result = ctInstance.fixMissingReferences(tree, field as any, entry); + expect(result).to.have.length(1); + expect((ctInstance as any).missingRefs.e1).to.have.length(0); + expect(result[0].uid).to.equal('blt1'); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('keeps blt reference when found in entryMetaData and content type allowed', () => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).currentTitle = 'E1'; + (ctInstance as any).missingRefs = { e1: [] }; + (ctInstance as any).entryMetaData = [{ uid: 'blt1', ctUid: 'ct1' }]; + const field = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1'] }; + const entry = ['blt1']; + const tree: Record[] = []; + const result = ctInstance.fixMissingReferences(tree, field as any, entry as any); + expect(result).to.have.length(1); + expect(result[0]).to.deep.include({ uid: 'blt1', _content_type_uid: 'ct1' }); + expect((ctInstance as any).missingRefs.e1).to.have.length(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('pushes fullRef when reference_to has multiple and ref has wrong content type', () => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).currentTitle = 'E1'; + (ctInstance as any).missingRefs = { e1: [] }; + (ctInstance as any).entryMetaData = [{ uid: 'ref-uid', ctUid: 'ct3' }]; + const field = { uid: 'ref', display_name: 'Ref', data_type: 'reference', reference_to: ['ct1', 'ct2'] }; + const fullRef = { uid: 'ref-uid', _content_type_uid: 'ct3' }; + const entry = [fullRef]; + const tree: Record[] = []; + ctInstance.fixMissingReferences(tree, field as any, entry); + expect((ctInstance as any).missingRefs.e1).to.have.length(1); + expect((ctInstance as any).missingRefs.e1[0].missingRefs).to.have.length(1); + expect((ctInstance as any).missingRefs.e1[0].missingRefs[0]).to.deep.equal(fullRef); + }); + }); + + describe('modularBlockRefCheck invalid keys with fix', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('deletes invalid block key when fix is true', () => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).currentTitle = 'E1'; + (ctInstance as any).missingRefs = { e1: [] }; + const blocks = [{ uid: 'block_1', title: 'Block 1', schema: [] }]; + const entryBlock = { block_1: {}, invalid_key: {} }; + const tree: Record[] = []; + const result = (ctInstance as any).modularBlockRefCheck(tree, blocks, entryBlock, 0); + expect(result.invalid_key).to.be.undefined; + expect((ctInstance as any).missingRefs.e1).to.have.length(1); + }); + }); + + describe('fixGroupField', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('processes array group field entry when entry is array', () => { + if ((Entries.prototype.runFixOnSchema as any).restore) (Entries.prototype.runFixOnSchema as any).restore(); + Sinon.stub(Entries.prototype, 'runFixOnSchema').callsFake((_t: any, _s: any, e: any) => e); + const ctInstance = new Entries(constructorParam); + const field = { uid: 'gf', display_name: 'GF', schema: [{ uid: 'f1', display_name: 'F1' }] }; + const entry = [{ f1: 'v1' }]; + const result = (ctInstance as any).fixGroupField([], field, entry); + expect(result).to.eql(entry); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('processes single group field entry when entry is not array', () => { + if ((Entries.prototype.runFixOnSchema as any).restore) (Entries.prototype.runFixOnSchema as any).restore(); + Sinon.stub(Entries.prototype, 'runFixOnSchema').callsFake((_t: any, _s: any, e: any) => e); + const ctInstance = new Entries(constructorParam); + const field = { uid: 'gf', display_name: 'GF', schema: [{ uid: 'f1', display_name: 'F1' }] }; + const entry = { f1: 'v1' }; + const result = (ctInstance as any).fixGroupField([], field, entry); + expect(result).to.eql(entry); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('skips fixes when group field has no schema', () => { + const ctInstance = new Entries(constructorParam); + const field = { uid: 'gf', display_name: 'GF', schema: [] }; + const entry = { f1: 'v1' }; + const result = (ctInstance as any).fixGroupField([], field, entry); + expect(result).to.eql(entry); + }); + }); + + describe('fixMissingExtensionOrApp', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('deletes entry field when extension missing and fix true', () => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).currentTitle = 'E1'; + (ctInstance as any).missingRefs = { e1: [] }; + ctInstance.extensions = []; + const field = { uid: 'ext_f', display_name: 'Ext', data_type: 'extension' }; + const entry: Record = { ext_f: { metadata: { extension_uid: 'missing_ext' } } }; + (ctInstance as any).fixMissingExtensionOrApp([], field, entry); + expect(entry.ext_f).to.be.undefined; + expect((ctInstance as any).missingRefs.e1).to.have.length(1); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('logs when no extension data for field', () => { + const ctInstance = new Entries(constructorParam); + ctInstance.extensions = ['ext1']; + const field = { uid: 'ext_f', display_name: 'Ext', data_type: 'extension' }; + const entry: Record = {}; + (ctInstance as any).fixMissingExtensionOrApp([], field, entry); + expect(entry.ext_f).to.be.undefined; + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('keeps field when extension is valid', () => { + const ctInstance = new Entries({ ...constructorParam, fix: true }); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).currentTitle = 'E1'; + (ctInstance as any).missingRefs = { e1: [] }; + ctInstance.extensions = ['valid-ext']; + const field = { uid: 'ext_f', display_name: 'Ext', data_type: 'extension' }; + const entry: Record = { ext_f: { metadata: { extension_uid: 'valid-ext' } } }; + (ctInstance as any).fixMissingExtensionOrApp([], field, entry); + expect(entry.ext_f).to.be.ok; + expect((ctInstance as any).missingRefs.e1).to.have.length(0); + }); + }); + + describe('fixModularBlocksReferences', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('fixes modular blocks and filters empty', () => { + if ((Entries.prototype.modularBlockRefCheck as any).restore) (Entries.prototype.modularBlockRefCheck as any).restore(); + if ((Entries.prototype.runFixOnSchema as any).restore) (Entries.prototype.runFixOnSchema as any).restore(); + Sinon.stub(Entries.prototype, 'modularBlockRefCheck').callsFake((_t: any, blocks: any, entryBlock: any) => { + const key = blocks?.[0]?.uid || 'b1'; + return { [key]: entryBlock?.[key] || {} }; + }); + Sinon.stub(Entries.prototype, 'runFixOnSchema').callsFake((_t: any, _s: any, e: any) => e); + const ctInstance = new Entries(constructorParam); + const blocks = [{ uid: 'b1', title: 'B1', schema: [{ uid: 'f1' }] }]; + const entry = [{ b1: { f1: 'v1' } }]; + const result = (ctInstance as any).fixModularBlocksReferences([], blocks, entry); + expect(result).to.be.an('array'); + }); + }); + + describe('fixJsonRteMissingReferences', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns entry when entry has no children', () => { + const ctInstance = new Entries(constructorParam); + const field = { uid: 'rte', display_name: 'RTE', data_type: 'richtext' }; + const entry = { type: 'doc', children: [] }; + const result = (ctInstance as any).fixJsonRteMissingReferences([], field, entry); + expect(result).to.eql(entry); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('processes array entry by mapping over each child', () => { + const ctInstance = new Entries(constructorParam); + const field = { uid: 'rte', display_name: 'RTE', data_type: 'richtext' }; + const child1 = { type: 'p', uid: 'c1', children: [] }; + const child2 = { type: 'reference', uid: 'c2', children: [] }; + const entry = [child1, child2]; + const result = (ctInstance as any).fixJsonRteMissingReferences([], field, entry); + expect(result).to.be.an('array'); + expect(result).to.have.length(2); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('filters out invalid refs and recursively fixes children with children', () => { + if ((Entries.prototype.jsonRefCheck as any).restore) (Entries.prototype.jsonRefCheck as any).restore(); + Sinon.stub(Entries.prototype, 'jsonRefCheck').callsFake(function (_tree: any, _field: any, child: any) { + return (child as any).uid !== 'invalid' ? true : null; + }); + const ctInstance = new Entries(constructorParam); + (ctInstance as any).currentUid = 'e1'; + (ctInstance as any).entryMetaData = [{ uid: 'valid', ctUid: 'ct1' }]; + const field = { uid: 'rte', display_name: 'RTE', data_type: 'richtext', reference_to: ['ct1'] }; + const validChild = { type: 'reference', uid: 'valid', attrs: { 'entry-uid': 'valid' }, children: [] }; + const invalidChild = { type: 'reference', uid: 'invalid', attrs: {}, children: [] }; + const nestedChild = { type: 'p', uid: 'nested', children: [{ type: 'text', text: 'x' }] }; + const entry = { type: 'doc', children: [validChild, invalidChild, nestedChild] }; + const result = (ctInstance as any).fixJsonRteMissingReferences([], field, entry); + expect((result as any).children).to.have.length(2); + expect((result as any).children.filter((c: any) => c?.uid === 'invalid')).to.have.length(0); + (Entries.prototype.jsonRefCheck as any).restore(); + }); + }); }); diff --git a/packages/contentstack-audit/test/unit/modules/extensions.test.ts b/packages/contentstack-audit/test/unit/modules/extensions.test.ts index bb07376a4..ac6a443a8 100644 --- a/packages/contentstack-audit/test/unit/modules/extensions.test.ts +++ b/packages/contentstack-audit/test/unit/modules/extensions.test.ts @@ -1,3 +1,4 @@ +import fs from 'fs'; import { resolve } from 'path'; import { fancy } from 'fancy-test'; import { expect } from 'chai'; @@ -387,4 +388,59 @@ describe('Extensions scope containing content_types uids', () => { }, ); }); + + describe('fixExtensionsScope single confirmation', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(cliux, 'confirm', async () => true) + .it('fixExtensionsScope asks for confirmation once when multiple extensions would be deleted', async () => { + const ext = new Extensions({ + moduleName: 'extensions', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + }), + fix: true, + }); + (ext as any).extensionsPath = resolve(__dirname, '..', 'mock', 'contents', 'extensions', 'extensions.json'); + const twoOrphanExtensions: Extension[] = [ + { uid: 'orph1', title: 'Orphan 1', scope: { content_types: [] }, type: 'widget' } as any, + { uid: 'orph2', title: 'Orphan 2', scope: { content_types: [] }, type: 'widget' } as any, + ]; + ext.missingCts = new Set(['ct-missing']); + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readFileSync').returns( + JSON.stringify({ orph1: { uid: 'orph1', title: 'Orphan 1', scope: { content_types: [] } }, orph2: { uid: 'orph2', title: 'Orphan 2', scope: { content_types: [] } } }), + ); + const confirmStub = sinon.stub(cliux, 'confirm').resolves(true); + sinon.stub(ext, 'writeFixContent').resolves(); + await (ext as any).fixExtensionsScope(twoOrphanExtensions); + expect(confirmStub.callCount).to.equal(1); + (fs.existsSync as any).restore?.(); + (fs.readFileSync as any).restore?.(); + }); + }); + + describe('writeFixContent with preConfirmed', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('writeFixContent does not prompt when preConfirmed is true', async () => { + const ext = new Extensions({ + moduleName: 'extensions', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + }), + fix: true, + }); + const writeStub = sinon.stub(fs, 'writeFileSync'); + const confirmStub = sinon.stub(cliux, 'confirm'); + await (ext as any).writeFixContent({ ext1: {} as Extension }, true); + expect(writeStub.called).to.be.true; + expect(confirmStub.called).to.be.false; + writeStub.restore(); + }); + }); }); diff --git a/packages/contentstack-audit/test/unit/modules/field-rules.test.ts b/packages/contentstack-audit/test/unit/modules/field-rules.test.ts index 6999ab5a3..b66cb1d32 100644 --- a/packages/contentstack-audit/test/unit/modules/field-rules.test.ts +++ b/packages/contentstack-audit/test/unit/modules/field-rules.test.ts @@ -1,6 +1,6 @@ import fs from 'fs'; import sinon from 'sinon'; -import { resolve } from 'path'; +import { resolve, join } from 'path'; import { fancy } from 'fancy-test'; import { expect } from 'chai'; import cloneDeep from 'lodash/cloneDeep'; @@ -116,6 +116,45 @@ describe('Field Rules', () => { expect(logSpy.callCount).to.be.equals(1); }); + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(FieldRule.prototype, 'prepareEntryMetaData', async () => {}) + .stub(FieldRule.prototype, 'prerequisiteData', async () => {}) + .stub(FieldRule.prototype, 'lookForReference', async () => {}) + .stub(FieldRule.prototype, 'fixFieldRules', () => {}) + .stub(FieldRule.prototype, 'validateFieldRules', () => {}) + .it('should create progress and call progressManager.tick when totalCount > 0', async () => { + const frInstance = new FieldRule(constructorParam); + (frInstance as any).createSimpleProgress = sinon.stub().callsFake(function (this: any) { + const progress = { updateStatus: sinon.stub(), tick: sinon.stub(), complete: sinon.stub() }; + this.progressManager = progress; + return progress; + }); + await frInstance.run(5); + expect((frInstance as any).createSimpleProgress.calledWith('field-rules', 5)).to.be.true; + const progress = (frInstance as any).createSimpleProgress.firstCall.returnValue; + expect(progress.updateStatus.calledWith('Validating field rules...')).to.be.true; + expect(progress.tick.callCount).to.equal(frInstance.ctSchema!.length); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(FieldRule.prototype, 'prerequisiteData', async () => {}) + .stub(FieldRule.prototype, 'lookForReference', async () => {}) + .stub(FieldRule.prototype, 'fixFieldRules', () => {}) + .stub(FieldRule.prototype, 'validateFieldRules', () => {}) + .it('should call completeProgress(false) and rethrow when run() throws', async () => { + const frInstance = new FieldRule(constructorParam); + sinon.stub(frInstance, 'prepareEntryMetaData').rejects(new Error('prepare failed')); + const completeSpy = sinon.spy(frInstance as any, 'completeProgress'); + try { + await frInstance.run(); + } catch (e: any) { + expect(e.message).to.equal('prepare failed'); + } + expect(completeSpy.calledWith(false, 'prepare failed')).to.be.true; + }); + fancy .stub(fs, 'rmSync', () => {}) .stdout({ print: process.env.PRINT === 'true' || false }) @@ -140,6 +179,98 @@ describe('Field Rules', () => { }); }); + describe('validateFieldRules', () => { + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('logs valid when target_field is in schemaMap', () => { + const frInstance = new FieldRule(constructorParam); + frInstance.schemaMap = ['title', 'desc']; + const schema = { + uid: 'ct_1', + title: 'CT One', + field_rules: [ + { + conditions: [], + actions: [{ action: 'show', target_field: 'title' }], + rule_type: 'entry', + }, + ], + } as any; + frInstance.validateFieldRules(schema); + expect((frInstance as any).missingRefs['ct_1'] || []).to.have.length(0); + }); + }); + + describe('prerequisiteData', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('handles error when loading extensions file throws', async () => { + const frInstance = new FieldRule(constructorParam); + const extPath = resolve(constructorParam.config.basePath, 'extensions', 'extensions.json'); + sinon.stub(fs, 'existsSync').callsFake((p: fs.PathLike) => String(p) === extPath); + sinon.stub(fs, 'readFileSync').callsFake(() => { + throw new Error('read error'); + }); + await frInstance.prerequisiteData(); + expect(frInstance.extensions).to.deep.equal([]); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('loads marketplace apps and pushes extension UIDs', async () => { + const frInstance = new FieldRule(constructorParam); + const marketPath = resolve(constructorParam.config.basePath, 'marketplace_apps', 'marketplace_apps.json'); + sinon.stub(fs, 'existsSync').callsFake((p: fs.PathLike) => String(p) === marketPath); + const marketplaceData = [ + { + uid: 'app1', + ui_location: { + locations: [{ meta: { extension_uid: 'ext_1' } }, { meta: { extension_uid: 'ext_2' } }], + }, + }, + ]; + sinon.stub(fs, 'readFileSync').callsFake((p: fs.PathOrFileDescriptor) => { + if (String(p) === marketPath) return JSON.stringify(marketplaceData); + return '{}'; + }); + await frInstance.prerequisiteData(); + expect(frInstance.extensions).to.include('ext_1'); + expect(frInstance.extensions).to.include('ext_2'); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('handles error when loading marketplace apps file throws', async () => { + const frInstance = new FieldRule(constructorParam); + const marketPath = resolve(constructorParam.config.basePath, 'marketplace_apps', 'marketplace_apps.json'); + sinon.stub(fs, 'existsSync').callsFake((p: fs.PathLike) => String(p) === marketPath); + sinon.stub(fs, 'readFileSync').callsFake(() => { + throw new Error('marketplace read error'); + }); + await frInstance.prerequisiteData(); + expect(frInstance.extensions).to.deep.equal([]); + }); + }); + + describe('fixFieldRules', () => { + fancy.stdout({ print: process.env.PRINT === 'true' || false }).it('keeps valid action and logs info when target_field in schemaMap', () => { + const frInstance = new FieldRule({ ...constructorParam, fix: true }); + frInstance.schemaMap = ['title']; + const schema = { + uid: 'ct_1', + title: 'CT One', + field_rules: [ + { + conditions: [{ operand_field: 'title', operator: 'equals', value: 'x' }], + actions: [{ action: 'show', target_field: 'title' }], + rule_type: 'entry', + }, + ], + } as any; + frInstance.fixFieldRules(schema); + expect(schema.field_rules).to.have.length(1); + expect(schema.field_rules[0].actions).to.have.length(1); + }); + }); + describe('writeFixContent method', () => { fancy .stdout({ print: process.env.PRINT === 'true' || false }) @@ -163,6 +294,78 @@ describe('Field Rules', () => { await ctInstance.writeFixContent(); expect(spy.callCount).to.be.equals(1); }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('skips file write when user declines confirmation', async () => { + sinon.replace(cliux, 'confirm', async () => false); + const ctInstance = new FieldRule({ ...constructorParam, fix: true }); + (ctInstance as any).schema = [{ uid: 'ct_1', title: 'CT' }]; + const writeSpy = sinon.stub(fs, 'writeFileSync'); + await ctInstance.writeFixContent(); + expect(writeSpy.callCount).to.equal(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'writeFileSync', () => {}) + .it('skips confirmation when copy-dir or external-config skipConfirm is set', async () => { + const ctInstance = new FieldRule({ + ...constructorParam, + fix: true, + config: { + ...constructorParam.config, + flags: { 'copy-dir': true } as any, + }, + }); + (ctInstance as any).schema = [{ uid: 'ct_1', title: 'CT' }]; + const confirmSpy = sinon.stub(cliux, 'confirm').resolves(false); + await ctInstance.writeFixContent(); + expect(confirmSpy.callCount).to.equal(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('skips file write when fix mode is disabled', async () => { + const ctInstance = new FieldRule({ ...constructorParam, fix: false }); + (ctInstance as any).schema = [{ uid: 'ct_1', title: 'CT' }]; + const writeSpy = sinon.stub(fs, 'writeFileSync'); + await ctInstance.writeFixContent(); + expect(writeSpy.callCount).to.equal(0); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'writeFileSync', () => {}) + .stub(cliux, 'confirm', async () => true) + .it('skips schema with missing uid and writes rest', async () => { + const ctInstance = new FieldRule({ ...constructorParam, fix: true }); + (ctInstance as any).schema = [ + { uid: undefined, title: 'NoUid' }, + { uid: 'ct_1', title: 'CT One' }, + ]; + const writeSpy = sinon.spy(fs, 'writeFileSync'); + await ctInstance.writeFixContent(); + expect(writeSpy.callCount).to.equal(1); + expect(writeSpy.calledWith(join(ctInstance.folderPath, 'ct_1.json'), sinon.match.string)).to.be.true; + }); + }); + + describe('prepareEntryMetaData', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('logs when additional locales file not found', async () => { + const frInstance = new FieldRule(constructorParam); + const localesFolderPath = resolve(constructorParam.config.basePath, frInstance.config.moduleConfig.locales.dirName); + const localesPath = join(localesFolderPath, frInstance.config.moduleConfig.locales.fileName); + const origExists = fs.existsSync; + sinon.stub(fs, 'existsSync').callsFake((p: fs.PathLike) => { + if (String(p) === localesPath) return false; + return origExists.call(fs, p); + }); + await frInstance.prepareEntryMetaData(); + expect(frInstance.locales.length).to.be.greaterThanOrEqual(0); + }); }); describe('Test Other methods', () => { diff --git a/packages/contentstack-audit/test/unit/modules/workflow.test.ts b/packages/contentstack-audit/test/unit/modules/workflow.test.ts index 69ad1e73c..d5a942023 100644 --- a/packages/contentstack-audit/test/unit/modules/workflow.test.ts +++ b/packages/contentstack-audit/test/unit/modules/workflow.test.ts @@ -1,9 +1,9 @@ import fs from 'fs'; -import { resolve } from 'path'; +import { join, resolve } from 'path'; import { fancy } from 'fancy-test'; import { expect } from 'chai'; import cloneDeep from 'lodash/cloneDeep'; -import { ux } from '@contentstack/cli-utilities'; +import { ux, cliux } from '@contentstack/cli-utilities'; import sinon from 'sinon'; import config from '../../../src/config'; @@ -17,11 +17,25 @@ describe('Workflows', () => { // Mock the logger for all tests sinon.stub(require('@contentstack/cli-utilities'), 'log').value(mockLogger); }); - + afterEach(() => { sinon.restore(); }); - + + describe('validateModules', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('returns default workflows when moduleName not in moduleConfig', () => { + const wf = new Workflows({ + moduleName: 'workflows' as any, + ctSchema: [], + config: Object.assign(config, { basePath: resolve(__dirname, '..', 'mock', 'contents'), flags: {} }), + }); + const result = (wf as any).validateModules('invalid-module' as any, config.moduleConfig); + expect(result).to.equal('workflows'); + }); + }); + describe('run method with invalid path for workflows', () => { const wf = new Workflows({ moduleName: 'workflows', @@ -93,6 +107,67 @@ describe('Workflows', () => { ); }); + describe('run method with totalCount', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('creates progress when totalCount is provided', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: cloneDeep(require('../mock/contents/workflows/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + }), + }); + const createProgress = sinon.spy(wf as any, 'createSimpleProgress'); + await wf.run(5); + expect(createProgress.calledWith('workflows', 5)).to.be.true; + }); + }); + + describe('run method with no branch config', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('runs and hits no branch configuration path', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: cloneDeep(require('../mock/contents/workflows/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + branch: undefined, + }), + }); + const result = await wf.run(); + expect(result).to.be.an('array'); + }); + }); + + describe('run method throws', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('completeProgress false and rethrows when run throws', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: cloneDeep(require('../mock/contents/workflows/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + }), + }); + sinon.stub(fs, 'readFileSync').throws(new Error('read failed')); + const completeProgress = sinon.spy(wf as any, 'completeProgress'); + try { + await wf.run(); + } catch (e: any) { + expect(completeProgress.calledWith(false, 'read failed')).to.be.true; + expect(e.message).to.equal('read failed'); + } finally { + (fs.readFileSync as any).restore?.(); + } + }); + }); + describe('run method with audit fix for workflows with valid path and empty ctSchema', () => { const wf = new Workflows({ moduleName: 'workflows', @@ -111,6 +186,7 @@ describe('Workflows', () => { .stub(wf, 'WriteFileSync', () => {}) .stub(wf, 'writeFixContent', () => {}) .it('the run function should run and flow should go till fixWorkflowSchema', async () => { + wf.config.branch = 'development'; const fixedReference = await wf.run(); expect(fixedReference).eql([ { @@ -147,4 +223,186 @@ describe('Workflows', () => { ]); }); }); + + describe('fixWorkflowSchema', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(cliux, 'confirm', async () => true) + .it('hits no branch configuration when config.branch is missing', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: cloneDeep(require('../mock/contents/workflows/ctSchema.json')), + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + branch: undefined, + }), + fix: true, + }); + wf.workflowPath = join(resolve(__dirname, '..', 'mock', 'contents'), 'workflows', 'workflows.json'); + wf.workflowSchema = values(JSON.parse(fs.readFileSync(wf.workflowPath, 'utf8'))); + wf.missingCts = new Set(['ct45', 'ct14']); + const writeStub = sinon.stub(wf, 'writeFixContent').resolves(); + await (wf as any).fixWorkflowSchema(); + expect(writeStub.called).to.be.true; + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(cliux, 'confirm', async () => true) + .it('deletes workflow when no valid content types and user confirms', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + }), + fix: true, + }); + wf.workflowSchema = [{ uid: 'orphan', name: 'Orphan', content_types: ['ct-missing'], branches: [] } as any]; + wf.missingCts = new Set(['ct-missing']); + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readFileSync').returns(JSON.stringify({ orphan: { uid: 'orphan', name: 'Orphan', content_types: ['ct-missing'] } })); + const writeStub = sinon.stub(wf, 'writeFixContent').resolves(); + await (wf as any).fixWorkflowSchema(); + expect(writeStub.called).to.be.true; + const writeArg = writeStub.firstCall.args[0]; + expect(writeArg.orphan).to.be.undefined; + (fs.existsSync as any).restore?.(); + (fs.readFileSync as any).restore?.(); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(cliux, 'confirm', async () => true) + .it('fixWorkflowSchema asks for confirmation once when multiple workflows would be deleted', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + }), + fix: true, + }); + wf.workflowSchema = [ + { uid: 'orphan1', name: 'Orphan 1', content_types: ['ct-missing'], branches: [] } as any, + { uid: 'orphan2', name: 'Orphan 2', content_types: ['ct-missing'], branches: [] } as any, + ]; + wf.missingCts = new Set(['ct-missing']); + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readFileSync').returns( + JSON.stringify({ + orphan1: { uid: 'orphan1', name: 'Orphan 1', content_types: ['ct-missing'] }, + orphan2: { uid: 'orphan2', name: 'Orphan 2', content_types: ['ct-missing'] }, + }), + ); + const confirmStub = sinon.stub(cliux, 'confirm').resolves(true); + sinon.stub(wf, 'writeFixContent').resolves(); + await (wf as any).fixWorkflowSchema(); + expect(confirmStub.callCount).to.equal(1); + (fs.existsSync as any).restore?.(); + (fs.readFileSync as any).restore?.(); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(cliux, 'confirm', async () => false) + .it('keeps workflow when no valid content types and user declines', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: [], + config: Object.assign(config, { basePath: resolve(__dirname, '..', 'mock', 'contents'), flags: {} }), + fix: true, + }); + wf.workflowSchema = [{ uid: 'keep', name: 'Keep', content_types: ['ct-missing'], branches: [] } as any]; + wf.missingCts = new Set(['ct-missing']); + sinon.stub(fs, 'existsSync').returns(true); + sinon.stub(fs, 'readFileSync').returns(JSON.stringify({ keep: { uid: 'keep', name: 'Keep', content_types: ['ct-missing'] } })); + const writeStub = sinon.stub(wf, 'writeFixContent').resolves(); + await (wf as any).fixWorkflowSchema(); + expect(writeStub.called).to.be.true; + expect(writeStub.firstCall.args[0].keep).to.be.ok; + (fs.existsSync as any).restore?.(); + (fs.readFileSync as any).restore?.(); + }); + }); + + describe('writeFixContent', () => { + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(cliux, 'confirm', async () => true) + .it('writes file when fix true and user confirms', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + }), + fix: true, + }); + const writeStub = sinon.stub(fs, 'writeFileSync'); + await (wf as any).writeFixContent({ wf1: {} }); + expect(writeStub.called).to.be.true; + writeStub.restore(); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('skips write when fix is false', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + }), + fix: false, + }); + const writeStub = sinon.stub(fs, 'writeFileSync'); + await (wf as any).writeFixContent({ wf1: {} }); + expect(writeStub.called).to.be.false; + writeStub.restore(); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('writes when fix true and copy-dir flag set', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: { 'copy-dir': true }, + }), + fix: true, + }); + const writeStub = sinon.stub(fs, 'writeFileSync'); + await (wf as any).writeFixContent({ wf1: {} }); + expect(writeStub.called).to.be.true; + writeStub.restore(); + }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .it('writeFixContent does not prompt when preConfirmed is true', async () => { + const wf = new Workflows({ + moduleName: 'workflows', + ctSchema: [], + config: Object.assign(config, { + basePath: resolve(__dirname, '..', 'mock', 'contents'), + flags: {}, + }), + fix: true, + }); + const writeStub = sinon.stub(fs, 'writeFileSync'); + const confirmStub = sinon.stub(cliux, 'confirm'); + await (wf as any).writeFixContent({ wf1: {} }, true); + expect(writeStub.called).to.be.true; + expect(confirmStub.called).to.be.false; + writeStub.restore(); + }); + }); }); From 4fa7cb17063fb4b94e13b1865950958867527eff Mon Sep 17 00:00:00 2001 From: raj pandey Date: Sun, 22 Mar 2026 11:09:30 +0530 Subject: [PATCH 2/8] fix(seed): stop chdir before nested import --- packages/contentstack-bootstrap/package.json | 4 ++-- packages/contentstack-seed/package.json | 2 +- packages/contentstack-seed/src/seed/importer.ts | 2 -- .../test/seed/github/client.test.ts | 4 ++-- .../contentstack-seed/test/seed/importer.test.ts | 16 ---------------- .../test/seed/interactive.test.ts | 5 ++++- 6 files changed, 9 insertions(+), 24 deletions(-) diff --git a/packages/contentstack-bootstrap/package.json b/packages/contentstack-bootstrap/package.json index 8ba005a79..1d641a871 100644 --- a/packages/contentstack-bootstrap/package.json +++ b/packages/contentstack-bootstrap/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-cm-bootstrap", "description": "Bootstrap contentstack apps", - "version": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "scripts": { @@ -16,7 +16,7 @@ "test:report": "nyc --reporter=lcov mocha \"test/**/*.test.js\"" }, "dependencies": { - "@contentstack/cli-cm-seed": "~2.0.0-beta.11", + "@contentstack/cli-cm-seed": "~2.0.0-beta.12", "@contentstack/cli-command": "~2.0.0-beta.3", "@contentstack/cli-utilities": "~2.0.0-beta.3", "@contentstack/cli-config": "~2.0.0-beta.4", diff --git a/packages/contentstack-seed/package.json b/packages/contentstack-seed/package.json index 90f34e703..8ba667100 100644 --- a/packages/contentstack-seed/package.json +++ b/packages/contentstack-seed/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-cm-seed", "description": "create a Stack from existing content types, entries, assets, etc.", - "version": "2.0.0-beta.11", + "version": "2.0.0-beta.12", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { diff --git a/packages/contentstack-seed/src/seed/importer.ts b/packages/contentstack-seed/src/seed/importer.ts index e3b8b2b9b..29dd133a2 100644 --- a/packages/contentstack-seed/src/seed/importer.ts +++ b/packages/contentstack-seed/src/seed/importer.ts @@ -1,5 +1,4 @@ import * as fs from 'fs'; -import * as process from 'process'; import * as path from 'path'; import ImportCommand from '@contentstack/cli-cm-import'; import { pathValidator, sanitizePath } from '@contentstack/cli-utilities'; @@ -29,6 +28,5 @@ export async function run(options: ImporterOptions) { ? ['-k', options.api_key, '-d', importPath, '--alias', options.alias!] : ['-k', options.api_key, '-d', importPath]; - process.chdir(options.tmpPath); await ImportCommand.run(args.concat('--skip-audit')); } diff --git a/packages/contentstack-seed/test/seed/github/client.test.ts b/packages/contentstack-seed/test/seed/github/client.test.ts index 4968b4050..59cca5f18 100644 --- a/packages/contentstack-seed/test/seed/github/client.test.ts +++ b/packages/contentstack-seed/test/seed/github/client.test.ts @@ -195,7 +195,7 @@ describe('GitHubClient', () => { const mockExtract = new Stream(); (zlib.createUnzip as jest.Mock) = jest.fn().mockReturnValue(mockUnzip); - (tar.extract as jest.Mock) = jest.fn().mockReturnValue(mockExtract); + (tar.extract as unknown as jest.Mock) = jest.fn().mockReturnValue(mockExtract); // Mock pipe chain mockStream.pipe = jest.fn().mockReturnValue(mockUnzip); @@ -222,7 +222,7 @@ describe('GitHubClient', () => { const mockExtract = new Stream(); (zlib.createUnzip as jest.Mock) = jest.fn().mockReturnValue(mockUnzip); - (tar.extract as jest.Mock) = jest.fn().mockReturnValue(mockExtract); + (tar.extract as unknown as jest.Mock) = jest.fn().mockReturnValue(mockExtract); mockStream.pipe = jest.fn().mockReturnValue(mockUnzip); mockUnzip.pipe = jest.fn().mockReturnValue(mockExtract); diff --git a/packages/contentstack-seed/test/seed/importer.test.ts b/packages/contentstack-seed/test/seed/importer.test.ts index 039100943..1527cb8d3 100644 --- a/packages/contentstack-seed/test/seed/importer.test.ts +++ b/packages/contentstack-seed/test/seed/importer.test.ts @@ -19,10 +19,6 @@ import ImportCommand from '@contentstack/cli-cm-import'; import * as path from 'node:path'; import * as cliUtilities from '@contentstack/cli-utilities'; -// Mock process.chdir -const mockChdir = jest.fn(); -jest.spyOn(process, 'chdir').mockImplementation(mockChdir); - describe('Importer', () => { const mockOptions = { master_locale: 'en-us', @@ -52,7 +48,6 @@ describe('Importer', () => { const expectedPath = path.resolve(mockOptions.tmpPath, 'stack'); expect(cliUtilities.pathValidator).toHaveBeenCalledWith(expectedPath); expect(cliUtilities.sanitizePath).toHaveBeenCalledWith(mockOptions.tmpPath); - expect(mockChdir).toHaveBeenCalledWith(mockOptions.tmpPath); expect(ImportCommand.run).toHaveBeenCalledWith(['-k', mockOptions.api_key, '-d', expectedPath, '--skip-audit']); }); @@ -124,7 +119,6 @@ describe('Importer', () => { const expectedPath = path.resolve(testPath, 'stack'); expect(cliUtilities.pathValidator).toHaveBeenCalledWith(expectedPath); - expect(mockChdir).toHaveBeenCalledWith(testPath); } }); @@ -159,16 +153,6 @@ describe('Importer', () => { expect(cliUtilities.pathValidator).toHaveBeenCalled(); }); - it('should change directory before running import', async () => { - await importer.run(mockOptions); - - // Verify chdir is called before ImportCommand.run - const chdirCallOrder = mockChdir.mock.invocationCallOrder[0]; - const importCallOrder = (ImportCommand.run as jest.Mock).mock.invocationCallOrder[0]; - - expect(chdirCallOrder).toBeLessThan(importCallOrder); - }); - it('should handle import command errors', async () => { const mockError = new Error('Import failed'); (ImportCommand.run as jest.Mock) = jest.fn().mockRejectedValue(mockError); diff --git a/packages/contentstack-seed/test/seed/interactive.test.ts b/packages/contentstack-seed/test/seed/interactive.test.ts index c587725cb..f3cf43d75 100644 --- a/packages/contentstack-seed/test/seed/interactive.test.ts +++ b/packages/contentstack-seed/test/seed/interactive.test.ts @@ -3,7 +3,10 @@ const mockInquirer = { prompt: jest.fn(), }; -jest.mock('inquirer', () => mockInquirer); +jest.mock('inquirer', () => ({ + __esModule: true, + default: mockInquirer, +})); import * as interactive from '../../src/seed/interactive'; import { Organization, Stack } from '../../src/seed/contentstack/client'; From 5345b1472d51d6982b096666c516eb24dbe984f8 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Sun, 22 Mar 2026 20:28:05 +0530 Subject: [PATCH 3/8] Fixed global field prompt when empty global fields --- .../src/modules/content-types.ts | 5 +++++ .../test/unit/modules/content-types.test.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/contentstack-audit/src/modules/content-types.ts b/packages/contentstack-audit/src/modules/content-types.ts index 79f23dfdd..6b79913c5 100644 --- a/packages/contentstack-audit/src/modules/content-types.ts +++ b/packages/contentstack-audit/src/modules/content-types.ts @@ -222,6 +222,11 @@ export default class ContentType extends BaseClass { let canWrite = true; if (!this.inMemoryFix && this.fix) { + if (Array.isArray(this.schema) && this.schema.length === 0) { + log.debug('No schemas to write, skipping writeFixContent', this.config.auditContext); + return; + } + 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); diff --git a/packages/contentstack-audit/test/unit/modules/content-types.test.ts b/packages/contentstack-audit/test/unit/modules/content-types.test.ts index b85dae300..991dea934 100644 --- a/packages/contentstack-audit/test/unit/modules/content-types.test.ts +++ b/packages/contentstack-audit/test/unit/modules/content-types.test.ts @@ -164,10 +164,24 @@ describe('Content types', () => { .it('should prompt and ask confirmation', async () => { sinon.replace(cliux, 'confirm', async () => false); const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).schema = constructorParam.ctSchema?.length ? [constructorParam.ctSchema[0]] : [{ uid: 'ct1', title: 'T' } as ContentTypeStruct]; const spy = sinon.spy(cliux, 'confirm'); await ctInstance.writeFixContent(); expect(spy.callCount).to.be.equals(1); }); + + fancy + .stdout({ print: process.env.PRINT === 'true' || false }) + .stub(fs, 'writeFileSync', () => {}) + .it('should not prompt or write when schema is empty in fix mode', async () => { + const ctInstance = new ContentType({ ...constructorParam, fix: true }); + (ctInstance as any).schema = []; + const confirmSpy = sinon.spy(cliux, 'confirm'); + const fsSpy = sinon.spy(fs, 'writeFileSync'); + await ctInstance.writeFixContent(); + expect(confirmSpy.callCount).to.be.equals(0); + expect(fsSpy.callCount).to.be.equals(0); + }); }); describe('lookForReference method', () => { From e17326b2fac708fd8d3c7cbd6f6e9238bb19c939 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 24 Mar 2026 23:21:05 +0530 Subject: [PATCH 4/8] feat: added support for the management token --- .../contentstack-export/src/config/index.ts | 1 + .../src/export/modules/stack.ts | 150 +++++++++++++----- .../src/types/default-config.ts | 1 + .../contentstack-export/src/types/index.ts | 1 + .../test/unit/export/modules/assets.test.ts | 1 + .../unit/export/modules/base-class.test.ts | 1 + .../test/unit/export/modules/stack.test.ts | 38 +++-- 7 files changed, 142 insertions(+), 51 deletions(-) diff --git a/packages/contentstack-export/src/config/index.ts b/packages/contentstack-export/src/config/index.ts index 14fe590b3..23de9a5a2 100644 --- a/packages/contentstack-export/src/config/index.ts +++ b/packages/contentstack-export/src/config/index.ts @@ -201,6 +201,7 @@ const config: DefaultConfig = { stack: { dirName: 'stack', fileName: 'stack.json', + invalidKeys: ['SYS_ACL', 'user_uids', 'owner_uid'], }, dependency: { entries: ['stack', 'locales', 'content-types'], diff --git a/packages/contentstack-export/src/export/modules/stack.ts b/packages/contentstack-export/src/export/modules/stack.ts index 5007235da..9e12aa84a 100644 --- a/packages/contentstack-export/src/export/modules/stack.ts +++ b/packages/contentstack-export/src/export/modules/stack.ts @@ -1,4 +1,5 @@ import find from 'lodash/find'; +import omit from 'lodash/omit'; import { resolve as pResolve } from 'node:path'; import { handleAndLogError, @@ -43,38 +44,35 @@ export default class ExportStack extends BaseClass { try { log.debug('Starting stack export process...', this.exportConfig.context); - // Initial analysis with loading spinner + // Initial analysis with loading spinner (skip getStack when using management token — no SDK snapshot) const [stackData] = await this.withLoadingSpinner('STACK: Analyzing stack configuration...', async () => { - const stackData = isAuthenticated() ? await this.getStack() : null; + const stackData = + this.exportConfig.management_token || !isAuthenticated() ? null : await this.getStack(); return [stackData]; }); // Create nested progress manager const progress = this.createNestedProgress(this.currentModuleName); - // Add processes based on configuration - let processCount = 0; - - if (stackData?.org_uid) { - log.debug(`Found organization UID: '${stackData.org_uid}'.`, this.exportConfig.context); - this.exportConfig.org_uid = stackData.org_uid; + const orgUid = stackData?.org_uid ?? stackData?.organization_uid; + if (orgUid) { + log.debug(`Found organization UID: '${orgUid}'.`, this.exportConfig.context); + this.exportConfig.org_uid = orgUid; this.exportConfig.sourceStackName = stackData.name; log.debug(`Set source stack name: ${stackData.name}`, this.exportConfig.context); } if (!this.exportConfig.management_token) { progress.addProcess(PROCESS_NAMES.STACK_SETTINGS, 1); - processCount++; } + progress.addProcess(PROCESS_NAMES.STACK_DETAILS, 1); if (!this.exportConfig.preserveStackVersion && !this.exportConfig.hasOwnProperty('master_locale')) { progress.addProcess(PROCESS_NAMES.STACK_LOCALE, 1); - processCount++; - } else if (this.exportConfig.preserveStackVersion) { - progress.addProcess(PROCESS_NAMES.STACK_DETAILS, 1); - processCount++; } + let stackDetailsExportResult: any; + // Execute processes if (!this.exportConfig.management_token) { progress @@ -85,11 +83,28 @@ export default class ExportStack extends BaseClass { ); await this.exportStackSettings(); progress.completeProcess(PROCESS_NAMES.STACK_SETTINGS, true); + + progress + .startProcess(PROCESS_NAMES.STACK_DETAILS) + .updateStatus( + PROCESS_STATUS[PROCESS_NAMES.STACK_DETAILS].EXPORTING, + PROCESS_NAMES.STACK_DETAILS, + ); + stackDetailsExportResult = await this.exportStack(stackData); + progress.completeProcess(PROCESS_NAMES.STACK_DETAILS, true); } else { log.info( 'Skipping stack settings export: Operation is not supported when using a management token.', this.exportConfig.context, ); + progress + .startProcess(PROCESS_NAMES.STACK_DETAILS) + .updateStatus( + PROCESS_STATUS[PROCESS_NAMES.STACK_DETAILS].EXPORTING, + PROCESS_NAMES.STACK_DETAILS, + ); + stackDetailsExportResult = await this.writeStackJsonFromConfigApiKeyOnly(); + progress.completeProcess(PROCESS_NAMES.STACK_DETAILS, true); } if (!this.exportConfig.preserveStackVersion && !this.exportConfig.hasOwnProperty('master_locale')) { @@ -110,17 +125,8 @@ export default class ExportStack extends BaseClass { this.completeProgress(true); return masterLocale; } else if (this.exportConfig.preserveStackVersion) { - progress - .startProcess(PROCESS_NAMES.STACK_DETAILS) - .updateStatus( - PROCESS_STATUS[PROCESS_NAMES.STACK_DETAILS].EXPORTING, - PROCESS_NAMES.STACK_DETAILS, - ); - const stackResult = await this.exportStack(); - progress.completeProcess(PROCESS_NAMES.STACK_DETAILS, true); - this.completeProgress(true); - return stackResult; + return stackDetailsExportResult; } else { log.debug('Locale locale already set, skipping locale fetch', this.exportConfig.context); } @@ -225,33 +231,36 @@ export default class ExportStack extends BaseClass { }); } - async exportStack(): Promise { + /** + * Reuse stack snapshot from `getStack()` when present so we do not call `stack.fetch()` twice + * (same GET /stacks payload as writing stack.json). Falls back to `this.stack.fetch()` otherwise. + */ + async exportStack(preloadedStack?: Record | null): Promise { log.debug(`Starting stack export for: '${this.exportConfig.apiKey}'...`, this.exportConfig.context); await fsUtil.makeDirectory(this.stackFolderPath); log.debug(`Created stack directory at: '${this.stackFolderPath}'`, this.exportConfig.context); - return this.stack - .fetch() - .then((resp: any) => { - const stackFilePath = pResolve(this.stackFolderPath, this.stackConfig.fileName); - log.debug(`Writing stack data to: '${stackFilePath}'`, this.exportConfig.context); - fsUtil.writeFile(stackFilePath, resp); - - // Track progress for stack export completion + if (this.isStackFetchPayload(preloadedStack)) { + log.debug('Reusing stack payload from analysis step (no extra stack.fetch).', this.exportConfig.context); + try { + return this.persistStackJsonPayload(preloadedStack); + } catch (error: any) { this.progressManager?.tick( - true, - `stack: ${this.exportConfig.apiKey}`, - null, + false, + 'stack export', + error?.message || PROCESS_STATUS[PROCESS_NAMES.STACK_DETAILS].FAILED, PROCESS_NAMES.STACK_DETAILS, ); + handleAndLogError(error, { ...this.exportConfig.context }); + return undefined; + } + } - log.success( - `Stack details exported successfully for stack ${this.exportConfig.apiKey}`, - this.exportConfig.context, - ); - log.debug('Stack export completed successfully.', this.exportConfig.context); - return resp; + return this.stack + .fetch() + .then((resp: any) => { + return this.persistStackJsonPayload(resp); }) .catch((error: any) => { log.debug(`Error occurred while exporting stack: ${this.exportConfig.apiKey}`, this.exportConfig.context); @@ -265,6 +274,65 @@ export default class ExportStack extends BaseClass { }); } + private isStackFetchPayload(data: unknown): data is Record { + return ( + typeof data === 'object' && + data !== null && + !Array.isArray(data) && + ('api_key' in data || 'uid' in data) + ); + } + + /** + * Management-token exports cannot use Stack CMA endpoints for full metadata; write api_key from config only. + */ + private async writeStackJsonFromConfigApiKeyOnly(): Promise<{ api_key: string }> { + if (!this.exportConfig.apiKey || typeof this.exportConfig.apiKey !== 'string') { + throw new Error('Stack API key is required to write stack.json when using a management token.'); + } + + log.debug('Writing config-based stack.json (api_key only, no stack fetch).', this.exportConfig.context); + + await fsUtil.makeDirectory(this.stackFolderPath); + const payload = { api_key: this.exportConfig.apiKey }; + const stackFilePath = pResolve(this.stackFolderPath, this.stackConfig.fileName); + fsUtil.writeFile(stackFilePath, payload); + + this.progressManager?.tick( + true, + `stack: ${this.exportConfig.apiKey}`, + null, + PROCESS_NAMES.STACK_DETAILS, + ); + + log.success( + `Stack identifier written to stack.json from config for stack ${this.exportConfig.apiKey}`, + this.exportConfig.context, + ); + return payload; + } + + private persistStackJsonPayload(resp: Record): any { + const sanitized = omit(resp, this.stackConfig.invalidKeys ?? []); + const stackFilePath = pResolve(this.stackFolderPath, this.stackConfig.fileName); + log.debug(`Writing stack data to: '${stackFilePath}'`, this.exportConfig.context); + fsUtil.writeFile(stackFilePath, sanitized); + + this.progressManager?.tick( + true, + `stack: ${this.exportConfig.apiKey}`, + null, + PROCESS_NAMES.STACK_DETAILS, + ); + + log.success( + `Stack details exported successfully for stack ${this.exportConfig.apiKey}`, + this.exportConfig.context, + ); + log.debug('Stack export completed successfully.', this.exportConfig.context); + return sanitized; + } + async exportStackSettings(): Promise { log.info('Exporting stack settings...', this.exportConfig.context); await fsUtil.makeDirectory(this.stackFolderPath); diff --git a/packages/contentstack-export/src/types/default-config.ts b/packages/contentstack-export/src/types/default-config.ts index b08239927..ca833b28f 100644 --- a/packages/contentstack-export/src/types/default-config.ts +++ b/packages/contentstack-export/src/types/default-config.ts @@ -147,6 +147,7 @@ export default interface DefaultConfig { stack: { dirName: string; fileName: string; + invalidKeys: string[]; dependencies?: Modules[]; }; dependency: { diff --git a/packages/contentstack-export/src/types/index.ts b/packages/contentstack-export/src/types/index.ts index 63baf41e6..7db80019e 100644 --- a/packages/contentstack-export/src/types/index.ts +++ b/packages/contentstack-export/src/types/index.ts @@ -127,6 +127,7 @@ export interface CustomRoleConfig { export interface StackConfig { dirName: string; fileName: string; + invalidKeys: string[]; dependencies?: Modules[]; limit?: number; } diff --git a/packages/contentstack-export/test/unit/export/modules/assets.test.ts b/packages/contentstack-export/test/unit/export/modules/assets.test.ts index 346d68756..37d0f6719 100644 --- a/packages/contentstack-export/test/unit/export/modules/assets.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/assets.test.ts @@ -174,6 +174,7 @@ describe('ExportAssets', () => { stack: { dirName: 'stack', fileName: 'stack.json', + invalidKeys: ['SYS_ACL', 'user_uids', 'owner_uid'], }, dependency: { entries: [], diff --git a/packages/contentstack-export/test/unit/export/modules/base-class.test.ts b/packages/contentstack-export/test/unit/export/modules/base-class.test.ts index 53ece4d15..baf9d73b0 100644 --- a/packages/contentstack-export/test/unit/export/modules/base-class.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/base-class.test.ts @@ -192,6 +192,7 @@ describe('BaseClass', () => { stack: { dirName: 'stack', fileName: 'stack.json', + invalidKeys: ['SYS_ACL', 'user_uids', 'owner_uid'], }, dependency: { entries: [], diff --git a/packages/contentstack-export/test/unit/export/modules/stack.test.ts b/packages/contentstack-export/test/unit/export/modules/stack.test.ts index 52645c028..69b86cf15 100644 --- a/packages/contentstack-export/test/unit/export/modules/stack.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/stack.test.ts @@ -187,6 +187,7 @@ describe('ExportStack', () => { stack: { dirName: 'stack', fileName: 'stack.json', + invalidKeys: ['SYS_ACL', 'user_uids', 'owner_uid'], limit: 100, }, dependency: { @@ -424,22 +425,28 @@ describe('ExportStack', () => { }); describe('exportStack() method', () => { - it('should export stack successfully and write to file', async () => { + it('should export stack successfully and write to file omitting invalidKeys', async () => { const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; - const stackData = { name: 'Test Stack', uid: 'stack-uid', org_uid: 'org-123' }; + const stackData = { + name: 'Test Stack', + uid: 'stack-uid', + org_uid: 'org-123', + SYS_ACL: {}, + user_uids: ['u1'], + owner_uid: 'owner-1', + }; + const expectedWritten = { name: 'Test Stack', uid: 'stack-uid', org_uid: 'org-123' }; mockStackClient.fetch = sinon.stub().resolves(stackData); const result = await exportStack.exportStack(); expect(writeFileStub.called).to.be.true; expect(makeDirectoryStub.called).to.be.true; - // Should return the stack data - expect(result).to.deep.equal(stackData); - // Verify file was written with correct path + expect(result).to.deep.equal(expectedWritten); const writeCall = writeFileStub.getCall(0); expect(writeCall.args[0]).to.include('stack.json'); - expect(writeCall.args[1]).to.deep.equal(stackData); + expect(writeCall.args[1]).to.deep.equal(expectedWritten); }); it('should handle errors when exporting stack without throwing', async () => { @@ -544,9 +551,11 @@ describe('ExportStack', () => { getStackStub.restore(); }); - it('should skip exportStackSettings when management_token is present', async () => { - const getStackStub = sinon.stub(exportStack, 'getStack').resolves({}); + it('should write stack.json from config api_key only when management_token is present', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const getStackSpy = sinon.spy(exportStack, 'getStack'); const exportStackSettingsSpy = sinon.spy(exportStack, 'exportStackSettings'); + const exportStackSpy = sinon.spy(exportStack, 'exportStack'); exportStack.exportConfig.management_token = 'some-token'; exportStack.exportConfig.preserveStackVersion = false; @@ -555,11 +564,20 @@ describe('ExportStack', () => { await exportStack.start(); - // Verify exportStackSettings was NOT called + expect(getStackSpy.called).to.be.false; expect(exportStackSettingsSpy.called).to.be.false; + expect(exportStackSpy.called).to.be.false; - getStackStub.restore(); + const stackJsonWrite = writeFileStub.getCalls().find((c) => String(c.args[0]).includes('stack.json')); + expect(stackJsonWrite).to.exist; + expect(stackJsonWrite!.args[1]).to.deep.equal({ api_key: 'test-api-key' }); + + const settingsWrite = writeFileStub.getCalls().find((c) => String(c.args[0]).includes('settings.json')); + expect(settingsWrite).to.be.undefined; + + getStackSpy.restore(); exportStackSettingsSpy.restore(); + exportStackSpy.restore(); }); }); }); From 9d88829beeb142a08a7a5c356d5563f1cf58bd71 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Wed, 25 Mar 2026 10:54:59 +0530 Subject: [PATCH 5/8] Fixed the redundant condition in the workflow and extensions --- packages/contentstack-audit/src/modules/extensions.ts | 6 ++---- packages/contentstack-audit/src/modules/workflows.ts | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/contentstack-audit/src/modules/extensions.ts b/packages/contentstack-audit/src/modules/extensions.ts index 7651eaeaa..d1c7416ff 100644 --- a/packages/contentstack-audit/src/modules/extensions.ts +++ b/packages/contentstack-audit/src/modules/extensions.ts @@ -222,10 +222,8 @@ export default class Extensions extends BaseClass { let shouldWrite: boolean; if (!this.fix) { shouldWrite = false; - } else if (preConfirmed === true) { - shouldWrite = true; - } else if (preConfirmed === false) { - shouldWrite = false; + } else if (preConfirmed !== undefined) { + shouldWrite = preConfirmed; } else if ( this.config.flags['copy-dir'] || this.config.flags['external-config']?.skipConfirm || diff --git a/packages/contentstack-audit/src/modules/workflows.ts b/packages/contentstack-audit/src/modules/workflows.ts index 4cc1fcef9..e1ea013ca 100644 --- a/packages/contentstack-audit/src/modules/workflows.ts +++ b/packages/contentstack-audit/src/modules/workflows.ts @@ -279,10 +279,8 @@ export default class Workflows extends BaseClass { let shouldWrite: boolean; if (!this.fix) { shouldWrite = false; - } else if (preConfirmed === true) { - shouldWrite = true; - } else if (preConfirmed === false) { - shouldWrite = false; + } else if (preConfirmed !== undefined) { + shouldWrite = preConfirmed; } else if ( this.config.flags['copy-dir'] || this.config.flags['external-config']?.skipConfirm || From 8c90e711bd17fc62817a12f91e18301bd021ffc2 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Thu, 26 Mar 2026 13:36:39 +0530 Subject: [PATCH 6/8] Fix removed the master_key anddescription --- packages/contentstack-export/src/config/index.ts | 2 +- .../test/unit/export/modules/stack.test.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/contentstack-export/src/config/index.ts b/packages/contentstack-export/src/config/index.ts index 23de9a5a2..0b1e48ed4 100644 --- a/packages/contentstack-export/src/config/index.ts +++ b/packages/contentstack-export/src/config/index.ts @@ -201,7 +201,7 @@ const config: DefaultConfig = { stack: { dirName: 'stack', fileName: 'stack.json', - invalidKeys: ['SYS_ACL', 'user_uids', 'owner_uid'], + invalidKeys: ['SYS_ACL', 'user_uids', 'owner_uid', 'description', 'master_key'], }, dependency: { entries: ['stack', 'locales', 'content-types'], diff --git a/packages/contentstack-export/test/unit/export/modules/stack.test.ts b/packages/contentstack-export/test/unit/export/modules/stack.test.ts index 69b86cf15..c291db60e 100644 --- a/packages/contentstack-export/test/unit/export/modules/stack.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/stack.test.ts @@ -187,7 +187,7 @@ describe('ExportStack', () => { stack: { dirName: 'stack', fileName: 'stack.json', - invalidKeys: ['SYS_ACL', 'user_uids', 'owner_uid'], + invalidKeys: ['SYS_ACL', 'user_uids', 'owner_uid', 'description', 'master_key'], limit: 100, }, dependency: { @@ -435,6 +435,8 @@ describe('ExportStack', () => { SYS_ACL: {}, user_uids: ['u1'], owner_uid: 'owner-1', + description: 'Stack description', + master_key: 'secret-master-key', }; const expectedWritten = { name: 'Test Stack', uid: 'stack-uid', org_uid: 'org-123' }; mockStackClient.fetch = sinon.stub().resolves(stackData); From 3d76ceff8d7402a85f52b9a4f1449e088b34fe29 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Fri, 27 Mar 2026 14:20:37 +0530 Subject: [PATCH 7/8] Update package versions across multiple Contentstack CLI packages to the latest beta releases, ensuring compatibility with updated dependencies. --- packages/contentstack-audit/package.json | 6 +++--- packages/contentstack-bootstrap/package.json | 10 +++++----- packages/contentstack-branches/package.json | 6 +++--- packages/contentstack-clone/package.json | 10 +++++----- packages/contentstack-export-to-csv/package.json | 6 +++--- packages/contentstack-export/package.json | 8 ++++---- packages/contentstack-import-setup/package.json | 6 +++--- packages/contentstack-import/package.json | 10 +++++----- packages/contentstack-migration/package.json | 6 +++--- packages/contentstack-seed/package.json | 8 ++++---- packages/contentstack-variants/package.json | 4 ++-- 11 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/contentstack-audit/package.json b/packages/contentstack-audit/package.json index c9881a893..2d5a9f94b 100644 --- a/packages/contentstack-audit/package.json +++ b/packages/contentstack-audit/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/cli-audit", - "version": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "description": "Contentstack audit plugin", "author": "Contentstack CLI", "homepage": "https://github.com/contentstack/cli", @@ -18,8 +18,8 @@ "/oclif.manifest.json" ], "dependencies": { - "@contentstack/cli-command": "~2.0.0-beta.4", - "@contentstack/cli-utilities": "~2.0.0-beta.4", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.5", "@oclif/core": "^4.3.0", "@oclif/plugin-help": "^6.2.28", "chalk": "^5.6.2", diff --git a/packages/contentstack-bootstrap/package.json b/packages/contentstack-bootstrap/package.json index fb7a3ba44..1e1506f31 100644 --- a/packages/contentstack-bootstrap/package.json +++ b/packages/contentstack-bootstrap/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-cm-bootstrap", "description": "Bootstrap contentstack apps", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "scripts": { @@ -16,10 +16,10 @@ "test:report": "nyc --reporter=lcov mocha \"test/**/*.test.js\"" }, "dependencies": { - "@contentstack/cli-cm-seed": "~2.0.0-beta.12", - "@contentstack/cli-command": "~2.0.0-beta.4", - "@contentstack/cli-utilities": "~2.0.0-beta.4", - "@contentstack/cli-config": "~2.0.0-beta.5", + "@contentstack/cli-cm-seed": "~2.0.0-beta.13", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.5", + "@contentstack/cli-config": "~2.0.0-beta.6", "@oclif/core": "^4.3.0", "@oclif/plugin-help": "^6.2.37", "inquirer": "12.11.1", diff --git a/packages/contentstack-branches/package.json b/packages/contentstack-branches/package.json index 475d6d48b..5bb717010 100644 --- a/packages/contentstack-branches/package.json +++ b/packages/contentstack-branches/package.json @@ -1,14 +1,14 @@ { "name": "@contentstack/cli-cm-branches", "description": "Contentstack CLI plugin to do branches operations", - "version": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { - "@contentstack/cli-command": "~2.0.0-beta.4", + "@contentstack/cli-command": "~2.0.0-beta.5", "@oclif/core": "^4.3.0", "@oclif/plugin-help": "^6.2.28", - "@contentstack/cli-utilities": "~2.0.0-beta.4", + "@contentstack/cli-utilities": "~2.0.0-beta.5", "chalk": "^5.6.2", "just-diff": "^6.0.2", "lodash": "^4.17.23" diff --git a/packages/contentstack-clone/package.json b/packages/contentstack-clone/package.json index a36c1c729..aadb6e4f8 100644 --- a/packages/contentstack-clone/package.json +++ b/packages/contentstack-clone/package.json @@ -1,15 +1,15 @@ { "name": "@contentstack/cli-cm-clone", "description": "Contentstack stack clone plugin", - "version": "2.0.0-beta.14", + "version": "2.0.0-beta.15", "author": "Contentstack", "bugs": "https://github.com/rohitmishra209/cli-cm-clone/issues", "dependencies": { "@colors/colors": "^1.6.0", - "@contentstack/cli-cm-export": "~2.0.0-beta.13", - "@contentstack/cli-cm-import": "~2.0.0-beta.13", - "@contentstack/cli-command": "~2.0.0-beta.4", - "@contentstack/cli-utilities": "~2.0.0-beta.4", + "@contentstack/cli-cm-export": "~2.0.0-beta.14", + "@contentstack/cli-cm-import": "~2.0.0-beta.14", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.5", "@oclif/core": "^4.3.0", "@oclif/plugin-help": "^6.2.28", "chalk": "^5.6.2", diff --git a/packages/contentstack-export-to-csv/package.json b/packages/contentstack-export-to-csv/package.json index 8daade632..92d2b87b7 100644 --- a/packages/contentstack-export-to-csv/package.json +++ b/packages/contentstack-export-to-csv/package.json @@ -1,12 +1,12 @@ { "name": "@contentstack/cli-cm-export-to-csv", "description": "Export entries, taxonomies, terms, or organization users to CSV", - "version": "2.0.0-beta.4", + "version": "2.0.0-beta.5", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { - "@contentstack/cli-command": "~2.0.0-beta.4", - "@contentstack/cli-utilities": "~2.0.0-beta.4", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.5", "@oclif/core": "^4.8.0", "@oclif/plugin-help": "^6.2.32", "fast-csv": "^4.3.6" diff --git a/packages/contentstack-export/package.json b/packages/contentstack-export/package.json index 5f3e14c46..d7298bf52 100644 --- a/packages/contentstack-export/package.json +++ b/packages/contentstack-export/package.json @@ -1,13 +1,13 @@ { "name": "@contentstack/cli-cm-export", "description": "Contentstack CLI plugin to export content from stack", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { - "@contentstack/cli-command": "~2.0.0-beta.4", - "@contentstack/cli-utilities": "~2.0.0-beta.4", - "@contentstack/cli-variants": "~2.0.0-beta.10", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.5", + "@contentstack/cli-variants": "~2.0.0-beta.11", "@oclif/core": "^4.8.0", "async": "^3.2.6", "big-json": "^3.2.0", diff --git a/packages/contentstack-import-setup/package.json b/packages/contentstack-import-setup/package.json index 76a58adac..0855f28c8 100644 --- a/packages/contentstack-import-setup/package.json +++ b/packages/contentstack-import-setup/package.json @@ -1,12 +1,12 @@ { "name": "@contentstack/cli-cm-import-setup", "description": "Contentstack CLI plugin to setup the mappers and configurations for the import command", - "version": "2.0.0-beta.8", + "version": "2.0.0-beta.9", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { - "@contentstack/cli-command": "~2.0.0-beta.4", - "@contentstack/cli-utilities": "~2.0.0-beta.4", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.5", "@oclif/core": "^4.3.0", "big-json": "^3.2.0", "chalk": "^5.6.2", diff --git a/packages/contentstack-import/package.json b/packages/contentstack-import/package.json index aad5ec76b..007c3e918 100644 --- a/packages/contentstack-import/package.json +++ b/packages/contentstack-import/package.json @@ -1,14 +1,14 @@ { "name": "@contentstack/cli-cm-import", "description": "Contentstack CLI plugin to import content into stack", - "version": "2.0.0-beta.13", + "version": "2.0.0-beta.14", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { - "@contentstack/cli-audit": "~2.0.0-beta.8", - "@contentstack/cli-command": "~2.0.0-beta.4", - "@contentstack/cli-utilities": "~2.0.0-beta.4", - "@contentstack/cli-variants": "~2.0.0-beta.9", + "@contentstack/cli-audit": "~2.0.0-beta.9", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.5", + "@contentstack/cli-variants": "~2.0.0-beta.11", "@oclif/core": "^4.3.0", "big-json": "^3.2.0", "bluebird": "^3.7.2", diff --git a/packages/contentstack-migration/package.json b/packages/contentstack-migration/package.json index 1ce610ce8..5bb3d5c5e 100644 --- a/packages/contentstack-migration/package.json +++ b/packages/contentstack-migration/package.json @@ -1,11 +1,11 @@ { "name": "@contentstack/cli-migration", - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "author": "@contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { - "@contentstack/cli-command": "~2.0.0-beta.4", - "@contentstack/cli-utilities": "~2.0.0-beta.4", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.5", "@oclif/core": "^4.3.0", "@oclif/plugin-help": "^6.2.28", "async": "^3.2.6", diff --git a/packages/contentstack-seed/package.json b/packages/contentstack-seed/package.json index 2a2abf631..b59f70e3c 100644 --- a/packages/contentstack-seed/package.json +++ b/packages/contentstack-seed/package.json @@ -1,13 +1,13 @@ { "name": "@contentstack/cli-cm-seed", "description": "create a Stack from existing content types, entries, assets, etc.", - "version": "2.0.0-beta.12", + "version": "2.0.0-beta.13", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { - "@contentstack/cli-cm-import": "~2.0.0-beta.13", - "@contentstack/cli-command": "~2.0.0-beta.4", - "@contentstack/cli-utilities": "~2.0.0-beta.4", + "@contentstack/cli-cm-import": "~2.0.0-beta.14", + "@contentstack/cli-command": "~2.0.0-beta.5", + "@contentstack/cli-utilities": "~2.0.0-beta.5", "inquirer": "12.11.1", "mkdirp": "^1.0.4", "tar": "^7.5.11", diff --git a/packages/contentstack-variants/package.json b/packages/contentstack-variants/package.json index 3850adf3f..d55e6f9b6 100644 --- a/packages/contentstack-variants/package.json +++ b/packages/contentstack-variants/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/cli-variants", - "version": "2.0.0-beta.10", + "version": "2.0.0-beta.11", "description": "Variants plugin", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -28,7 +28,7 @@ "typescript": "^5.8.3" }, "dependencies": { - "@contentstack/cli-utilities": "~2.0.0-beta.4", + "@contentstack/cli-utilities": "~2.0.0-beta.5", "@oclif/core": "^4.3.0", "@oclif/plugin-help": "^6.2.28", "lodash": "^4.17.23", From 13abb8b4e3475e926d6cddaa68ed79b64d381cc7 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Mon, 30 Mar 2026 16:09:38 +0530 Subject: [PATCH 8/8] update pnpm-lock --- .talismanrc | 2 +- pnpm-lock.yaml | 180 +++++++++++++++++++++++++------------------------ 2 files changed, 93 insertions(+), 89 deletions(-) diff --git a/.talismanrc b/.talismanrc index 61f1fa0bc..6cbd26fc6 100644 --- a/.talismanrc +++ b/.talismanrc @@ -1,4 +1,4 @@ fileignoreconfig: - filename: pnpm-lock.yaml - checksum: ac795ffbdc0f82463baf34715a30b76cf6a67b2d8893ec835a5861aa915b20bf + checksum: 78b7fca30ae03e2570a384c5432c10f0e6b023f492b68929795adcb4613e8673 version: '1.0' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23bb207d4..03be6d177 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,11 +18,11 @@ importers: packages/contentstack-audit: dependencies: '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@20.19.37) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@20.19.37) '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@20.19.37) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@20.19.37) '@oclif/core': specifier: ^4.3.0 version: 4.10.2 @@ -103,17 +103,17 @@ importers: packages/contentstack-bootstrap: dependencies: '@contentstack/cli-cm-seed': - specifier: ~2.0.0-beta.12 + specifier: ~2.0.0-beta.13 version: link:../contentstack-seed '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@18.19.130) - '@contentstack/cli-config': specifier: ~2.0.0-beta.5 version: 2.0.0-beta.5(@types/node@18.19.130) + '@contentstack/cli-config': + specifier: ~2.0.0-beta.6 + version: 2.0.0-beta.6(@types/node@18.19.130) '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@18.19.130) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@18.19.130) '@oclif/core': specifier: ^4.3.0 version: 4.10.2 @@ -179,11 +179,11 @@ importers: packages/contentstack-branches: dependencies: '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@22.19.15) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@22.19.15) '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@22.19.15) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@22.19.15) '@oclif/core': specifier: ^4.3.0 version: 4.10.2 @@ -246,17 +246,17 @@ importers: specifier: ^1.6.0 version: 1.6.0 '@contentstack/cli-cm-export': - specifier: ~2.0.0-beta.13 + specifier: ~2.0.0-beta.14 version: link:../contentstack-export '@contentstack/cli-cm-import': - specifier: ~2.0.0-beta.13 + specifier: ~2.0.0-beta.14 version: link:../contentstack-import '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@18.19.130) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@18.19.130) '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@18.19.130) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@18.19.130) '@oclif/core': specifier: ^4.3.0 version: 4.10.2 @@ -334,13 +334,13 @@ importers: packages/contentstack-export: dependencies: '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@22.19.15) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@22.19.15) '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@22.19.15) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@22.19.15) '@contentstack/cli-variants': - specifier: ~2.0.0-beta.10 + specifier: ~2.0.0-beta.11 version: link:../contentstack-variants '@oclif/core': specifier: ^4.8.0 @@ -449,11 +449,11 @@ importers: packages/contentstack-export-to-csv: dependencies: '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@20.19.37) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@20.19.37) '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@20.19.37) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@20.19.37) '@oclif/core': specifier: ^4.8.0 version: 4.10.2 @@ -516,16 +516,16 @@ importers: packages/contentstack-import: dependencies: '@contentstack/cli-audit': - specifier: ~2.0.0-beta.8 + specifier: ~2.0.0-beta.9 version: link:../contentstack-audit '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@14.18.63)(debug@4.4.3) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@14.18.63)(debug@4.4.3) '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@14.18.63)(debug@4.4.3) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@14.18.63)(debug@4.4.3) '@contentstack/cli-variants': - specifier: ~2.0.0-beta.9 + specifier: ~2.0.0-beta.11 version: link:../contentstack-variants '@oclif/core': specifier: ^4.3.0 @@ -628,11 +628,11 @@ importers: packages/contentstack-import-setup: dependencies: '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@14.18.63) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@14.18.63) '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@14.18.63)(debug@4.4.3) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@14.18.63)(debug@4.4.3) '@oclif/core': specifier: ^4.3.0 version: 4.10.2 @@ -731,11 +731,11 @@ importers: packages/contentstack-migration: dependencies: '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@14.18.63) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@14.18.63) '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@14.18.63)(debug@4.4.3) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@14.18.63)(debug@4.4.3) '@oclif/core': specifier: ^4.3.0 version: 4.10.2 @@ -816,14 +816,14 @@ importers: packages/contentstack-seed: dependencies: '@contentstack/cli-cm-import': - specifier: ~2.0.0-beta.13 + specifier: ~2.0.0-beta.14 version: link:../contentstack-import '@contentstack/cli-command': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@18.19.130) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@18.19.130) '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@18.19.130) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@18.19.130) inquirer: specifier: 12.11.1 version: 12.11.1(@types/node@18.19.130) @@ -889,8 +889,8 @@ importers: packages/contentstack-variants: dependencies: '@contentstack/cli-utilities': - specifier: ~2.0.0-beta.4 - version: 2.0.0-beta.4(@types/node@20.19.37) + specifier: ~2.0.0-beta.5 + version: 2.0.0-beta.5(@types/node@20.19.37) '@oclif/core': specifier: ^4.3.0 version: 4.10.2 @@ -1271,22 +1271,26 @@ packages: resolution: {integrity: sha512-BHxs/NqwVx6D/Q99Mtl3smXdMmoCZ62fjWejyfQY/1tIZv7s5uHw8vt3f8EaYDWwdkgkNIeRJ1O1gXnmG922Pw==} engines: {node: '>=14.0.0'} - '@contentstack/cli-command@2.0.0-beta.4': - resolution: {integrity: sha512-Eei1JaCU51uf62+Aug802KORWZTymosttyxLe3ub2yshgrpmfEeaFLYlO8Z7gSDT/i2ulvCnF1vq7Miio6mzbA==} + '@contentstack/cli-command@2.0.0-beta.5': + resolution: {integrity: sha512-fsvawypwNfaje4e0FAe/H6b93GXMnZV5xl8ON99IGRdtJ9RFFHsZG8zbUM89MAm9ivTbpAksJ4zBn4hZHf66iA==} engines: {node: '>=14.0.0'} '@contentstack/cli-config@2.0.0-beta.5': resolution: {integrity: sha512-lbfIbUmTC88cm3POoe1uUq+oqLvjFO31/wuHnhObQ1E3ql9Ldrjs8qDcpFArr2QkpS9o3VxM5pDDluDmX8IgXg==} engines: {node: '>=14.0.0'} + '@contentstack/cli-config@2.0.0-beta.6': + resolution: {integrity: sha512-wJvxZGSv7PRBEKfm/Jm6iorA+e4q0QVo5NCvn7xWp61aNixs9B53yO0ZwHYd0qi0B+d+H5qlgpm9BMad5EKfRg==} + engines: {node: '>=14.0.0'} + '@contentstack/cli-dev-dependencies@2.0.0-beta.0': resolution: {integrity: sha512-tLP05taIeepvp5Xte2LKDTKeYtDjCxOLlNWzwMFhMFYU1Z7oOgiCu8RVHNz+EkAm5xScKORx1OyEgyNLFoTLBw==} - '@contentstack/cli-utilities@2.0.0-beta.4': - resolution: {integrity: sha512-C+nDA0d9/hK703aAVYrUeR2oK23g3vk/jBzb9CEh1nt2Wc/obzFjcmLHVZJq9riljkTpgsoDVGtQLW0IOmdisA==} + '@contentstack/cli-utilities@2.0.0-beta.5': + resolution: {integrity: sha512-rURu8H5ZpYlxtpWunQaMHwH3Q0oAbgmqbrnHoFBKZeNSm7BSq3/y9udNWgYzrfyGjVRHetoACL9jsQj1Ayt9Rg==} - '@contentstack/management@1.27.6': - resolution: {integrity: sha512-92h8YzKZ2EDzMogf0fmBHapCjVpzHkDBIj0Eb/MhPFIhlybDlAZhcM/di6zwgicEJj5UjTJ+ETXXQMEJZouDew==} + '@contentstack/management@1.29.1': + resolution: {integrity: sha512-TFzimKEcqLCXxh5GH9QnNCV0Ta0PrsSWMmXtshQYGw7atbtKpQNHhoZqO4ifVoMFlSnSe21MQrsJUoVbigSOSA==} engines: {node: '>=8.0.0'} '@contentstack/marketplace-sdk@1.5.0': @@ -7206,8 +7210,8 @@ snapshots: '@contentstack/cli-auth@2.0.0-beta.9(@types/node@22.19.15)': dependencies: - '@contentstack/cli-command': 2.0.0-beta.4(@types/node@22.19.15) - '@contentstack/cli-utilities': 2.0.0-beta.4(@types/node@22.19.15) + '@contentstack/cli-command': 2.0.0-beta.5(@types/node@22.19.15) + '@contentstack/cli-utilities': 2.0.0-beta.5(@types/node@22.19.15) '@oclif/core': 4.10.2 '@oclif/plugin-help': 6.2.40 otplib: 12.0.1 @@ -7215,9 +7219,9 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-command@2.0.0-beta.4(@types/node@14.18.63)': + '@contentstack/cli-command@2.0.0-beta.5(@types/node@14.18.63)': dependencies: - '@contentstack/cli-utilities': 2.0.0-beta.4(@types/node@14.18.63) + '@contentstack/cli-utilities': 2.0.0-beta.5(@types/node@14.18.63) '@oclif/core': 4.10.2 '@oclif/plugin-help': 6.2.40 contentstack: 3.27.0 @@ -7225,9 +7229,9 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-command@2.0.0-beta.4(@types/node@14.18.63)(debug@4.4.3)': + '@contentstack/cli-command@2.0.0-beta.5(@types/node@14.18.63)(debug@4.4.3)': dependencies: - '@contentstack/cli-utilities': 2.0.0-beta.4(@types/node@14.18.63)(debug@4.4.3) + '@contentstack/cli-utilities': 2.0.0-beta.5(@types/node@14.18.63)(debug@4.4.3) '@oclif/core': 4.10.2 '@oclif/plugin-help': 6.2.40 contentstack: 3.27.0 @@ -7235,9 +7239,9 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-command@2.0.0-beta.4(@types/node@18.19.130)': + '@contentstack/cli-command@2.0.0-beta.5(@types/node@18.19.130)': dependencies: - '@contentstack/cli-utilities': 2.0.0-beta.4(@types/node@18.19.130) + '@contentstack/cli-utilities': 2.0.0-beta.5(@types/node@18.19.130) '@oclif/core': 4.10.2 '@oclif/plugin-help': 6.2.40 contentstack: 3.27.0 @@ -7245,9 +7249,9 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-command@2.0.0-beta.4(@types/node@20.19.37)': + '@contentstack/cli-command@2.0.0-beta.5(@types/node@20.19.37)': dependencies: - '@contentstack/cli-utilities': 2.0.0-beta.4(@types/node@20.19.37) + '@contentstack/cli-utilities': 2.0.0-beta.5(@types/node@20.19.37) '@oclif/core': 4.10.2 '@oclif/plugin-help': 6.2.40 contentstack: 3.27.0 @@ -7255,9 +7259,9 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-command@2.0.0-beta.4(@types/node@22.19.15)': + '@contentstack/cli-command@2.0.0-beta.5(@types/node@22.19.15)': dependencies: - '@contentstack/cli-utilities': 2.0.0-beta.4(@types/node@22.19.15) + '@contentstack/cli-utilities': 2.0.0-beta.5(@types/node@22.19.15) '@oclif/core': 4.10.2 '@oclif/plugin-help': 6.2.40 contentstack: 3.27.0 @@ -7265,10 +7269,10 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-config@2.0.0-beta.5(@types/node@18.19.130)': + '@contentstack/cli-config@2.0.0-beta.5(@types/node@22.19.15)': dependencies: - '@contentstack/cli-command': 2.0.0-beta.4(@types/node@18.19.130) - '@contentstack/cli-utilities': 2.0.0-beta.4(@types/node@18.19.130) + '@contentstack/cli-command': 2.0.0-beta.5(@types/node@22.19.15) + '@contentstack/cli-utilities': 2.0.0-beta.5(@types/node@22.19.15) '@contentstack/utils': 1.7.1 '@oclif/core': 4.10.2 '@oclif/plugin-help': 6.2.40 @@ -7277,10 +7281,10 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-config@2.0.0-beta.5(@types/node@22.19.15)': + '@contentstack/cli-config@2.0.0-beta.6(@types/node@18.19.130)': dependencies: - '@contentstack/cli-command': 2.0.0-beta.4(@types/node@22.19.15) - '@contentstack/cli-utilities': 2.0.0-beta.4(@types/node@22.19.15) + '@contentstack/cli-command': 2.0.0-beta.5(@types/node@18.19.130) + '@contentstack/cli-utilities': 2.0.0-beta.5(@types/node@18.19.130) '@contentstack/utils': 1.7.1 '@oclif/core': 4.10.2 '@oclif/plugin-help': 6.2.40 @@ -7298,9 +7302,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@contentstack/cli-utilities@2.0.0-beta.4(@types/node@14.18.63)': + '@contentstack/cli-utilities@2.0.0-beta.5(@types/node@14.18.63)': dependencies: - '@contentstack/management': 1.27.6(debug@4.4.3) + '@contentstack/management': 1.29.1(debug@4.4.3) '@contentstack/marketplace-sdk': 1.5.0(debug@4.4.3) '@oclif/core': 4.10.2 axios: 1.13.6(debug@4.4.3) @@ -7333,9 +7337,9 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-utilities@2.0.0-beta.4(@types/node@14.18.63)(debug@4.4.3)': + '@contentstack/cli-utilities@2.0.0-beta.5(@types/node@14.18.63)(debug@4.4.3)': dependencies: - '@contentstack/management': 1.27.6(debug@4.4.3) + '@contentstack/management': 1.29.1(debug@4.4.3) '@contentstack/marketplace-sdk': 1.5.0(debug@4.4.3) '@oclif/core': 4.10.2 axios: 1.13.6(debug@4.4.3) @@ -7368,9 +7372,9 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-utilities@2.0.0-beta.4(@types/node@18.19.130)': + '@contentstack/cli-utilities@2.0.0-beta.5(@types/node@18.19.130)': dependencies: - '@contentstack/management': 1.27.6(debug@4.4.3) + '@contentstack/management': 1.29.1(debug@4.4.3) '@contentstack/marketplace-sdk': 1.5.0(debug@4.4.3) '@oclif/core': 4.10.2 axios: 1.13.6(debug@4.4.3) @@ -7403,9 +7407,9 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-utilities@2.0.0-beta.4(@types/node@20.19.37)': + '@contentstack/cli-utilities@2.0.0-beta.5(@types/node@20.19.37)': dependencies: - '@contentstack/management': 1.27.6(debug@4.4.3) + '@contentstack/management': 1.29.1(debug@4.4.3) '@contentstack/marketplace-sdk': 1.5.0(debug@4.4.3) '@oclif/core': 4.10.2 axios: 1.13.6(debug@4.4.3) @@ -7438,9 +7442,9 @@ snapshots: - '@types/node' - debug - '@contentstack/cli-utilities@2.0.0-beta.4(@types/node@22.19.15)': + '@contentstack/cli-utilities@2.0.0-beta.5(@types/node@22.19.15)': dependencies: - '@contentstack/management': 1.27.6(debug@4.4.3) + '@contentstack/management': 1.29.1(debug@4.4.3) '@contentstack/marketplace-sdk': 1.5.0(debug@4.4.3) '@oclif/core': 4.10.2 axios: 1.13.6(debug@4.4.3) @@ -7473,7 +7477,7 @@ snapshots: - '@types/node' - debug - '@contentstack/management@1.27.6(debug@4.4.3)': + '@contentstack/management@1.29.1(debug@4.4.3)': dependencies: '@contentstack/utils': 1.8.0 assert: 2.1.0 @@ -10775,7 +10779,7 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint-config-xo-space: 0.35.0(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-mocha: 10.5.0(eslint@8.57.1) eslint-plugin-n: 15.7.0(eslint@8.57.1) eslint-plugin-perfectionist: 2.11.0(eslint@8.57.1)(typescript@5.9.3) @@ -10889,7 +10893,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) transitivePeerDependencies: - supports-color @@ -10954,7 +10958,7 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9