From 68c32c7f6faa23c047bdac5aed75cf0c2e50c303 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Sat, 2 May 2026 23:45:55 +0530 Subject: [PATCH] feat(import): publish taxonomies after import --- .../src/import/modules/base-class.ts | 11 + .../src/import/modules/taxonomies.ts | 261 ++++++++++++++++-- .../src/utils/constants.ts | 5 + .../unit/import/modules/base-class.test.ts | 54 ++++ .../unit/import/modules/taxonomies.test.ts | 141 ++++++++-- 5 files changed, 433 insertions(+), 39 deletions(-) diff --git a/packages/contentstack-import/src/import/modules/base-class.ts b/packages/contentstack-import/src/import/modules/base-class.ts index 55f8067e3..286485139 100644 --- a/packages/contentstack-import/src/import/modules/base-class.ts +++ b/packages/contentstack-import/src/import/modules/base-class.ts @@ -58,6 +58,7 @@ export type ApiModuleType = | 'create-entries' | 'update-entries' | 'publish-entries' + | 'publish-taxonomies' | 'delete-entries' | 'create-taxonomies' | 'create-terms' @@ -343,6 +344,8 @@ export default abstract class BaseClass { if ( !apiData || (entity === 'publish-entries' && !apiData.entryUid) || + (entity === 'publish-taxonomies' && + (!apiData.environments?.length || !apiData.locales?.length || !apiData.items?.length)) || (entity === 'update-extensions' && !apiData.uid) ) { return Promise.resolve(); @@ -489,6 +492,14 @@ export default abstract class BaseClass { }) .then(onSuccess) .catch(onReject); + case 'publish-taxonomies': { + const publishParams = this.importConfig.branchName ? { branch: this.importConfig.branchName } : {}; + return (this.stack as any) + .taxonomy() + .publish(apiData, '3.2', publishParams) + .then(onSuccess) + .catch(onReject); + } case 'delete-entries': return this.stack .contentType(apiData.cTUid) diff --git a/packages/contentstack-import/src/import/modules/taxonomies.ts b/packages/contentstack-import/src/import/modules/taxonomies.ts index bd9b1a87c..c57462a6c 100644 --- a/packages/contentstack-import/src/import/modules/taxonomies.ts +++ b/packages/contentstack-import/src/import/modules/taxonomies.ts @@ -1,7 +1,7 @@ import { join } from 'node:path'; import values from 'lodash/values'; import isEmpty from 'lodash/isEmpty'; -import { log, handleAndLogError } from '@contentstack/cli-utilities'; +import { log, handleAndLogError, CLIProgressManager } from '@contentstack/cli-utilities'; import { PATH_CONSTANTS } from '../../constants'; import BaseClass, { ApiOptions } from './base-class'; @@ -19,6 +19,8 @@ export default class ImportTaxonomies extends BaseClass { private termsSuccessPath: string; private termsFailsPath: string; private localesFilePath: string; + private envUidMapperPath: string; + private envUidMapper: Record = {}; private isLocaleBasedStructure: boolean = false; public createdTaxonomies: Record = {}; public failedTaxonomies: Record = {}; @@ -46,8 +48,16 @@ export default class ImportTaxonomies extends BaseClass { importConfig.modules.locales.dirName, importConfig.modules.locales.fileName, ); + this.envUidMapperPath = join( + importConfig.backupDir, + PATH_CONSTANTS.MAPPER, + PATH_CONSTANTS.MAPPER_MODULES.ENVIRONMENTS, + PATH_CONSTANTS.FILES.UID_MAPPING, + ); } + // --- Lifecycle --- + /** * @method start * @returns {Promise} Promise @@ -56,7 +66,7 @@ export default class ImportTaxonomies extends BaseClass { try { log.debug('Starting taxonomies import process...', this.importConfig.context); - const [taxonomiesCount] = await this.analyzeTaxonomies(); + const [taxonomiesCount, publishJobCount] = await this.analyzeTaxonomies(); if (taxonomiesCount === 0) { log.info('No taxonomies found to import', this.importConfig.context); return; @@ -67,8 +77,12 @@ export default class ImportTaxonomies extends BaseClass { // Check if locale-based structure exists before import this.isLocaleBasedStructure = this.detectAndScanLocaleStructure(); - const progress = this.createSimpleProgress(this.currentModuleName, taxonomiesCount); - progress.updateStatus(PROCESS_STATUS[PROCESS_NAMES.TAXONOMIES_IMPORT].IMPORTING); + const progress = this.createNestedProgress(this.currentModuleName); + this.initializeTaxonomiesProgress(progress, taxonomiesCount, publishJobCount); + + progress + .startProcess(PROCESS_NAMES.TAXONOMIES_IMPORT) + .updateStatus(PROCESS_STATUS[PROCESS_NAMES.TAXONOMIES_IMPORT].IMPORTING, PROCESS_NAMES.TAXONOMIES_IMPORT); log.debug('Starting taxonomies import', this.importConfig.context); if (this.isLocaleBasedStructure) { @@ -79,6 +93,19 @@ export default class ImportTaxonomies extends BaseClass { await this.importTaxonomiesLegacy(); } + progress.completeProcess(PROCESS_NAMES.TAXONOMIES_IMPORT, true); + + if (publishJobCount > 0) { + progress + .startProcess(PROCESS_NAMES.TAXONOMIES_PUBLISH) + .updateStatus( + PROCESS_STATUS[PROCESS_NAMES.TAXONOMIES_PUBLISH].PUBLISHING, + PROCESS_NAMES.TAXONOMIES_PUBLISH, + ); + await this.processTaxonomyPublishing(); + progress.completeProcess(PROCESS_NAMES.TAXONOMIES_PUBLISH, true); + } + this.createSuccessAndFailedFile(); this.completeProgressWithMessage(); } catch (error) { @@ -87,6 +114,8 @@ export default class ImportTaxonomies extends BaseClass { } } + // --- Import --- + /** * create taxonomy and enter success & failure related data into taxonomies mapper file * @method importTaxonomies @@ -344,6 +373,191 @@ export default class ImportTaxonomies extends BaseClass { return true; } + // --- Progress --- + + /** + * Registers nested progress for taxonomy import and optional taxonomy publish when publish jobs exist. + */ + initializeTaxonomiesProgress(progress: CLIProgressManager, taxonomyCount: number, publishJobCount: number): void { + progress.addProcess(PROCESS_NAMES.TAXONOMIES_IMPORT, taxonomyCount); + if (publishJobCount > 0) { + progress.addProcess(PROCESS_NAMES.TAXONOMIES_PUBLISH, publishJobCount); + } + } + + // --- Publish --- + + /** + * Reads source env UID → destination stack env UID map produced during environments import. + */ + private readEnvUidMapperSync(): Record { + if (!fileHelper.fileExistsSync(this.envUidMapperPath)) { + log.debug(`Environment UID mapper not found at ${this.envUidMapperPath}`, this.importConfig.context); + return {}; + } + + try { + const raw = fsUtil.readFile(this.envUidMapperPath, true) as Record; + const out: Record = {}; + for (const [k, v] of Object.entries(raw || {})) { + if (v !== undefined && v !== null && String(v).trim() !== '') { + out[k] = String(v); + } + } + return out; + } catch { + log.debug('Failed to read environment UID mapper', this.importConfig.context); + return {}; + } + } + + private countPublishEligibleTaxonomies(envMapper: Record): number { + let count = 0; + for (const key of Object.keys(this.taxonomies || {})) { + const meta = this.taxonomies[key] as Record; + const taxonomyUid = meta?.uid || key; + const filePath = this.findTaxonomyFilePath(taxonomyUid); + if (!filePath) continue; + + const details = this.loadTaxonomyFile(filePath); + const tax = details?.taxonomy as Record | undefined; + if (!tax?.publish_details?.length || !tax?.locale) continue; + + const hasMapped = (tax.publish_details as any[]).some( + (p: any) => p?.environment && envMapper[String(p.environment)], + ); + if (hasMapped) count++; + } + return count; + } + + private collectTaxonomyPublishJobs(): Array<{ taxonomy: Record }> { + const jobs: Array<{ taxonomy: Record }> = []; + const seen = new Set(); + + for (const key of Object.keys(this.taxonomies || {})) { + const meta = this.taxonomies[key] as Record; + const taxonomyUid = meta?.uid || key; + if (seen.has(taxonomyUid)) continue; + + const filePath = this.findTaxonomyFilePath(taxonomyUid); + if (!filePath) continue; + + const details = this.loadTaxonomyFile(filePath); + const tax = details?.taxonomy as Record | undefined; + if (!tax?.publish_details?.length || !tax?.locale) continue; + + seen.add(taxonomyUid); + jobs.push({ taxonomy: tax }); + } + + return jobs; + } + + private loadEnvUidMapper(): void { + this.envUidMapper = this.readEnvUidMapperSync(); + if (isEmpty(this.envUidMapper)) { + log.warn( + 'Environment UID mapper is empty; taxonomy publishing is skipped. Import environments first or ensure mapper/environments/uid-mapping.json exists.', + this.importConfig.context, + ); + } + } + + async processTaxonomyPublishing(): Promise { + this.loadEnvUidMapper(); + const jobs = this.collectTaxonomyPublishJobs(); + + if (jobs.length === 0) { + log.debug('No taxonomies with publish_details to publish', this.importConfig.context); + return; + } + + log.info('Starting taxonomy publishing process', this.importConfig.context); + + const onSuccess = ({ apiData }: any) => { + const taxonomyUid = apiData?.items?.[0]?.uid; + this.progressManager?.tick( + true, + `taxonomy published: ${taxonomyUid}`, + null, + PROCESS_NAMES.TAXONOMIES_PUBLISH, + ); + log.success(`Published taxonomy '${taxonomyUid}'`, this.importConfig.context); + }; + + const onReject = ({ error, apiData }: any) => { + const taxonomyUid = apiData?.items?.[0]?.uid; + handleAndLogError( + error, + { ...this.importConfig.context, taxonomyUid }, + `Failed to publish taxonomy '${taxonomyUid}'`, + ); + this.progressManager?.tick( + false, + `taxonomy publish: ${taxonomyUid}`, + (error as Error)?.message || `Failed to publish taxonomy '${taxonomyUid}'`, + PROCESS_NAMES.TAXONOMIES_PUBLISH, + ); + }; + + await this.makeConcurrentCall( + { + apiContent: jobs as unknown as Record[], + processName: 'publish taxonomies', + apiParams: { + serializeData: this.serializePublishTaxonomies.bind(this), + reject: onReject, + resolve: onSuccess, + entity: 'publish-taxonomies', + includeParamOnCompletion: true, + }, + concurrencyLimit: this.importConfig.concurrency || this.importConfig.fetchConcurrency || 1, + }, + undefined, + false, + ); + } + + /** + * Builds taxonomy publish payload: destination env UIDs from mapper, locales from taxonomy.locale, items: [{ uid }]. + */ + serializePublishTaxonomies(apiOptions: ApiOptions): ApiOptions { + const job = apiOptions.apiData as { taxonomy?: Record }; + const taxonomy = job?.taxonomy; + + if (!taxonomy?.publish_details?.length || !taxonomy?.locale) { + apiOptions.apiData = undefined; + return apiOptions; + } + + const environments: string[] = []; + for (const pub of taxonomy.publish_details as any[]) { + const sourceEnvUid = pub?.environment; + if (!sourceEnvUid) continue; + const destUid = this.envUidMapper[String(sourceEnvUid)]; + if (destUid && !environments.includes(destUid)) { + environments.push(destUid); + } + } + + if (environments.length === 0) { + apiOptions.apiData = undefined; + return apiOptions; + } + + const locales = [String(taxonomy.locale)]; + apiOptions.apiData = { + environments, + locales, + items: [{ uid: taxonomy.uid }], + }; + + return apiOptions; + } + + // --- Mapper output --- + /** * create taxonomies success and fail in (mapper/taxonomies) * create terms success and fail in (mapper/taxonomies/terms) @@ -396,25 +610,36 @@ export default class ImportTaxonomies extends BaseClass { } } - private async analyzeTaxonomies(): Promise<[number]> { + // --- Analyze & prepare --- + + private async analyzeTaxonomies(): Promise<[number, number]> { return this.withLoadingSpinner('TAXONOMIES: Analyzing import data...', async () => { log.debug('Checking for taxonomies folder existence', this.importConfig.context); - if (fileHelper.fileExistsSync(this.taxonomiesFolderPath)) { - log.debug(`Found taxonomies folder: ${this.taxonomiesFolderPath}`, this.importConfig.context); - - this.taxonomies = fsUtil.readFile(join(this.taxonomiesFolderPath, 'taxonomies.json'), true) as Record< - string, - unknown - >; - - const taxonomyCount = Object.keys(this.taxonomies || {}).length; - log.debug(`Loaded ${taxonomyCount} taxonomy items from file`, this.importConfig.context); - return [taxonomyCount]; - } else { + if (!fileHelper.fileExistsSync(this.taxonomiesFolderPath)) { log.info(`No Taxonomies Found! - '${this.taxonomiesFolderPath}'`, this.importConfig.context); - return [0]; + return [0, 0]; } + + log.debug(`Found taxonomies folder: ${this.taxonomiesFolderPath}`, this.importConfig.context); + + this.taxonomies = fsUtil.readFile(join(this.taxonomiesFolderPath, 'taxonomies.json'), true) as Record< + string, + unknown + >; + + this.isLocaleBasedStructure = this.detectAndScanLocaleStructure(); + + const taxonomyCount = Object.keys(this.taxonomies || {}).length; + const envMapper = this.readEnvUidMapperSync(); + const publishJobCount = this.countPublishEligibleTaxonomies(envMapper); + + log.debug( + `Loaded ${taxonomyCount} taxonomy items; ${publishJobCount} eligible for publish (mapped environments).`, + this.importConfig.context, + ); + + return [taxonomyCount, publishJobCount]; }); } diff --git a/packages/contentstack-import/src/utils/constants.ts b/packages/contentstack-import/src/utils/constants.ts index c751bcfc8..80e36f7b5 100644 --- a/packages/contentstack-import/src/utils/constants.ts +++ b/packages/contentstack-import/src/utils/constants.ts @@ -57,6 +57,7 @@ export const PROCESS_NAMES = { CONTENT_TYPES_EXT_UPDATE: 'Content Types Ext Update', WEBHOOKS_IMPORT: 'Webhooks Import', TAXONOMIES_IMPORT: 'Taxonomies Import', + TAXONOMIES_PUBLISH: 'Taxonomies Publish', PERSONALIZE_PROJECTS: 'Projects', } as const; @@ -267,6 +268,10 @@ export const PROCESS_STATUS = { IMPORTING: 'Importing taxonomies...', FAILED: 'Failed to import taxonomies.', }, + [PROCESS_NAMES.TAXONOMIES_PUBLISH]: { + PUBLISHING: 'Publishing taxonomies...', + FAILED: 'Failed to publish taxonomies.', + }, [PROCESS_NAMES.PERSONALIZE_PROJECTS]: { IMPORTING: 'Importing personalization projects...', FAILED: 'Failed to import personalization projects.', diff --git a/packages/contentstack-import/test/unit/import/modules/base-class.test.ts b/packages/contentstack-import/test/unit/import/modules/base-class.test.ts index 43ad5ac72..bc8ea9219 100644 --- a/packages/contentstack-import/test/unit/import/modules/base-class.test.ts +++ b/packages/contentstack-import/test/unit/import/modules/base-class.test.ts @@ -74,6 +74,7 @@ describe('BaseClass', () => { create: sinon.stub().resolves({ uid: 'term-123' }), }), import: sinon.stub().resolves({ uid: 'import-123' }), + publish: sinon.stub().resolves({ notice: 'queued' }), }), globalField: sinon.stub().returns({ create: sinon.stub().resolves({ uid: 'gf-123' }), @@ -858,6 +859,59 @@ describe('BaseClass', () => { expect(result).to.be.undefined; expect(mockStackClient.taxonomy().import.called).to.be.false; }); + + it('should handle publish-taxonomies with api version 3.2', async () => { + const payload = { + environments: ['blt-env-dest'], + locales: ['en-us'], + items: [{ uid: 'tax-uid' }], + }; + mockApiOptions.entity = 'publish-taxonomies' as any; + mockApiOptions.apiData = payload; + + await testClass.makeAPICall(mockApiOptions); + + expect(mockStackClient.taxonomy().publish.calledOnce).to.be.true; + expect(mockStackClient.taxonomy().publish.firstCall.args[0]).to.deep.equal(payload); + expect(mockStackClient.taxonomy().publish.firstCall.args[1]).to.equal('3.2'); + expect(mockStackClient.taxonomy().publish.firstCall.args[2]).to.deep.equal({}); + expect(mockApiOptions.resolve.calledOnce).to.be.true; + }); + + it('should pass branch to publish-taxonomies when branchName is set', async () => { + const payload = { + environments: ['e1'], + locales: ['en-us'], + items: [{ uid: 't1' }], + }; + mockImportConfig.branchName = 'main-branch'; + testClass = new TestBaseClass({ + importConfig: mockImportConfig, + stackAPIClient: mockStackClient, + }); + + mockApiOptions.entity = 'publish-taxonomies' as any; + mockApiOptions.apiData = payload; + + await testClass.makeAPICall(mockApiOptions); + + expect(mockStackClient.taxonomy().publish.firstCall.args[2]).to.deep.equal({ branch: 'main-branch' }); + delete mockImportConfig.branchName; + }); + + it('should skip publish-taxonomies when environments empty', async () => { + mockApiOptions.entity = 'publish-taxonomies' as any; + mockApiOptions.apiData = { + environments: [], + locales: ['en-us'], + items: [{ uid: 't' }], + }; + + await testClass.makeAPICall(mockApiOptions); + + expect(mockStackClient.taxonomy().publish.called).to.be.false; + expect(mockApiOptions.resolve.called).to.be.false; + }); }); }); diff --git a/packages/contentstack-import/test/unit/import/modules/taxonomies.test.ts b/packages/contentstack-import/test/unit/import/modules/taxonomies.test.ts index 4ef453fd9..6744a4f85 100644 --- a/packages/contentstack-import/test/unit/import/modules/taxonomies.test.ts +++ b/packages/contentstack-import/test/unit/import/modules/taxonomies.test.ts @@ -5,6 +5,16 @@ import values from 'lodash/values'; import ImportTaxonomies from '../../../../src/import/modules/taxonomies'; import { fsUtil, fileHelper } from '../../../../src/utils'; +function nestedProgressMock(sb: sinon.SinonSandbox) { + return { + addProcess: sb.stub().returnsThis(), + startProcess: sb.stub().returnsThis(), + updateStatus: sb.stub().returnsThis(), + completeProcess: sb.stub().returnsThis(), + getFailureCount: sb.stub().returns(0), + }; +} + describe('ImportTaxonomies', () => { let importTaxonomies: ImportTaxonomies; let mockStackClient: any; @@ -67,11 +77,8 @@ describe('ImportTaxonomies', () => { sandbox.stub(importTaxonomies as any, 'withLoadingSpinner').callsFake(async (msg: string, fn: () => Promise) => { return await fn(); }); - sandbox.stub(importTaxonomies as any, 'analyzeTaxonomies').resolves([1]); - const mockProgress = { - updateStatus: sandbox.stub() - }; - sandbox.stub(importTaxonomies as any, 'createSimpleProgress').returns(mockProgress); + sandbox.stub(importTaxonomies as any, 'analyzeTaxonomies').resolves([1, 0]); + sandbox.stub(importTaxonomies as any, 'createNestedProgress').returns(nestedProgressMock(sandbox)); sandbox.stub(importTaxonomies as any, 'prepareMapperDirectories').resolves(); sandbox.stub(importTaxonomies as any, 'createSuccessAndFailedFile').resolves(); sandbox.stub(importTaxonomies as any, 'completeProgress').resolves(); @@ -104,6 +111,9 @@ describe('ImportTaxonomies', () => { expect((importTaxonomies as any).localesFilePath).to.equal( join(testBackupDir, 'locales', 'locales.json'), ); + expect((importTaxonomies as any).envUidMapperPath).to.equal( + join(testBackupDir, 'mapper', 'environments', 'uid-mapping.json'), + ); }); it('should set context module to taxonomies', () => { @@ -135,9 +145,7 @@ describe('ImportTaxonomies', () => { sandbox.stub(importTaxonomies as any, 'withLoadingSpinner').callsFake(async (msg: string, fn: () => Promise) => { return await fn(); }); - sandbox.stub(importTaxonomies as any, 'createSimpleProgress').returns({ - updateStatus: sinon.stub() - }); + sandbox.stub(importTaxonomies as any, 'createNestedProgress').returns(nestedProgressMock(sandbox)); const prepareMapperDirectoriesStub = sandbox.stub(importTaxonomies as any, 'prepareMapperDirectories').resolves(); const importTaxonomiesStub = sandbox.stub(importTaxonomies as any, 'importTaxonomies').resolves(); sandbox.stub(importTaxonomies as any, 'createSuccessAndFailedFile').resolves(); @@ -189,7 +197,7 @@ describe('ImportTaxonomies', () => { sandbox.stub(importTaxonomies as any, 'withLoadingSpinner').callsFake(async (msg: string, fn: () => Promise) => { return await fn(); }); - const analyzeTaxonomiesStub = sandbox.stub(importTaxonomies as any, 'analyzeTaxonomies').resolves([0]); + const analyzeTaxonomiesStub = sandbox.stub(importTaxonomies as any, 'analyzeTaxonomies').resolves([0, 0]); sandbox.stub(importTaxonomies as any, 'completeProgress').resolves(); await importTaxonomies.start(); @@ -211,10 +219,8 @@ describe('ImportTaxonomies', () => { sandbox.stub(importTaxonomies as any, 'withLoadingSpinner').callsFake(async (msg: string, fn: () => Promise) => { return await fn(); }); - sandbox.stub(importTaxonomies as any, 'analyzeTaxonomies').resolves([0]); // 0 taxonomies - sandbox.stub(importTaxonomies as any, 'createSimpleProgress').returns({ - updateStatus: sinon.stub() - }); + sandbox.stub(importTaxonomies as any, 'analyzeTaxonomies').resolves([0, 0]); // 0 taxonomies + sandbox.stub(importTaxonomies as any, 'createNestedProgress').returns(nestedProgressMock(sandbox)); sandbox.stub(importTaxonomies as any, 'prepareMapperDirectories').resolves(); sandbox.stub(importTaxonomies as any, 'importTaxonomies').resolves(); sandbox.stub(importTaxonomies as any, 'createSuccessAndFailedFile').resolves(); @@ -241,10 +247,8 @@ describe('ImportTaxonomies', () => { sandbox.stub(importTaxonomies as any, 'withLoadingSpinner').callsFake(async (msg: string, fn: () => Promise) => { return await fn(); }); - sandbox.stub(importTaxonomies as any, 'analyzeTaxonomies').resolves([1]); // 1 taxonomy - sandbox.stub(importTaxonomies as any, 'createSimpleProgress').returns({ - updateStatus: sinon.stub() - }); + sandbox.stub(importTaxonomies as any, 'analyzeTaxonomies').resolves([1, 0]); // 1 taxonomy + sandbox.stub(importTaxonomies as any, 'createNestedProgress').returns(nestedProgressMock(sandbox)); sandbox.stub(importTaxonomies as any, 'prepareMapperDirectories').resolves(); sandbox.stub(importTaxonomies as any, 'importTaxonomies').callsFake(async () => { (importTaxonomies as any).createdTaxonomies = { 'taxonomy_1': { uid: 'taxonomy_1' } }; @@ -392,6 +396,103 @@ describe('ImportTaxonomies', () => { }); }); + describe('serializePublishTaxonomies', () => { + it('maps source environment UIDs to destination UIDs and sets items to uid only', () => { + (importTaxonomies as any).envUidMapper = { bltSrc: 'bltDest' }; + const apiOptions: any = { + entity: 'publish-taxonomies', + apiData: { + taxonomy: { + uid: 'tax1', + locale: 'en-us', + publish_details: [{ environment: 'bltSrc', time: '', user: '' }], + }, + }, + resolve: sandbox.stub(), + reject: sandbox.stub(), + }; + + const result = (importTaxonomies as any).serializePublishTaxonomies(apiOptions); + + expect(result.apiData).to.deep.equal({ + environments: ['bltDest'], + locales: ['en-us'], + items: [{ uid: 'tax1' }], + }); + }); + + it('dedupes multiple publish_details environments', () => { + (importTaxonomies as any).envUidMapper = { e1: 'd1', e2: 'd2' }; + const apiOptions: any = { + entity: 'publish-taxonomies', + apiData: { + taxonomy: { + uid: 'tax2', + locale: 'fr-fr', + publish_details: [{ environment: 'e1' }, { environment: 'e2' }, { environment: 'e1' }], + }, + }, + resolve: sandbox.stub(), + reject: sandbox.stub(), + }; + + const result = (importTaxonomies as any).serializePublishTaxonomies(apiOptions); + + expect(result.apiData.environments).to.deep.equal(['d1', 'd2']); + expect(result.apiData.locales).to.deep.equal(['fr-fr']); + expect(result.apiData.items).to.deep.equal([{ uid: 'tax2' }]); + }); + + it('returns undefined when publish_details empty', () => { + (importTaxonomies as any).envUidMapper = { x: 'y' }; + const apiOptions: any = { + entity: 'publish-taxonomies', + apiData: { + taxonomy: { uid: 't', locale: 'en-us', publish_details: [] }, + }, + resolve: sandbox.stub(), + reject: sandbox.stub(), + }; + + expect((importTaxonomies as any).serializePublishTaxonomies(apiOptions).apiData).to.be.undefined; + }); + + it('returns undefined when no env mapping resolves', () => { + (importTaxonomies as any).envUidMapper = {}; + const apiOptions: any = { + entity: 'publish-taxonomies', + apiData: { + taxonomy: { + uid: 'tax1', + locale: 'en-us', + publish_details: [{ environment: 'missing' }], + }, + }, + resolve: sandbox.stub(), + reject: sandbox.stub(), + }; + + expect((importTaxonomies as any).serializePublishTaxonomies(apiOptions).apiData).to.be.undefined; + }); + + it('returns undefined when taxonomy.locale missing', () => { + (importTaxonomies as any).envUidMapper = { e: 'd' }; + const apiOptions: any = { + entity: 'publish-taxonomies', + apiData: { + taxonomy: { + uid: 'tax1', + publish_details: [{ environment: 'e' }], + }, + }, + resolve: sandbox.stub(), + reject: sandbox.stub(), + }; + + expect((importTaxonomies as any).serializePublishTaxonomies(apiOptions).apiData).to.be.undefined; + }); + }); + describe('createSuccessAndFailedFile', () => { it('should write all four files when data exists', () => { (importTaxonomies as any).createSuccessAndFailedFile.restore(); @@ -1123,10 +1224,8 @@ describe('ImportTaxonomies', () => { sandbox.stub(importTaxonomies as any, 'withLoadingSpinner').callsFake(async (msg: string, fn: () => Promise) => { return await fn(); }); - sandbox.stub(importTaxonomies as any, 'analyzeTaxonomies').resolves([1]); - sandbox.stub(importTaxonomies as any, 'createSimpleProgress').returns({ - updateStatus: sinon.stub() - }); + sandbox.stub(importTaxonomies as any, 'analyzeTaxonomies').resolves([1, 0]); + sandbox.stub(importTaxonomies as any, 'createNestedProgress').returns(nestedProgressMock(sandbox)); // Make prepareMapperDirectories reject with the error sandbox.stub(importTaxonomies as any, 'prepareMapperDirectories').rejects(new Error('Directory creation failed'));