From c2d84d5f5a47664d9c324c8025fec04c4e19dce4 Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Tue, 28 Oct 2025 11:36:43 +0530 Subject: [PATCH 1/2] chore: add test cases for envs, extensions, locales,stacks, taxonomies and webhooks --- .talismanrc | 12 + .../unit/export/modules/environments.test.ts | 287 ++++++++++++++++ .../unit/export/modules/extensions.test.ts | 287 ++++++++++++++++ .../test/unit/export/modules/locales.test.ts | 323 ++++++++++++++++++ .../test/unit/export/modules/stack.test.ts | 315 +++++++++++++++++ .../unit/export/modules/taxonomies.test.ts | 310 +++++++++++++++++ .../test/unit/export/modules/webhooks.test.ts | 231 +++++++++++++ 7 files changed, 1765 insertions(+) create mode 100644 packages/contentstack-export/test/unit/export/modules/environments.test.ts create mode 100644 packages/contentstack-export/test/unit/export/modules/extensions.test.ts create mode 100644 packages/contentstack-export/test/unit/export/modules/locales.test.ts create mode 100644 packages/contentstack-export/test/unit/export/modules/stack.test.ts create mode 100644 packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts create mode 100644 packages/contentstack-export/test/unit/export/modules/webhooks.test.ts diff --git a/.talismanrc b/.talismanrc index 68005c1db4..395390c9c2 100644 --- a/.talismanrc +++ b/.talismanrc @@ -131,4 +131,16 @@ fileignoreconfig: checksum: 58165d06d92f55be8abb04c4ecc47df775a1c47f1cee529f1be5277187700f97 - filename: packages/contentstack-import/test/unit/import/modules/locales.test.ts checksum: 011ec3efd7a29ed274f073c8678229eaef46f33e272e7e1db1206fa1a20383f0 +- filename: packages/contentstack-export/test/unit/export/modules/environments.test.ts + checksum: 530573c4c92387b755ca1b4eef88ae8bb2ae076be9a726bba7b67a525cba23e9 +- filename: packages/contentstack-export/test/unit/export/modules/extensions.test.ts + checksum: 857978a21ea981183254245f6b3cb5f51778d68fc726ddb26005ac96c706650f +- filename: packages/contentstack-export/test/unit/export/modules/webhooks.test.ts + checksum: 2e2d75281a57f873fb7f5fff0e5a9e863b631efd2fd92c4d2c81d9c8aeb3e252 +- filename: packages/contentstack-export/test/unit/export/modules/locales.test.ts + checksum: 93bdd99ee566fd38545b38a8b528947af1d42a31908aca85e2cb221e39a5b6cc +- filename: packages/contentstack-export/test/unit/export/modules/stack.test.ts + checksum: bb0f20845d85fd56197f1a8c67b8f71c57dcd1836ed9cfd86d1f49f41e84d3a0 +- filename: packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts + checksum: 621c1de129488b6a0372a91056ebb84353bcc642ce06de59e3852cfee8d0ce49 version: "1.0" \ No newline at end of file diff --git a/packages/contentstack-export/test/unit/export/modules/environments.test.ts b/packages/contentstack-export/test/unit/export/modules/environments.test.ts new file mode 100644 index 0000000000..da7949b00f --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/environments.test.ts @@ -0,0 +1,287 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility } from '@contentstack/cli-utilities'; +import ExportEnvironments from '../../../../src/export/modules/environments'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportEnvironments', () => { + let exportEnvironments: any; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + environment: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'env-1', name: 'Production' }, + { uid: 'env-2', name: 'Development' } + ], + count: 2 + }) + }) + }) + }; + + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + context: { + command: 'cm:stacks:export', + module: 'environments', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: {}, + preserveStackVersion: false, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: '', + onlyTSModules: [], + modules: { + types: ['environments'], + environments: { + dirName: 'environments', + fileName: 'environments.json', + limit: 100, + invalidKeys: [] + } + } + } as any; + + exportEnvironments = new ExportEnvironments({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'environments' + }); + + sinon.stub(FsUtility.prototype, 'writeFile').resolves(); + sinon.stub(FsUtility.prototype, 'makeDirectory').resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Constructor', () => { + it('should initialize with correct parameters', () => { + expect(exportEnvironments).to.be.instanceOf(ExportEnvironments); + }); + + it('should initialize environments object', () => { + expect(exportEnvironments.environments).to.be.an('object'); + }); + + it('should set context module to environments', () => { + expect(exportEnvironments.exportConfig.context.module).to.equal('environments'); + }); + }); + + describe('getEnvironments() method', () => { + it('should fetch and process environments correctly', async () => { + const environments = [ + { uid: 'env-1', name: 'Production', ACL: 'test' }, + { uid: 'env-2', name: 'Development', ACL: 'test' } + ]; + + mockStackClient.environment.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: environments, + count: 2 + }) + }) + }); + + await exportEnvironments.getEnvironments(); + + // Verify environments were processed + expect(Object.keys(exportEnvironments.environments).length).to.equal(2); + expect(exportEnvironments.environments['env-1']).to.exist; + expect(exportEnvironments.environments['env-1'].name).to.equal('Production'); + // Verify ACL was removed + expect(exportEnvironments.environments['env-1'].ACL).to.be.undefined; + }); + + it('should call getEnvironments recursively when more environments exist', async () => { + let callCount = 0; + mockStackClient.environment.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: Array(100).fill({ uid: 'test', name: 'Test' }), + count: 150 + }); + } else { + return Promise.resolve({ + items: Array(50).fill({ uid: 'test2', name: 'Test2' }), + count: 150 + }); + } + }) + }) + }); + + await exportEnvironments.getEnvironments(); + + // Verify multiple calls were made for recursive fetching + expect(callCount).to.be.greaterThan(1); + }); + + it('should handle API errors gracefully', async () => { + mockStackClient.environment.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('API Error')) + }) + }); + + await exportEnvironments.getEnvironments(); + + // Verify method completes without throwing + expect(exportEnvironments.environments).to.exist; + }); + + it('should handle no items response and not process environments', async () => { + mockStackClient.environment.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + const initialCount = Object.keys(exportEnvironments.environments).length; + await exportEnvironments.getEnvironments(); + + // Verify no new environments were added + expect(Object.keys(exportEnvironments.environments).length).to.equal(initialCount); + }); + + it('should handle empty environments array gracefully', async () => { + mockStackClient.environment.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: null, + count: 0 + }) + }) + }); + + const initialCount = Object.keys(exportEnvironments.environments).length; + await exportEnvironments.getEnvironments(); + + // Verify no processing occurred with null items + expect(Object.keys(exportEnvironments.environments).length).to.equal(initialCount); + }); + }); + + describe('start() method', () => { + it('should complete full export flow and write files', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + const environments = [ + { uid: 'env-1', name: 'Production' }, + { uid: 'env-2', name: 'Development' } + ]; + + mockStackClient.environment.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: environments, + count: 2 + }) + }) + }); + + await exportEnvironments.start(); + + // Verify environments were processed + expect(Object.keys(exportEnvironments.environments).length).to.equal(2); + expect(exportEnvironments.environments['env-1']).to.exist; + expect(exportEnvironments.environments['env-2']).to.exist; + // Verify file was written + expect(writeFileStub.called).to.be.true; + }); + + it('should handle empty environments and log NOT_FOUND', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + mockStackClient.environment.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + exportEnvironments.environments = {}; + await exportEnvironments.start(); + + // Verify writeFile was NOT called when environments are empty + expect(writeFileStub.called).to.be.false; + }); + }); + + describe('sanitizeAttribs() method', () => { + it('should sanitize environment attributes and remove ACL', () => { + const environments = [ + { uid: 'env-1', name: 'Production', ACL: 'remove' }, + { uid: 'env-2', name: 'Development', ACL: 'remove' } + ]; + + exportEnvironments.sanitizeAttribs(environments); + + expect(exportEnvironments.environments['env-1'].ACL).to.be.undefined; + expect(exportEnvironments.environments['env-1'].name).to.equal('Production'); + }); + + it('should handle environments without name field', () => { + const environments = [ + { uid: 'env-1', ACL: 'remove' } + ]; + + exportEnvironments.sanitizeAttribs(environments); + + expect(exportEnvironments.environments['env-1']).to.exist; + expect(exportEnvironments.environments['env-1'].ACL).to.be.undefined; + }); + + it('should handle empty environments array', () => { + const environments: any[] = []; + + exportEnvironments.sanitizeAttribs(environments); + + expect(Object.keys(exportEnvironments.environments).length).to.equal(0); + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/extensions.test.ts b/packages/contentstack-export/test/unit/export/modules/extensions.test.ts new file mode 100644 index 0000000000..714e1954bc --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/extensions.test.ts @@ -0,0 +1,287 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility } from '@contentstack/cli-utilities'; +import ExportExtensions from '../../../../src/export/modules/extensions'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportExtensions', () => { + let exportExtensions: any; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + extension: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'ext-1', title: 'Extension 1' }, + { uid: 'ext-2', title: 'Extension 2' } + ], + count: 2 + }) + }) + }) + }; + + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + context: { + command: 'cm:stacks:export', + module: 'extensions', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: {}, + preserveStackVersion: false, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: '', + onlyTSModules: [], + modules: { + types: ['extensions'], + extensions: { + dirName: 'extensions', + fileName: 'extensions.json', + limit: 100, + invalidKeys: [] + } + } + } as any; + + exportExtensions = new ExportExtensions({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'extensions' + }); + + sinon.stub(FsUtility.prototype, 'writeFile').resolves(); + sinon.stub(FsUtility.prototype, 'makeDirectory').resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Constructor', () => { + it('should initialize with correct parameters', () => { + expect(exportExtensions).to.be.instanceOf(ExportExtensions); + }); + + it('should initialize extensions object', () => { + expect(exportExtensions.extensions).to.be.an('object'); + }); + + it('should set context module to extensions', () => { + expect(exportExtensions.exportConfig.context.module).to.equal('extensions'); + }); + }); + + describe('getExtensions() method', () => { + it('should fetch and process extensions correctly', async () => { + const extensions = [ + { uid: 'ext-1', title: 'Extension 1', SYS_ACL: 'test' }, + { uid: 'ext-2', title: 'Extension 2', SYS_ACL: 'test' } + ]; + + mockStackClient.extension.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: extensions, + count: 2 + }) + }) + }); + + await exportExtensions.getExtensions(); + + // Verify extensions were processed + expect(Object.keys(exportExtensions.extensions).length).to.equal(2); + expect(exportExtensions.extensions['ext-1']).to.exist; + expect(exportExtensions.extensions['ext-1'].title).to.equal('Extension 1'); + // Verify SYS_ACL was removed + expect(exportExtensions.extensions['ext-1'].SYS_ACL).to.be.undefined; + }); + + it('should call getExtensions recursively when more extensions exist', async () => { + let callCount = 0; + mockStackClient.extension.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: Array(100).fill({ uid: 'test', title: 'Test' }), + count: 150 + }); + } else { + return Promise.resolve({ + items: Array(50).fill({ uid: 'test2', title: 'Test2' }), + count: 150 + }); + } + }) + }) + }); + + await exportExtensions.getExtensions(); + + // Verify multiple calls were made for recursive fetching + expect(callCount).to.be.greaterThan(1); + }); + + it('should handle API errors gracefully', async () => { + mockStackClient.extension.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('API Error')) + }) + }); + + await exportExtensions.getExtensions(); + + // Verify method completes without throwing + expect(exportExtensions.extensions).to.exist; + }); + + it('should handle no items response and not process extensions', async () => { + mockStackClient.extension.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + const initialCount = Object.keys(exportExtensions.extensions).length; + await exportExtensions.getExtensions(); + + // Verify no new extensions were added + expect(Object.keys(exportExtensions.extensions).length).to.equal(initialCount); + }); + + it('should handle empty extensions array gracefully', async () => { + mockStackClient.extension.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: null, + count: 0 + }) + }) + }); + + const initialCount = Object.keys(exportExtensions.extensions).length; + await exportExtensions.getExtensions(); + + // Verify no processing occurred with null items + expect(Object.keys(exportExtensions.extensions).length).to.equal(initialCount); + }); + }); + + describe('start() method', () => { + it('should complete full export flow and write files', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + const extensions = [ + { uid: 'ext-1', title: 'Extension 1' }, + { uid: 'ext-2', title: 'Extension 2' } + ]; + + mockStackClient.extension.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: extensions, + count: 2 + }) + }) + }); + + await exportExtensions.start(); + + // Verify extensions were processed + expect(Object.keys(exportExtensions.extensions).length).to.equal(2); + expect(exportExtensions.extensions['ext-1']).to.exist; + expect(exportExtensions.extensions['ext-2']).to.exist; + // Verify file was written + expect(writeFileStub.called).to.be.true; + }); + + it('should handle empty extensions and log NOT_FOUND', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + mockStackClient.extension.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + exportExtensions.extensions = {}; + await exportExtensions.start(); + + // Verify writeFile was NOT called when extensions are empty + expect(writeFileStub.called).to.be.false; + }); + }); + + describe('sanitizeAttribs() method', () => { + it('should sanitize extension attributes and remove SYS_ACL', () => { + const extensions = [ + { uid: 'ext-1', title: 'Extension 1', SYS_ACL: 'remove' }, + { uid: 'ext-2', title: 'Extension 2', SYS_ACL: 'remove' } + ]; + + exportExtensions.sanitizeAttribs(extensions); + + expect(exportExtensions.extensions['ext-1'].SYS_ACL).to.be.undefined; + expect(exportExtensions.extensions['ext-1'].title).to.equal('Extension 1'); + }); + + it('should handle extensions without title field', () => { + const extensions = [ + { uid: 'ext-1', SYS_ACL: 'remove' } + ]; + + exportExtensions.sanitizeAttribs(extensions); + + expect(exportExtensions.extensions['ext-1']).to.exist; + expect(exportExtensions.extensions['ext-1'].SYS_ACL).to.be.undefined; + }); + + it('should handle empty extensions array', () => { + const extensions: any[] = []; + + exportExtensions.sanitizeAttribs(extensions); + + expect(Object.keys(exportExtensions.extensions).length).to.equal(0); + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/locales.test.ts b/packages/contentstack-export/test/unit/export/modules/locales.test.ts new file mode 100644 index 0000000000..5f76a2cd10 --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/locales.test.ts @@ -0,0 +1,323 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility } from '@contentstack/cli-utilities'; +import ExportLocales from '../../../../src/export/modules/locales'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportLocales', () => { + let exportLocales: any; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + locale: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'locale-1', code: 'en-us', name: 'English (US)', fallback_locale: null } + ], + count: 1 + }) + }) + }) + }; + + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + context: { + command: 'cm:stacks:export', + module: 'locales', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: { + userSession: '', + globalfields: '', + locales: '', + labels: '', + environments: '', + assets: '', + content_types: '', + entries: '', + users: '', + extension: '', + webhooks: '', + stacks: '' + }, + preserveStackVersion: false, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: '', + onlyTSModules: [], + modules: { + types: ['locales'], + locales: { + dirName: 'locales', + fileName: 'locales.json', + requiredKeys: ['code', 'name'] + }, + masterLocale: { + dirName: 'master_locale', + fileName: 'master_locale.json', + requiredKeys: ['code'] + } + } + } as any; + + exportLocales = new ExportLocales({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'locales' + }); + + // Stub FsUtility methods + sinon.stub(FsUtility.prototype, 'writeFile').resolves(); + sinon.stub(FsUtility.prototype, 'makeDirectory').resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Constructor', () => { + it('should initialize with correct parameters', () => { + expect(exportLocales).to.be.instanceOf(ExportLocales); + }); + + it('should set context module to locales', () => { + expect(exportLocales.exportConfig.context.module).to.equal('locales'); + }); + + it('should initialize locale config', () => { + expect(exportLocales.localeConfig).to.exist; + }); + + it('should initialize empty locales objects', () => { + expect(exportLocales.locales).to.be.an('object'); + expect(exportLocales.masterLocale).to.be.an('object'); + }); + }); + + describe('getLocales() method', () => { + it('should fetch and process locales correctly', async () => { + exportLocales.locales = {}; + exportLocales.masterLocale = {}; + exportLocales.exportConfig.master_locale = { code: 'en-us' }; + + const locales = [ + { uid: 'locale-1', code: 'en-us', name: 'English' }, + { uid: 'locale-2', code: 'es-es', name: 'Spanish' } + ]; + + exportLocales.stackAPIClient = { + locale: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: locales, + count: 2 + }) + }) + }) + }; + + await exportLocales.getLocales(); + + // Verify locales were processed + expect(Object.keys(exportLocales.locales).length).to.be.greaterThan(0); + expect(Object.keys(exportLocales.masterLocale).length).to.be.greaterThan(0); + }); + + it('should call getLocales recursively when more locales exist', async () => { + let callCount = 0; + mockStackClient.locale.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: Array(100).fill({ uid: `locale-${callCount}`, code: 'en' }), + count: 150 + }); + } else { + return Promise.resolve({ + items: Array(50).fill({ uid: `locale-${callCount}`, code: 'en' }), + count: 150 + }); + } + }) + }) + }); + + await exportLocales.getLocales(); + + // Verify multiple calls were made + expect(callCount).to.be.greaterThan(1); + }); + + it('should handle API errors and throw', async () => { + mockStackClient.locale.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('API Error')) + }) + }); + + try { + await exportLocales.getLocales(); + expect.fail('Should have thrown an error'); + } catch (error: any) { + expect(error).to.exist; + expect(error.message).to.include('API Error'); + } + }); + }); + + describe('start() method', () => { + it('should complete full export flow and write files', async () => { + exportLocales.exportConfig.master_locale = { code: 'en-us' }; + + exportLocales.stackAPIClient = { + locale: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'locale-1', code: 'en-us', name: 'English' }, + { uid: 'locale-2', code: 'es-es', name: 'Spanish' } + ], + count: 2 + }) + }) + }) + }; + + await exportLocales.start(); + + // Verify locales were fetched and processed + expect(Object.keys(exportLocales.locales).length).to.be.greaterThan(0); + // Verify writeFile was called (stub created in beforeEach) + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + expect(writeFileStub.called).to.be.true; + }); + + it('should handle errors during export', async () => { + exportLocales.stackAPIClient = { + locale: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('API Error')) + }) + }) + }; + + try { + await exportLocales.start(); + expect.fail('Should have thrown an error'); + } catch (error:any) { + expect(error).to.exist; + expect(error.message).to.include('API Error'); + } + }); + }); + + describe('getLocales() method', () => { + it('should handle no items response', async () => { + mockStackClient.locale.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + await exportLocales.getLocales(); + + expect(mockStackClient.locale.called).to.be.true; + }); + + it('should handle empty locales array', async () => { + mockStackClient.locale.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: null, + count: 0 + }) + }) + }); + + await exportLocales.getLocales(); + + expect(mockStackClient.locale.called).to.be.true; + }); + }); + + describe('sanitizeAttribs() method', () => { + it('should sanitize locale attributes', () => { + exportLocales.locales = {}; + exportLocales.masterLocale = {}; + + const locales = [ + { uid: 'locale-1', code: 'en-us', name: 'English', extraField: 'remove' }, + { uid: 'locale-2', code: 'es-es', name: 'Spanish', extraField: 'remove' } + ]; + + exportLocales.sanitizeAttribs(locales); + + expect(exportLocales.locales).to.be.an('object'); + }); + + it('should separate master locale from regular locales', () => { + exportLocales.locales = {}; + exportLocales.masterLocale = {}; + exportLocales.exportConfig.master_locale = { code: 'en-us' }; + + const locales = [ + { uid: 'locale-1', code: 'en-us', name: 'English' }, + { uid: 'locale-2', code: 'es-es', name: 'Spanish' } + ]; + + exportLocales.sanitizeAttribs(locales); + + // Master locale with code 'en-us' should be in masterLocale object + expect(Object.keys(exportLocales.masterLocale).length).to.be.greaterThan(0); + // Spanish locale should be in regular locales + expect(Object.keys(exportLocales.locales).length).to.be.greaterThan(0); + }); + + it('should handle empty locales array', () => { + exportLocales.locales = {}; + exportLocales.masterLocale = {}; + + const locales: any[] = []; + + exportLocales.sanitizeAttribs(locales); + + expect(Object.keys(exportLocales.locales).length).to.equal(0); + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/stack.test.ts b/packages/contentstack-export/test/unit/export/modules/stack.test.ts new file mode 100644 index 0000000000..6522a832d6 --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/stack.test.ts @@ -0,0 +1,315 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { log, FsUtility } from '@contentstack/cli-utilities'; +import ExportStack from '../../../../src/export/modules/stack'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportStack', () => { + let exportStack: ExportStack; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + fetch: sinon.stub().resolves({ name: 'Test Stack', uid: 'stack-uid', org_uid: 'org-uid' }), + settings: sinon.stub().resolves({ + name: 'Stack Settings', + description: 'Stack settings description', + settings: { global: { example: 'value' } } + }), + locale: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'locale-1', name: 'English (United States)', code: 'en-us', fallback_locale: null } + ], + count: 1 + }) + }) + }) + }; + + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + source_stack: 'test-stack', + preserveStackVersion: false, + hasOwnProperty: sinon.stub().returns(false), + org_uid: '', + sourceStackName: '', + context: { + command: 'cm:stacks:export', + module: 'stack', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + management_token: '', + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: { + userSession: '', + globalfields: '', + locales: '', + labels: '', + environments: '', + assets: '', + content_types: '', + entries: '', + users: '', + extension: '', + webhooks: '', + stacks: '' + }, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: '', + onlyTSModules: [], + modules: { + types: ['stack'], + locales: { + dirName: 'locales', + fileName: 'locales.json', + requiredKeys: ['code'] + }, + customRoles: { + dirName: 'custom_roles', + fileName: 'custom_roles.json', + customRolesLocalesFileName: '' + }, + 'custom-roles': { + dirName: 'custom_roles', + fileName: 'custom_roles.json', + customRolesLocalesFileName: '' + }, + environments: { + dirName: 'environments', + fileName: 'environments.json' + }, + labels: { + dirName: 'labels', + fileName: 'labels.json', + invalidKeys: [] + }, + webhooks: { + dirName: 'webhooks', + fileName: 'webhooks.json' + }, + releases: { + dirName: 'releases', + fileName: 'releases.json', + releasesList: 'releases_list.json', + invalidKeys: [] + }, + workflows: { + dirName: 'workflows', + fileName: 'workflows.json', + invalidKeys: [] + }, + globalfields: { + dirName: 'global_fields', + fileName: 'globalfields.json', + validKeys: ['title', 'uid'] + }, + 'global-fields': { + dirName: 'global_fields', + fileName: 'globalfields.json', + validKeys: ['title', 'uid'] + }, + assets: { + dirName: 'assets', + fileName: 'assets.json', + batchLimit: 100, + host: 'https://api.contentstack.io', + invalidKeys: [], + chunkFileSize: 5, + downloadLimit: 5, + fetchConcurrency: 5, + assetsMetaKeys: [], + securedAssets: false, + displayExecutionTime: false, + enableDownloadStatus: false, + includeVersionedAssets: false + }, + content_types: { + dirName: 'content_types', + fileName: 'content_types.json', + validKeys: ['title', 'uid'], + limit: 100 + }, + 'content-types': { + dirName: 'content_types', + fileName: 'content_types.json', + validKeys: ['title', 'uid'], + limit: 100 + }, + entries: { + dirName: 'entries', + fileName: 'entries.json', + invalidKeys: [], + batchLimit: 100, + downloadLimit: 5, + limit: 100, + exportVersions: false + }, + personalize: { + dirName: 'personalize', + baseURL: {} + }, + variantEntry: { + dirName: 'variant_entries', + fileName: 'variant_entries.json', + chunkFileSize: 5, + query: { skip: 0, limit: 100, include_variant: true, include_count: false, include_publish_details: true } + }, + extensions: { + dirName: 'extensions', + fileName: 'extensions.json' + }, + stack: { + dirName: 'stack', + fileName: 'stack.json', + limit: 100 + }, + dependency: { + entries: [] + }, + marketplace_apps: { + dirName: 'marketplace_apps', + fileName: 'marketplace_apps.json' + }, + 'marketplace-apps': { + dirName: 'marketplace_apps', + fileName: 'marketplace_apps.json' + }, + masterLocale: { + dirName: 'master_locale', + fileName: 'master_locale.json', + requiredKeys: ['code'] + }, + taxonomies: { + dirName: 'taxonomies', + fileName: 'taxonomies.json', + invalidKeys: [], + limit: 100 + }, + events: { + dirName: 'events', + fileName: 'events.json', + invalidKeys: [] + }, + audiences: { + dirName: 'audiences', + fileName: 'audiences.json', + invalidKeys: [] + }, + attributes: { + dirName: 'attributes', + fileName: 'attributes.json', + invalidKeys: [] + } + } + } as any; + + exportStack = new ExportStack({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'stack' + }); + + // Stub FsUtility methods + sinon.stub(FsUtility.prototype, 'writeFile').resolves(); + sinon.stub(FsUtility.prototype, 'makeDirectory').resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Constructor', () => { + it('should initialize with correct parameters', () => { + expect(exportStack).to.be.instanceOf(ExportStack); + }); + + it('should set context module to stack', () => { + expect((exportStack as any).exportConfig.context.module).to.equal('stack'); + }); + + it('should initialize stackConfig', () => { + expect((exportStack as any).stackConfig).to.exist; + }); + + it('should initialize query params', () => { + expect((exportStack as any).qs).to.deep.equal({ include_count: true }); + }); + }); + + describe('getStack() method', () => { + + + }); + + describe('getLocales() method', () => { + it('should fetch and return master locale', async () => { + const locale = await exportStack.getLocales(); + + expect(locale).to.exist; + expect(locale.code).to.equal('en-us'); + }); + + it('should handle error when fetching locales', async () => { + // Test error handling + const locale = await exportStack.getLocales(); + + expect(locale).to.exist; + }); + }); + + describe('exportStack() method', () => { + it('should export stack successfully', async () => { + await exportStack.exportStack(); + + // Should complete without error + }); + + it('should handle errors when exporting stack', async () => { + // Should handle error gracefully + await exportStack.exportStack(); + }); + }); + + describe('exportStackSettings() method', () => { + it('should export stack settings successfully', async () => { + await exportStack.exportStackSettings(); + + // Should complete without error + }); + + it('should handle errors when exporting settings', async () => { + // Should handle error gracefully + await exportStack.exportStackSettings(); + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts b/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts new file mode 100644 index 0000000000..bca3711835 --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts @@ -0,0 +1,310 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility } from '@contentstack/cli-utilities'; +import ExportTaxonomies from '../../../../src/export/modules/taxonomies'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportTaxonomies', () => { + let exportTaxonomies: any; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + taxonomy: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'taxonomy-1', name: 'Category' }, + { uid: 'taxonomy-2', name: 'Tag' } + ], + count: 2 + }) + }) + }) + }; + + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + context: { + command: 'cm:stacks:export', + module: 'taxonomies', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: {}, + preserveStackVersion: false, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: '', + onlyTSModules: [], + modules: { + types: ['taxonomies'], + taxonomies: { + dirName: 'taxonomies', + fileName: 'taxonomies.json', + invalidKeys: [], + limit: 100 + } + } + } as any; + + exportTaxonomies = new ExportTaxonomies({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'taxonomies' + }); + + sinon.stub(FsUtility.prototype, 'writeFile').resolves(); + sinon.stub(FsUtility.prototype, 'makeDirectory').resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Constructor', () => { + it('should initialize with correct parameters', () => { + expect(exportTaxonomies).to.be.instanceOf(ExportTaxonomies); + }); + + it('should initialize taxonomies object', () => { + expect(exportTaxonomies.taxonomies).to.be.an('object'); + }); + + it('should set context module to taxonomies', () => { + expect(exportTaxonomies.exportConfig.context.module).to.equal('taxonomies'); + }); + }); + + describe('getAllTaxonomies() method', () => { + it('should fetch and process taxonomies correctly', async () => { + const taxonomies = [ + { uid: 'taxonomy-1', name: 'Category', invalidField: 'remove' }, + { uid: 'taxonomy-2', name: 'Tag', invalidField: 'remove' } + ]; + + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: taxonomies, + count: 2 + }) + }) + }); + + await exportTaxonomies.getAllTaxonomies(); + + // Verify taxonomies were processed + expect(Object.keys(exportTaxonomies.taxonomies).length).to.equal(2); + expect(exportTaxonomies.taxonomies['taxonomy-1']).to.exist; + expect(exportTaxonomies.taxonomies['taxonomy-1'].name).to.equal('Category'); + }); + + it('should call getAllTaxonomies recursively when more taxonomies exist', async () => { + let callCount = 0; + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: Array(100).fill({ uid: `taxonomy-${callCount}`, name: 'Test' }), + count: 150 + }); + } else { + return Promise.resolve({ + items: Array(50).fill({ uid: `taxonomy-${callCount}`, name: 'Test' }), + count: 150 + }); + } + }) + }) + }); + + await exportTaxonomies.getAllTaxonomies(); + + // Verify multiple calls were made + expect(callCount).to.be.greaterThan(1); + }); + }); + + describe('start() method', () => { + it('should complete full export flow and call makeAPICall for each taxonomy', async () => { + const mockMakeAPICall = sinon.stub(exportTaxonomies, 'makeAPICall').resolves(); + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + // Mock getAllTaxonomies to return one taxonomy + const mockTaxonomy = { + uid: 'taxonomy-1', + name: 'Category' + }; + + // Mock the API call to return taxonomies + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [mockTaxonomy], + count: 1 + }) + }) + }); + + await exportTaxonomies.start(); + + // Verify makeAPICall was called for the taxonomy + expect(mockMakeAPICall.called).to.be.true; + expect(mockMakeAPICall.callCount).to.equal(1); + // Verify writeFile was called for taxonomies.json + expect(writeFileStub.called).to.be.true; + + mockMakeAPICall.restore(); + }); + + it('should handle empty taxonomies and not call makeAPICall', async () => { + const mockMakeAPICall = sinon.stub(exportTaxonomies, 'makeAPICall').resolves(); + + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + exportTaxonomies.taxonomies = {}; + await exportTaxonomies.start(); + + // Verify makeAPICall was NOT called when taxonomies are empty + expect(mockMakeAPICall.called).to.be.false; + + mockMakeAPICall.restore(); + }); + }); + + describe('getAllTaxonomies() method - edge cases', () => { + it('should handle no items response and not process taxonomies', async () => { + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + const initialCount = Object.keys(exportTaxonomies.taxonomies).length; + await exportTaxonomies.getAllTaxonomies(); + + // Verify no new taxonomies were added + expect(Object.keys(exportTaxonomies.taxonomies).length).to.equal(initialCount); + }); + + it('should handle empty taxonomies array gracefully', async () => { + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: null, + count: 0 + }) + }) + }); + + const initialCount = Object.keys(exportTaxonomies.taxonomies).length; + await exportTaxonomies.getAllTaxonomies(); + + // Verify no processing occurred with null items + expect(Object.keys(exportTaxonomies.taxonomies).length).to.equal(initialCount); + }); + + it('should handle API errors gracefully without crashing', async () => { + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('API Error')) + }) + }); + + await exportTaxonomies.getAllTaxonomies(); + + // Verify method completes without throwing + expect(exportTaxonomies.taxonomies).to.exist; + }); + + it('should handle count undefined scenario and use items length', async () => { + const taxonomies = [{ uid: 'taxonomy-1', name: 'Category' }]; + + mockStackClient.taxonomy.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: taxonomies, + count: undefined + }) + }) + }); + + await exportTaxonomies.getAllTaxonomies(); + + // Verify taxonomies were still processed despite undefined count + expect(exportTaxonomies.taxonomies['taxonomy-1']).to.exist; + }); + }); + + describe('sanitizeTaxonomiesAttribs() method', () => { + it('should sanitize taxonomy attributes', () => { + const taxonomies = [ + { uid: 'taxonomy-1', name: 'Category', invalidField: 'remove' }, + { uid: 'taxonomy-2', name: 'Tag', invalidField: 'remove' } + ]; + + exportTaxonomies.sanitizeTaxonomiesAttribs(taxonomies); + + expect(exportTaxonomies.taxonomies['taxonomy-1']).to.exist; + expect(exportTaxonomies.taxonomies['taxonomy-1'].name).to.equal('Category'); + }); + + it('should handle taxonomies without name field', () => { + const taxonomies = [ + { uid: 'taxonomy-1', invalidField: 'remove' } + ]; + + exportTaxonomies.sanitizeTaxonomiesAttribs(taxonomies); + + expect(exportTaxonomies.taxonomies['taxonomy-1']).to.exist; + }); + + it('should handle empty taxonomies array', () => { + const taxonomies: any[] = []; + + exportTaxonomies.sanitizeTaxonomiesAttribs(taxonomies); + + expect(Object.keys(exportTaxonomies.taxonomies).length).to.equal(0); + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/webhooks.test.ts b/packages/contentstack-export/test/unit/export/modules/webhooks.test.ts new file mode 100644 index 0000000000..01235e2de4 --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/webhooks.test.ts @@ -0,0 +1,231 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility } from '@contentstack/cli-utilities'; +import ExportWebhooks from '../../../../src/export/modules/webhooks'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportWebhooks', () => { + let exportWebhooks: any; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + webhook: sinon.stub().returns({ + fetchAll: sinon.stub().resolves({ + items: [ + { uid: 'webhook-1', name: 'Webhook 1' }, + { uid: 'webhook-2', name: 'Webhook 2' } + ], + count: 2 + }) + }) + }; + + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + context: { + command: 'cm:stacks:export', + module: 'webhooks', + userId: 'user-123', + email: 'test@example.com', + sessionId: 'session-123', + apiKey: 'test-api-key', + orgId: 'org-123', + authenticationMethod: 'Basic Auth' + }, + cliLogsPath: '/test/logs', + forceStopMarketplaceAppsPrompt: false, + master_locale: { code: 'en-us' }, + region: { + name: 'us', + cma: 'https://api.contentstack.io', + cda: 'https://cdn.contentstack.io', + uiHost: 'https://app.contentstack.com' + }, + skipStackSettings: false, + skipDependencies: false, + languagesCode: ['en'], + apis: {}, + preserveStackVersion: false, + personalizationEnabled: false, + fetchConcurrency: 5, + writeConcurrency: 5, + developerHubBaseUrl: '', + marketplaceAppEncryptionKey: '', + onlyTSModules: [], + modules: { + types: ['webhooks'], + webhooks: { + dirName: 'webhooks', + fileName: 'webhooks.json', + limit: 100, + invalidKeys: [] + } + } + } as any; + + exportWebhooks = new ExportWebhooks({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'webhooks' + }); + + // Stub FsUtility methods - created once in beforeEach + sinon.stub(FsUtility.prototype, 'writeFile').resolves(); + sinon.stub(FsUtility.prototype, 'makeDirectory').resolves(); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Constructor', () => { + it('should initialize with correct parameters', () => { + expect(exportWebhooks).to.be.instanceOf(ExportWebhooks); + }); + + it('should initialize webhooks object', () => { + expect(exportWebhooks.webhooks).to.be.an('object'); + }); + + it('should set context module to webhooks', () => { + expect(exportWebhooks.exportConfig.context.module).to.equal('webhooks'); + }); + }); + + describe('getWebhooks() method', () => { + it('should fetch and process webhooks correctly', async () => { + const webhooks = [ + { uid: 'webhook-1', name: 'Webhook 1', SYS_ACL: 'test' }, + { uid: 'webhook-2', name: 'Webhook 2', SYS_ACL: 'test' } + ]; + + mockStackClient.webhook.returns({ + fetchAll: sinon.stub().resolves({ + items: webhooks, + count: 2 + }) + }); + + await exportWebhooks.getWebhooks(); + + // Verify webhooks were processed and SYS_ACL was removed + expect(Object.keys(exportWebhooks.webhooks).length).to.equal(2); + expect(exportWebhooks.webhooks['webhook-1'].SYS_ACL).to.be.undefined; + expect(exportWebhooks.webhooks['webhook-1'].name).to.equal('Webhook 1'); + }); + + it('should call getWebhooks recursively when more webhooks exist', async () => { + let callCount = 0; + mockStackClient.webhook.returns({ + fetchAll: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: Array(100).fill({ uid: `webhook-${callCount}`, name: 'Test' }), + count: 150 + }); + } else { + return Promise.resolve({ + items: Array(50).fill({ uid: `webhook-${callCount}`, name: 'Test' }), + count: 150 + }); + } + }) + }); + + await exportWebhooks.getWebhooks(); + + // Verify multiple calls were made + expect(callCount).to.be.greaterThan(1); + }); + }); + + describe('start() method', () => { + it('should complete full export flow and write webhooks to file', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + + const webhooks = [ + { uid: 'webhook-1', name: 'Webhook 1' }, + { uid: 'webhook-2', name: 'Webhook 2' } + ]; + + mockStackClient.webhook.returns({ + fetchAll: sinon.stub().resolves({ + items: webhooks, + count: 2 + }) + }); + + await exportWebhooks.start(); + + // Verify webhooks were processed + expect(Object.keys(exportWebhooks.webhooks).length).to.equal(2); + expect(exportWebhooks.webhooks['webhook-1']).to.exist; + expect(exportWebhooks.webhooks['webhook-2']).to.exist; + // Verify file was written + expect(writeFileStub.called).to.be.true; + expect(makeDirectoryStub.called).to.be.true; + }); + + it('should handle empty webhooks and log NOT_FOUND', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + mockStackClient.webhook.returns({ + fetchAll: sinon.stub().resolves({ + items: [], + count: 0 + }) + }); + + exportWebhooks.webhooks = {}; + await exportWebhooks.start(); + + // Verify writeFile was NOT called when webhooks are empty + expect(writeFileStub.called).to.be.false; + }); + }); + + describe('sanitizeAttribs() method', () => { + it('should sanitize webhook attributes and remove SYS_ACL', () => { + const webhooks = [ + { uid: 'webhook-1', name: 'Webhook 1', SYS_ACL: 'remove' }, + { uid: 'webhook-2', name: 'Webhook 2', SYS_ACL: 'remove' } + ]; + + exportWebhooks.sanitizeAttribs(webhooks); + + expect(exportWebhooks.webhooks['webhook-1'].SYS_ACL).to.be.undefined; + expect(exportWebhooks.webhooks['webhook-1'].name).to.equal('Webhook 1'); + }); + + it('should handle webhooks without name field', () => { + const webhooks = [ + { uid: 'webhook-1', SYS_ACL: 'remove' } + ]; + + exportWebhooks.sanitizeAttribs(webhooks); + + expect(exportWebhooks.webhooks['webhook-1']).to.exist; + expect(exportWebhooks.webhooks['webhook-1'].SYS_ACL).to.be.undefined; + }); + + it('should handle empty webhooks array', () => { + const webhooks: any[] = []; + + exportWebhooks.sanitizeAttribs(webhooks); + + expect(Object.keys(exportWebhooks.webhooks).length).to.equal(0); + }); + }); +}); + From ee7fb00b81d814a81fdc39d26b6e5b25b9ddaebc Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Tue, 28 Oct 2025 13:44:02 +0530 Subject: [PATCH 2/2] add test cases in stack.test --- .../test/unit/export/modules/stack.test.ts | 148 ++++++++++++++++-- 1 file changed, 137 insertions(+), 11 deletions(-) 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 6522a832d6..828fdd85f0 100644 --- a/packages/contentstack-export/test/unit/export/modules/stack.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/stack.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import { log, FsUtility } from '@contentstack/cli-utilities'; +import { FsUtility } from '@contentstack/cli-utilities'; import ExportStack from '../../../../src/export/modules/stack'; import ExportConfig from '../../../../src/types/export-config'; @@ -276,40 +276,166 @@ describe('ExportStack', () => { expect(locale).to.exist; expect(locale.code).to.equal('en-us'); + expect(locale.name).to.equal('English (United States)'); + }); + + it('should recursively search for master locale across multiple pages', async () => { + let callCount = 0; + const localeStub = { + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + // First batch without master locale + return Promise.resolve({ + items: new Array(100).fill({ uid: 'locale-test', code: 'en', fallback_locale: 'en-us' }), + count: 150 + }); + } else { + // Second batch with master locale + return Promise.resolve({ + items: [{ uid: 'locale-master', code: 'en-us', fallback_locale: null, name: 'English' }], + count: 150 + }); + } + }) + }) + }; + + mockStackClient.locale.returns(localeStub); + const locale = await exportStack.getLocales(); + + expect(callCount).to.be.greaterThan(1); + expect(locale.code).to.equal('en-us'); }); it('should handle error when fetching locales', async () => { - // Test error handling + const localeStub = { + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('API Error')) + }) + }; + + mockStackClient.locale.returns(localeStub); + + try { + await exportStack.getLocales(); + expect.fail('Should have thrown an error'); + } catch (error) { + expect(error).to.exist; + } + }); + + it('should handle no items response and skip searching', async () => { + const localeStub = { + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }; + + mockStackClient.locale.returns(localeStub); const locale = await exportStack.getLocales(); - expect(locale).to.exist; + expect(locale).to.be.undefined; + }); + + it('should find master locale in first batch when present', async () => { + const localeStub = { + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'locale-1', code: 'es-es', fallback_locale: 'en-us' }, + { uid: 'locale-master', code: 'en-us', fallback_locale: null, name: 'English' } + ], + count: 2 + }) + }) + }; + + mockStackClient.locale.returns(localeStub); + const locale = await exportStack.getLocales(); + + expect(locale.code).to.equal('en-us'); }); }); describe('exportStack() method', () => { - it('should export stack successfully', async () => { + it('should export stack successfully and write to file', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + await exportStack.exportStack(); - // Should complete without error + expect(writeFileStub.called).to.be.true; + expect(makeDirectoryStub.called).to.be.true; }); - it('should handle errors when exporting stack', async () => { - // Should handle error gracefully + it('should handle errors when exporting stack without throwing', async () => { + mockStackClient.fetch = sinon.stub().rejects(new Error('Stack fetch failed')); + + // Should complete without throwing despite error + // The assertion is that await doesn't throw await exportStack.exportStack(); }); }); describe('exportStackSettings() method', () => { - it('should export stack settings successfully', async () => { + it('should export stack settings successfully and write to file', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + await exportStack.exportStackSettings(); - // Should complete without error + expect(writeFileStub.called).to.be.true; + expect(makeDirectoryStub.called).to.be.true; }); - it('should handle errors when exporting settings', async () => { - // Should handle error gracefully + it('should handle errors when exporting settings without throwing', async () => { + mockStackClient.settings = sinon.stub().rejects(new Error('Settings fetch failed')); + + // Should complete without throwing despite error + // The assertion is that await doesn't throw await exportStack.exportStackSettings(); }); }); + + describe('start() method', () => { + it('should export stack when preserveStackVersion is true', async () => { + const exportStackStub = sinon.stub(exportStack, 'exportStack').resolves({ name: 'test-stack' }); + const exportStackSettingsStub = sinon.stub(exportStack, 'exportStackSettings').resolves(); + const getStackStub = sinon.stub(exportStack, 'getStack').resolves({}); + + exportStack.exportConfig.preserveStackVersion = true; + + await exportStack.start(); + + expect(exportStackStub.called).to.be.true; + + exportStackStub.restore(); + exportStackSettingsStub.restore(); + getStackStub.restore(); + }); + + it('should skip exportStackSettings when management_token is present', async () => { + const getStackStub = sinon.stub(exportStack, 'getStack').resolves({}); + const exportStackSettingsSpy = sinon.spy(exportStack, 'exportStackSettings'); + + exportStack.exportConfig.management_token = 'some-token'; + exportStack.exportConfig.preserveStackVersion = false; + exportStack.exportConfig.master_locale = { code: 'en-us' }; + exportStack.exportConfig.hasOwnProperty = sinon.stub().returns(true); + + await exportStack.start(); + + // Verify exportStackSettings was NOT called + expect(exportStackSettingsSpy.called).to.be.false; + + getStackStub.restore(); + exportStackSettingsSpy.restore(); + }); + }); });