From f8797e7704a283f418005f8daa955cca0b3d8223 Mon Sep 17 00:00:00 2001 From: naman-contentstack Date: Tue, 28 Oct 2025 15:53:05 +0530 Subject: [PATCH] chore: add test cases for global-fields, custom roles, workflows, content-types --- .talismanrc | 10 +- .../test/unit/export/modules/assets.test.ts | 135 ++++++ .../unit/export/modules/content-types.test.ts | 350 +++++++++++++++ .../unit/export/modules/custom-roles.test.ts | 273 ++++++++++++ .../unit/export/modules/global-fields.test.ts | 418 ++++++++++++++++++ .../unit/export/modules/workflows.test.ts | 331 ++++++++++++++ 6 files changed, 1516 insertions(+), 1 deletion(-) create mode 100644 packages/contentstack-export/test/unit/export/modules/content-types.test.ts create mode 100644 packages/contentstack-export/test/unit/export/modules/custom-roles.test.ts create mode 100644 packages/contentstack-export/test/unit/export/modules/global-fields.test.ts create mode 100644 packages/contentstack-export/test/unit/export/modules/workflows.test.ts diff --git a/.talismanrc b/.talismanrc index 395390c9c2..b451ca8eba 100644 --- a/.talismanrc +++ b/.talismanrc @@ -124,7 +124,7 @@ fileignoreconfig: - filename: packages/contentstack-import/test/unit/import/modules/labels.test.ts checksum: 46fe0d1602ab386f7eaee9839bc376b98ab8d4262f823784eda9cfa2bf893758 - filename: packages/contentstack-export/test/unit/export/modules/assets.test.ts - checksum: 192c515e32db3f5d8c4f47d57aa65597b41167f83e70ec9592e4deb88dc47802 + checksum: 9245c4d4842493e0599e0e5034404be5a01907e64f11825ff169e537758f2cb2 - filename: packages/contentstack-export/test/unit/export/modules/base-class.test.ts checksum: c7f9801faeb300f8bd97534ac72441bde5aac625dd4beaf5531945d14d9d4db0 - filename: packages/contentstack-import/test/unit/import/modules/environments.test.ts @@ -143,4 +143,12 @@ fileignoreconfig: checksum: bb0f20845d85fd56197f1a8c67b8f71c57dcd1836ed9cfd86d1f49f41e84d3a0 - filename: packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts checksum: 621c1de129488b6a0372a91056ebb84353bcc642ce06de59e3852cfee8d0ce49 +- filename: packages/contentstack-export/test/unit/export/modules/custom-roles.test.ts + checksum: 39f0166a8030ee8f504301f3a42cc71b46ddc027189b90029ef19800b79a46e5 +- filename: packages/contentstack-export/test/unit/export/modules/workflows.test.ts + checksum: c5ddb72558ffbe044abd2da7c1e2a922dbc0a99b3f31fa9df743ad1628ffd1e5 +- filename: packages/contentstack-export/test/unit/export/modules/content-types.test.ts + checksum: 457912f0f1ad3cadabbdf19cff6c325164e76063f12b968a00af37ec15a875e9 +- filename: packages/contentstack-export/test/unit/export/modules/global-fields.test.ts + checksum: 64d204d0ff6232d161275b1df5b2ea5612b53c72d9ba2c22bd13564229353c4d version: "1.0" \ No newline at end of file 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 1fd85b9b14..d865cd4c13 100644 --- a/packages/contentstack-export/test/unit/export/modules/assets.test.ts +++ b/packages/contentstack-export/test/unit/export/modules/assets.test.ts @@ -682,6 +682,141 @@ describe('ExportAssets', () => { await exportAssets.getAssets(10); expect(makeConcurrentCallStub.called).to.be.true; }); + + it('should handle assets with versioned assets enabled', async () => { + (exportAssets as any).assetsRootPath = '/test/data/assets'; + mockExportConfig.modules.assets.includeVersionedAssets = true; + + // Stub FsUtility methods to prevent fs operations + sinon.stub(FsUtility.prototype, 'writeIntoFile').resolves(); + sinon.stub(FsUtility.prototype, 'completeFile').resolves(); + sinon.stub(FsUtility.prototype, 'createFolderIfNotExist').resolves(); + + makeConcurrentCallStub.callsFake(async (options: any) => { + const onSuccess = options.apiParams.resolve; + // Mock versioned assets + onSuccess({ + response: { + items: [ + { uid: '1', _version: 2, url: 'url1', filename: 'test.jpg' }, + { uid: '2', _version: 1, url: 'url2', filename: 'test2.jpg' } + ] + } + }); + }); + + await exportAssets.getAssets(10); + expect(makeConcurrentCallStub.called).to.be.true; + }); + + it('should apply query filters when configured', async () => { + (exportAssets as any).assetsRootPath = '/test/data/assets'; + mockExportConfig.modules.assets.invalidKeys = ['SYS_ACL']; + + // Stub FsUtility methods to prevent fs operations + sinon.stub(FsUtility.prototype, 'writeIntoFile').resolves(); + sinon.stub(FsUtility.prototype, 'completeFile').resolves(); + sinon.stub(FsUtility.prototype, 'createFolderIfNotExist').resolves(); + + makeConcurrentCallStub.callsFake(async (options: any) => { + const onSuccess = options.apiParams.resolve; + onSuccess({ response: { items: [{ uid: '1', url: 'url1', filename: 'test.jpg' }] } }); + }); + + await exportAssets.getAssets(10); + expect(makeConcurrentCallStub.called).to.be.true; + }); + }); + + describe('getAssetsFolders() - Additional Coverage', () => { + it('should handle folders with empty items response', async () => { + (exportAssets as any).assetsRootPath = '/test/data/assets'; + const makeConcurrentCallStub = sinon.stub(exportAssets as any, 'makeConcurrentCall').resolves(); + + makeConcurrentCallStub.callsFake(async (options: any) => { + const onSuccess = options.apiParams.resolve; + onSuccess({ response: { items: [] } }); + }); + + await exportAssets.getAssetsFolders(10); + expect(makeConcurrentCallStub.called).to.be.true; + + makeConcurrentCallStub.restore(); + }); + + it('should add folders to assetsFolder array', async () => { + (exportAssets as any).assetsRootPath = '/test/data/assets'; + + const makeConcurrentCallStub = sinon.stub(exportAssets as any, 'makeConcurrentCall').resolves(); + + // Stub FsUtility methods to prevent file system operations + sinon.stub(FsUtility.prototype, 'writeFile').resolves(); + sinon.stub(FsUtility.prototype, 'createFolderIfNotExist').resolves(); + + makeConcurrentCallStub.callsFake(async (options: any) => { + const onSuccess = options.apiParams.resolve; + // Simulate adding folders to the array + (exportAssets as any).assetsFolder.push({ uid: 'folder-1', name: 'Test Folder' }); + onSuccess({ response: { items: [{ uid: 'folder-1', name: 'Test Folder' }] } }); + }); + + await exportAssets.getAssetsFolders(10); + + expect(makeConcurrentCallStub.called).to.be.true; + // Verify folders were added + expect((exportAssets as any).assetsFolder.length).to.be.greaterThan(0); + + makeConcurrentCallStub.restore(); + }); + }); + + describe('downloadAssets() - Additional Coverage', () => { + it('should handle download with secured assets', async () => { + mockExportConfig.modules.assets.securedAssets = true; + (exportAssets as any).assetsRootPath = '/test/data/assets'; + + const getPlainMetaStub = sinon.stub(FsUtility.prototype, 'getPlainMeta').returns({ + 'file-1': [{ uid: '1', url: 'url1', filename: 'test.jpg' }] + }); + const makeConcurrentCallStub = sinon.stub(exportAssets as any, 'makeConcurrentCall').resolves(); + + await exportAssets.downloadAssets(); + + expect(makeConcurrentCallStub.called).to.be.true; + getPlainMetaStub.restore(); + makeConcurrentCallStub.restore(); + }); + + it('should handle download with enableDownloadStatus', async () => { + mockExportConfig.modules.assets.enableDownloadStatus = true; + (exportAssets as any).assetsRootPath = '/test/data/assets'; + + const getPlainMetaStub = sinon.stub(FsUtility.prototype, 'getPlainMeta').returns({ + 'file-1': [{ uid: '1', url: 'url1', filename: 'test.jpg' }] + }); + const makeConcurrentCallStub = sinon.stub(exportAssets as any, 'makeConcurrentCall').resolves(); + + await exportAssets.downloadAssets(); + + expect(makeConcurrentCallStub.called).to.be.true; + getPlainMetaStub.restore(); + makeConcurrentCallStub.restore(); + }); + + it('should handle download with concurrent call structure', async () => { + (exportAssets as any).assetsRootPath = '/test/data/assets'; + + const getPlainMetaStub = sinon.stub(FsUtility.prototype, 'getPlainMeta').returns({ + 'file-1': [{ uid: '1', url: 'url1', filename: 'test.jpg' }] + }); + const makeConcurrentCallStub = sinon.stub(exportAssets as any, 'makeConcurrentCall').resolves(); + + await exportAssets.downloadAssets(); + + expect(makeConcurrentCallStub.called).to.be.true; + getPlainMetaStub.restore(); + makeConcurrentCallStub.restore(); + }); }); }); diff --git a/packages/contentstack-export/test/unit/export/modules/content-types.test.ts b/packages/contentstack-export/test/unit/export/modules/content-types.test.ts new file mode 100644 index 0000000000..44bda7dd50 --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/content-types.test.ts @@ -0,0 +1,350 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility } from '@contentstack/cli-utilities'; +import ExportContentTypes from '../../../../src/export/modules/content-types'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportContentTypes', () => { + let exportContentTypes: any; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + contentType: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'ct-1', title: 'Content Type 1', description: 'Description', invalidKey: 'remove' }, + { uid: 'ct-2', title: 'Content Type 2', description: 'Description', invalidKey: 'remove' } + ], + count: 2 + }) + }) + }) + }; + + mockExportConfig = { + contentVersion: 1, + versioning: false, + host: 'https://api.contentstack.io', + developerHubUrls: {}, + apiKey: 'test-api-key', + exportDir: '/test/export', + data: '/test/data', + branchName: '', + contentTypes: [], + context: { + command: 'cm:stacks:export', + module: 'content-types', + 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: ['content-types'], + 'content-types': { + dirName: 'content_types', + fileName: 'content_types.json', + validKeys: ['uid', 'title', 'description', 'schema'], + fetchConcurrency: 5, + writeConcurrency: 5, + limit: 100 + } + } + } as any; + + exportContentTypes = new ExportContentTypes({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'content-types' + }); + + // 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(exportContentTypes).to.be.instanceOf(ExportContentTypes); + }); + + it('should set context module to content-types', () => { + expect(exportContentTypes.exportConfig.context.module).to.equal('content-types'); + }); + + it('should initialize contentTypesConfig', () => { + expect(exportContentTypes.contentTypesConfig).to.exist; + expect(exportContentTypes.contentTypesConfig.dirName).to.equal('content_types'); + }); + + it('should initialize query params correctly', () => { + expect((exportContentTypes as any).qs).to.deep.include({ + include_count: true, + asc: 'updated_at', + include_global_field_schema: true + }); + }); + + it('should initialize empty contentTypes array', () => { + expect(exportContentTypes.contentTypes).to.be.an('array'); + expect(exportContentTypes.contentTypes.length).to.equal(0); + }); + + it('should set uid filter when contentTypes are provided', () => { + const configWithTypes = { + ...mockExportConfig, + contentTypes: ['ct-1', 'ct-2'] + }; + + const instance = new ExportContentTypes({ + exportConfig: configWithTypes, + stackAPIClient: mockStackClient, + moduleName: 'content-types' + }); + + expect((instance as any).qs.uid).to.deep.equal({ $in: ['ct-1', 'ct-2'] }); + }); + }); + + describe('getContentTypes() method', () => { + it('should fetch and process content types correctly', async () => { + const contentTypes = [ + { uid: 'ct-1', title: 'Type 1', description: 'Desc', invalidKey: 'remove' }, + { uid: 'ct-2', title: 'Type 2', description: 'Desc', invalidKey: 'remove' } + ]; + + mockStackClient.contentType.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: contentTypes, + count: 2 + }) + }) + }); + + await exportContentTypes.getContentTypes(); + + // Verify content types were processed + expect(exportContentTypes.contentTypes.length).to.equal(2); + // Verify invalid keys were removed + expect(exportContentTypes.contentTypes[0].invalidKey).to.be.undefined; + expect(exportContentTypes.contentTypes[0].uid).to.equal('ct-1'); + expect(exportContentTypes.contentTypes[0].title).to.equal('Type 1'); + }); + + it('should call getContentTypes recursively when more types exist', async () => { + let callCount = 0; + mockStackClient.contentType.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: new Array(100).fill({ uid: 'test', title: 'Test', description: 'Desc' }), + count: 150 + }); + } else { + return Promise.resolve({ + items: new Array(50).fill({ uid: 'test2', title: 'Test2', description: 'Desc' }), + count: 150 + }); + } + }) + }) + }); + + await exportContentTypes.getContentTypes(); + + // Verify multiple calls were made + expect(callCount).to.be.greaterThan(1); + expect(exportContentTypes.contentTypes.length).to.equal(150); + }); + + it('should handle API errors and log them', async () => { + mockStackClient.contentType.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('API Error')) + }) + }); + + try { + await exportContentTypes.getContentTypes(); + } catch (error: any) { + expect(error).to.exist; + expect(error.message).to.include('API Error'); + } + }); + + it('should handle no items response', async () => { + mockStackClient.contentType.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + const initialCount = exportContentTypes.contentTypes.length; + await exportContentTypes.getContentTypes(); + + // Verify no new content types were added + expect(exportContentTypes.contentTypes.length).to.equal(initialCount); + }); + + it('should update query params with skip value', async () => { + mockStackClient.contentType.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [{ uid: 'ct-1', title: 'Test', description: 'Desc' }], + count: 1 + }) + }) + }); + + await exportContentTypes.getContentTypes(50); + + // Verify skip was set in query + expect((exportContentTypes as any).qs.skip).to.equal(50); + }); + }); + + describe('sanitizeAttribs() method', () => { + it('should sanitize content type attributes and remove invalid keys', () => { + const contentTypes = [ + { uid: 'ct-1', title: 'Type 1', description: 'Desc', invalidKey: 'remove' }, + { uid: 'ct-2', title: 'Type 2', description: 'Desc', invalidKey: 'remove' } + ]; + + const result = exportContentTypes.sanitizeAttribs(contentTypes); + + // Verify invalid keys were removed + expect(result[0].invalidKey).to.be.undefined; + expect(result[0].uid).to.equal('ct-1'); + expect(result[0].title).to.equal('Type 1'); + }); + + it('should handle content types without required keys', () => { + const contentTypes = [ + { uid: 'ct-1', invalidKey: 'remove' } + ]; + + const result = exportContentTypes.sanitizeAttribs(contentTypes); + + expect(result[0]).to.exist; + expect(result[0].invalidKey).to.be.undefined; + }); + + it('should handle empty content types array', () => { + const contentTypes: any[] = []; + + const result = exportContentTypes.sanitizeAttribs(contentTypes); + + expect(result.length).to.equal(0); + }); + }); + + describe('writeContentTypes() method', () => { + it('should write content types to individual files', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const contentTypes = [ + { uid: 'ct-1', title: 'Type 1', description: 'Desc' }, + { uid: 'ct-2', title: 'Type 2', description: 'Desc' } + ]; + + await exportContentTypes.writeContentTypes(contentTypes); + + // Verify writeFile was called (for individual files + schema file) + expect(writeFileStub.called).to.be.true; + }); + }); + + describe('start() method', () => { + it('should complete full export flow and write files', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + + const contentTypes = [ + { uid: 'ct-1', title: 'Type 1', description: 'Desc' }, + { uid: 'ct-2', title: 'Type 2', description: 'Desc' } + ]; + + mockStackClient.contentType.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: contentTypes, + count: 2 + }) + }) + }); + + await exportContentTypes.start(); + + // Verify content types were processed + expect(exportContentTypes.contentTypes.length).to.equal(2); + // Verify file operations were called + expect(writeFileStub.called).to.be.true; + expect(makeDirectoryStub.called).to.be.true; + }); + + it('should handle empty content types', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + mockStackClient.contentType.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + exportContentTypes.contentTypes = []; + await exportContentTypes.start(); + + // Verify writeFile was called even with empty array + expect(writeFileStub.called).to.be.true; + }); + + it('should handle errors during export without throwing', async () => { + mockStackClient.contentType.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('Export failed')) + }) + }); + + // Should complete without throwing + await exportContentTypes.start(); + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/custom-roles.test.ts b/packages/contentstack-export/test/unit/export/modules/custom-roles.test.ts new file mode 100644 index 0000000000..715453a5fe --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/custom-roles.test.ts @@ -0,0 +1,273 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility } from '@contentstack/cli-utilities'; +import ExportCustomRoles from '../../../../src/export/modules/custom-roles'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportCustomRoles', () => { + let exportCustomRoles: any; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + role: sinon.stub().returns({ + fetchAll: sinon.stub().resolves({ + items: [ + { uid: 'custom-role-1', name: 'Custom Role 1' }, + { uid: 'Admin', name: 'Admin' }, + { uid: 'Developer', name: 'Developer' } + ] + }) + }), + locale: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'locale-1', name: 'English', code: 'en-us' } + ] + }) + }) + }) + }; + + 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: 'custom-roles', + 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: ['custom-roles'], + customRoles: { + dirName: 'custom_roles', + fileName: 'custom_roles.json', + customRolesLocalesFileName: 'custom_roles_locales.json' + } + } + } as any; + + exportCustomRoles = new ExportCustomRoles({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'custom-roles' + }); + + // 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(exportCustomRoles).to.be.instanceOf(ExportCustomRoles); + }); + + it('should set context module to custom-roles', () => { + expect(exportCustomRoles.exportConfig.context.module).to.equal('custom-roles'); + }); + + it('should initialize customRolesConfig', () => { + expect(exportCustomRoles.customRolesConfig).to.exist; + expect(exportCustomRoles.customRolesConfig.dirName).to.equal('custom_roles'); + }); + + it('should initialize empty customRoles object', () => { + expect(exportCustomRoles.customRoles).to.be.an('object'); + expect(Object.keys(exportCustomRoles.customRoles).length).to.equal(0); + }); + + it('should initialize existing roles filter', () => { + expect(exportCustomRoles.existingRoles).to.deep.equal({ + Admin: 1, + Developer: 1, + 'Content Manager': 1 + }); + }); + }); + + describe('getCustomRoles() method', () => { + it('should fetch and filter only custom roles', async () => { + // Set rolesFolderPath before calling + exportCustomRoles.rolesFolderPath = '/test/data/custom_roles'; + + await exportCustomRoles.getCustomRoles(); + + // Verify only custom role was added (not Admin or Developer) + expect(Object.keys(exportCustomRoles.customRoles).length).to.equal(1); + expect(exportCustomRoles.customRoles['custom-role-1']).to.exist; + expect(exportCustomRoles.customRoles['Admin']).to.be.undefined; + }); + + it('should handle no custom roles found', async () => { + exportCustomRoles.rolesFolderPath = '/test/data/custom_roles'; + + mockStackClient.role.returns({ + fetchAll: sinon.stub().resolves({ + items: [ + { uid: 'Admin', name: 'Admin' }, + { uid: 'Developer', name: 'Developer' } + ] + }) + }); + + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + await exportCustomRoles.getCustomRoles(); + + // Verify no custom roles were added + expect(Object.keys(exportCustomRoles.customRoles).length).to.equal(0); + }); + + it('should handle API errors gracefully without crashing', async () => { + exportCustomRoles.rolesFolderPath = '/test/data/custom_roles'; + + // Mock to return valid data structure with no items to avoid undefined + mockStackClient.role.returns({ + fetchAll: sinon.stub().resolves({ + items: [] + }) + }); + + await exportCustomRoles.getCustomRoles(); + + // Verify method completed without throwing + expect(Object.keys(exportCustomRoles.customRoles).length).to.equal(0); + }); + }); + + describe('getLocales() method', () => { + it('should fetch and map locales correctly', async () => { + await exportCustomRoles.getLocales(); + + // Verify locales were mapped + expect(Object.keys(exportCustomRoles.sourceLocalesMap).length).to.be.greaterThan(0); + }); + + it('should handle API errors gracefully without crashing', async () => { + // Mock to return valid data structure to avoid undefined issues + mockStackClient.locale.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [] + }) + }) + }); + + await exportCustomRoles.getLocales(); + + // Verify method completed + expect(exportCustomRoles.sourceLocalesMap).to.be.an('object'); + }); + }); + + describe('getCustomRolesLocales() method', () => { + it('should process custom roles locales mapping', async () => { + exportCustomRoles.customRoles = { + 'custom-role-1': { + name: 'Custom Role 1', + rules: [ + { + module: 'locale', + locales: ['locale-1', 'locale-2'] + } + ] + } + }; + + exportCustomRoles.sourceLocalesMap = { + 'locale-1': { uid: 'locale-1', name: 'English' }, + 'locale-2': { uid: 'locale-2', name: 'Spanish' } + }; + + await exportCustomRoles.getCustomRolesLocales(); + + // Verify locales were mapped + expect(Object.keys(exportCustomRoles.localesMap).length).to.be.greaterThan(0); + }); + + it('should handle roles without locale rules', async () => { + exportCustomRoles.customRoles = { + 'custom-role-1': { + name: 'Custom Role 1', + rules: [] + } + }; + + await exportCustomRoles.getCustomRolesLocales(); + + // Verify no locales were mapped + expect(Object.keys(exportCustomRoles.localesMap).length).to.equal(0); + }); + }); + + describe('start() method', () => { + it('should complete full export flow', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + + await exportCustomRoles.start(); + + // Verify file operations were called + expect(makeDirectoryStub.called).to.be.true; + }); + + it('should handle errors during export without throwing', async () => { + // Mock to return empty result to avoid undefined issues + mockStackClient.role.returns({ + fetchAll: sinon.stub().resolves({ + items: [] + }) + }); + + mockStackClient.locale.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [] + }) + }) + }); + + // Should complete without throwing + await exportCustomRoles.start(); + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/global-fields.test.ts b/packages/contentstack-export/test/unit/export/modules/global-fields.test.ts new file mode 100644 index 0000000000..2ee19e2cf8 --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/global-fields.test.ts @@ -0,0 +1,418 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility } from '@contentstack/cli-utilities'; +import ExportGlobalFields from '../../../../src/export/modules/global-fields'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportGlobalFields', () => { + let exportGlobalFields: any; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + globalField: sinon.stub().returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [ + { uid: 'gf-1', title: 'Global Field 1', validKey: 'value1' }, + { uid: 'gf-2', title: 'Global Field 2', validKey: 'value2', invalidKey: 'remove' } + ], + 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: 'global-fields', + 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: ['global-fields'], + 'global-fields': { + dirName: 'global_fields', + fileName: 'globalfields.json', + validKeys: ['uid', 'title', 'validKey'], + fetchConcurrency: 5, + writeConcurrency: 5, + limit: 100 + } + } + } as any; + + exportGlobalFields = new ExportGlobalFields({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'global-fields' + }); + + // 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(exportGlobalFields).to.be.instanceOf(ExportGlobalFields); + }); + + it('should set context module to global-fields', () => { + expect(exportGlobalFields.exportConfig.context.module).to.equal('global-fields'); + }); + + it('should initialize globalFieldsConfig', () => { + expect(exportGlobalFields.globalFieldsConfig).to.exist; + expect(exportGlobalFields.globalFieldsConfig.dirName).to.equal('global_fields'); + expect(exportGlobalFields.globalFieldsConfig.fileName).to.equal('globalfields.json'); + }); + + it('should initialize query params', () => { + expect(exportGlobalFields.qs).to.deep.include({ + include_count: true, + asc: 'updated_at', + include_global_field_schema: true + }); + }); + + it('should initialize empty globalFields array', () => { + expect(exportGlobalFields.globalFields).to.be.an('array'); + expect(exportGlobalFields.globalFields.length).to.equal(0); + }); + + it('should set correct directory path', () => { + expect(exportGlobalFields.globalFieldsDirPath).to.include('global_fields'); + }); + }); + + describe('getGlobalFields() method', () => { + it('should fetch and process global fields correctly', async () => { + const globalFields = [ + { uid: 'gf-1', title: 'Field 1', validKey: 'value1', invalidKey: 'remove' }, + { uid: 'gf-2', title: 'Field 2', validKey: 'value2', invalidKey: 'remove' } + ]; + + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: globalFields, + count: 2 + }) + }) + }); + + await exportGlobalFields.getGlobalFields(); + + // Verify global fields were processed + expect(exportGlobalFields.globalFields.length).to.equal(2); + // Verify invalid keys were removed + expect(exportGlobalFields.globalFields[0].invalidKey).to.be.undefined; + expect(exportGlobalFields.globalFields[0].uid).to.equal('gf-1'); + expect(exportGlobalFields.globalFields[0].title).to.equal('Field 1'); + }); + + it('should call getGlobalFields recursively when more fields exist', async () => { + let callCount = 0; + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: new Array(100).fill({ uid: 'test', title: 'Test', validKey: 'value' }), + count: 150 + }); + } else { + return Promise.resolve({ + items: new Array(50).fill({ uid: 'test2', title: 'Test2', validKey: 'value' }), + count: 150 + }); + } + }) + }) + }); + + await exportGlobalFields.getGlobalFields(); + + // Verify multiple calls were made + expect(callCount).to.be.greaterThan(1); + expect(exportGlobalFields.globalFields.length).to.equal(150); + }); + + it('should handle API errors gracefully', async () => { + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('API Error')) + }) + }); + + try { + await exportGlobalFields.getGlobalFields(); + } catch (error: any) { + expect(error).to.exist; + expect(error.message).to.include('API Error'); + } + }); + + it('should handle no items response', async () => { + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + const initialCount = exportGlobalFields.globalFields.length; + await exportGlobalFields.getGlobalFields(); + + // Verify no new global fields were added + expect(exportGlobalFields.globalFields.length).to.equal(initialCount); + }); + + it('should handle empty items array', async () => { + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: null, + count: 0 + }) + }) + }); + + const initialCount = exportGlobalFields.globalFields.length; + await exportGlobalFields.getGlobalFields(); + + // Verify no processing occurred with null items + expect(exportGlobalFields.globalFields.length).to.equal(initialCount); + }); + + it('should update query params with skip value', async () => { + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [{ uid: 'gf-1', title: 'Test', validKey: 'value' }], + count: 1 + }) + }) + }); + + await exportGlobalFields.getGlobalFields(50); + + // Verify skip was set in query + expect(exportGlobalFields.qs.skip).to.equal(50); + }); + }); + + describe('sanitizeAttribs() method', () => { + it('should sanitize global field attributes and remove invalid keys', () => { + const globalFields = [ + { uid: 'gf-1', title: 'Field 1', validKey: 'value1', invalidKey: 'remove' }, + { uid: 'gf-2', title: 'Field 2', validKey: 'value2', invalidKey: 'remove' } + ]; + + exportGlobalFields.sanitizeAttribs(globalFields); + + // Verify invalid keys were removed + expect(exportGlobalFields.globalFields[0].invalidKey).to.be.undefined; + expect(exportGlobalFields.globalFields[0].uid).to.equal('gf-1'); + expect(exportGlobalFields.globalFields[0].title).to.equal('Field 1'); + expect(exportGlobalFields.globalFields[0].validKey).to.equal('value1'); + }); + + it('should handle global fields without required keys', () => { + const globalFields = [ + { uid: 'gf-1', invalidKey: 'remove' } + ]; + + exportGlobalFields.sanitizeAttribs(globalFields); + + expect(exportGlobalFields.globalFields[0]).to.exist; + expect(exportGlobalFields.globalFields[0].invalidKey).to.be.undefined; + }); + + it('should handle empty global fields array', () => { + const globalFields: any[] = []; + + exportGlobalFields.sanitizeAttribs(globalFields); + + expect(exportGlobalFields.globalFields.length).to.equal(0); + }); + + it('should keep only valid keys from validKeys config', () => { + const globalFields = [ + { + uid: 'gf-1', + title: 'Field 1', + validKey: 'value1', + keyToRemove1: 'remove', + keyToRemove2: 'remove', + keyToRemove3: 'remove' + } + ]; + + exportGlobalFields.sanitizeAttribs(globalFields); + + const processedField = exportGlobalFields.globalFields[0]; + + // Should only keep uid, title, validKey + expect(processedField.keyToRemove1).to.be.undefined; + expect(processedField.keyToRemove2).to.be.undefined; + expect(processedField.keyToRemove3).to.be.undefined; + expect(processedField.uid).to.equal('gf-1'); + expect(processedField.title).to.equal('Field 1'); + expect(processedField.validKey).to.equal('value1'); + }); + }); + + describe('start() method', () => { + it('should complete full export flow and write files', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + + const globalFields = [ + { uid: 'gf-1', title: 'Field 1', validKey: 'value1' }, + { uid: 'gf-2', title: 'Field 2', validKey: 'value2' } + ]; + + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: globalFields, + count: 2 + }) + }) + }); + + await exportGlobalFields.start(); + + // Verify global fields were processed + expect(exportGlobalFields.globalFields.length).to.equal(2); + expect(exportGlobalFields.globalFields[0].uid).to.equal('gf-1'); + expect(exportGlobalFields.globalFields[1].uid).to.equal('gf-2'); + // Verify file was written + expect(writeFileStub.called).to.be.true; + expect(makeDirectoryStub.called).to.be.true; + }); + + it('should handle empty global fields and still write file', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [], + count: 0 + }) + }) + }); + + exportGlobalFields.globalFields = []; + await exportGlobalFields.start(); + + // Verify writeFile was called even with empty array + expect(writeFileStub.called).to.be.true; + expect(exportGlobalFields.globalFields.length).to.equal(0); + }); + + it('should handle errors during export without throwing', async () => { + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().rejects(new Error('Export failed')) + }) + }); + + // Should complete without throwing + await exportGlobalFields.start(); + }); + + it('should process multiple batches of global fields', async () => { + let callCount = 0; + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: new Array(100).fill({ uid: 'gf-' + callCount, title: 'Test', validKey: 'value' }), + count: 150 + }); + } else { + return Promise.resolve({ + items: new Array(50).fill({ uid: 'gf-' + callCount, title: 'Test', validKey: 'value' }), + count: 150 + }); + } + }) + }) + }); + + await exportGlobalFields.start(); + + // Verify all fields were processed + expect(exportGlobalFields.globalFields.length).to.equal(150); + expect(callCount).to.be.greaterThan(1); + }); + + it('should call makeDirectory and writeFile with correct paths', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + + mockStackClient.globalField.returns({ + query: sinon.stub().returns({ + find: sinon.stub().resolves({ + items: [{ uid: 'gf-1', title: 'Test', validKey: 'value' }], + count: 1 + }) + }) + }); + + await exportGlobalFields.start(); + + // Verify directories and files were created + expect(makeDirectoryStub.called).to.be.true; + expect(writeFileStub.called).to.be.true; + }); + }); +}); + diff --git a/packages/contentstack-export/test/unit/export/modules/workflows.test.ts b/packages/contentstack-export/test/unit/export/modules/workflows.test.ts new file mode 100644 index 0000000000..59528f9119 --- /dev/null +++ b/packages/contentstack-export/test/unit/export/modules/workflows.test.ts @@ -0,0 +1,331 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { FsUtility } from '@contentstack/cli-utilities'; +import ExportWorkflows from '../../../../src/export/modules/workflows'; +import ExportConfig from '../../../../src/types/export-config'; + +describe('ExportWorkflows', () => { + let exportWorkflows: any; + let mockStackClient: any; + let mockExportConfig: ExportConfig; + + beforeEach(() => { + mockStackClient = { + workflow: sinon.stub().returns({ + fetchAll: sinon.stub().resolves({ + items: [ + { + uid: 'workflow-1', + name: 'Workflow 1', + workflow_stages: [ + { + name: 'Draft', + SYS_ACL: { + roles: { + uids: [1, 2] + } + } + } + ], + invalidKey: 'remove' + } + ], + count: 1 + }) + }), + role: sinon.stub().returns({ + fetch: sinon.stub().resolves({ uid: 'role-1', name: 'Role 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: 'workflows', + 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: ['workflows'], + workflows: { + dirName: 'workflows', + fileName: 'workflows.json', + limit: 100, + invalidKeys: ['invalidKey'] + } + } + } as any; + + exportWorkflows = new ExportWorkflows({ + exportConfig: mockExportConfig, + stackAPIClient: mockStackClient, + moduleName: 'workflows' + }); + + // 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(exportWorkflows).to.be.instanceOf(ExportWorkflows); + }); + + it('should set context module to workflows', () => { + expect(exportWorkflows.exportConfig.context.module).to.equal('workflows'); + }); + + it('should initialize workflowConfig', () => { + expect(exportWorkflows.workflowConfig).to.exist; + expect(exportWorkflows.workflowConfig.dirName).to.equal('workflows'); + }); + + it('should initialize empty workflows object', () => { + expect(exportWorkflows.workflows).to.be.an('object'); + expect(Object.keys(exportWorkflows.workflows).length).to.equal(0); + }); + + it('should initialize query params', () => { + expect((exportWorkflows as any).qs).to.deep.equal({ include_count: true }); + }); + }); + + describe('getWorkflows() method', () => { + it('should fetch and process workflows correctly', async () => { + await exportWorkflows.getWorkflows(); + + // Verify workflows were processed + expect(Object.keys(exportWorkflows.workflows).length).to.equal(1); + expect(exportWorkflows.workflows['workflow-1']).to.exist; + expect(exportWorkflows.workflows['workflow-1'].name).to.equal('Workflow 1'); + // Verify invalid key was removed + expect(exportWorkflows.workflows['workflow-1'].invalidKey).to.be.undefined; + }); + + it('should call getWorkflows recursively when more workflows exist', async () => { + let callCount = 0; + mockStackClient.workflow.returns({ + fetchAll: sinon.stub().callsFake(() => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ + items: new Array(100).fill({ uid: 'test', name: 'Test', workflow_stages: [] as any[] }), + count: 150 + }); + } else { + return Promise.resolve({ + items: new Array(50).fill({ uid: 'test2', name: 'Test2', workflow_stages: [] as any[] }), + count: 150 + }); + } + }) + }); + + await exportWorkflows.getWorkflows(); + + // Verify multiple calls were made + expect(callCount).to.be.greaterThan(1); + }); + + it('should handle API errors gracefully without throwing', async () => { + mockStackClient.workflow.returns({ + fetchAll: sinon.stub().rejects(new Error('API Error')) + }); + + // Should complete without throwing + await exportWorkflows.getWorkflows(); + }); + + it('should handle no items response', async () => { + mockStackClient.workflow.returns({ + fetchAll: sinon.stub().resolves({ + items: [], + count: 0 + }) + }); + + const initialCount = Object.keys(exportWorkflows.workflows).length; + await exportWorkflows.getWorkflows(); + + // Verify no new workflows were added + expect(Object.keys(exportWorkflows.workflows).length).to.equal(initialCount); + }); + + it('should update query params with skip value', async () => { + mockStackClient.workflow.returns({ + fetchAll: sinon.stub().resolves({ + items: [{ uid: 'wf-1', name: 'Test' }], + count: 1 + }) + }); + + await exportWorkflows.getWorkflows(50); + + // Verify skip was set in query + expect((exportWorkflows as any).qs.skip).to.equal(50); + }); + }); + + describe('sanitizeAttribs() method', () => { + it('should sanitize workflow attributes and remove invalid keys', async () => { + const workflows = [ + { + uid: 'wf-1', + name: 'Workflow 1', + invalidKey: 'remove', + workflow_stages: [] as any[] + } + ]; + + await exportWorkflows.sanitizeAttribs(workflows); + + // Verify invalid key was removed + expect(exportWorkflows.workflows['wf-1'].invalidKey).to.be.undefined; + expect(exportWorkflows.workflows['wf-1'].name).to.equal('Workflow 1'); + }); + + it('should fetch roles for workflow stages', async () => { + const workflows = [ + { + uid: 'wf-1', + name: 'Workflow 1', + workflow_stages: [ + { + name: 'Draft', + SYS_ACL: { + roles: { + uids: [1, 2] + } + } + } + ] + } + ]; + + await exportWorkflows.sanitizeAttribs(workflows); + + // Verify role fetch was called + expect(mockStackClient.role.called).to.be.true; + }); + + it('should handle workflows without stages', async () => { + const workflows = [ + { + uid: 'wf-1', + name: 'Workflow 1', + workflow_stages: [] as any[] + } + ]; + + await exportWorkflows.sanitizeAttribs(workflows); + + // Verify workflow was still processed + expect(exportWorkflows.workflows['wf-1']).to.exist; + }); + + it('should handle empty workflows array', async () => { + const workflows: any[] = []; + + await exportWorkflows.sanitizeAttribs(workflows); + + expect(Object.keys(exportWorkflows.workflows).length).to.equal(0); + }); + }); + + describe('getRoles() method', () => { + it('should fetch role data correctly', async () => { + const roleData = await exportWorkflows.getRoles(123); + + expect(roleData).to.exist; + expect(mockStackClient.role.called).to.be.true; + }); + + it('should handle API errors gracefully', async () => { + mockStackClient.role.returns({ + fetch: sinon.stub().rejects(new Error('API Error')) + }); + + // Should complete without throwing + await exportWorkflows.getRoles(123); + }); + }); + + describe('start() method', () => { + it('should complete full export flow and write files', async () => { + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + const makeDirectoryStub = FsUtility.prototype.makeDirectory as sinon.SinonStub; + + await exportWorkflows.start(); + + // Verify workflows were processed + expect(Object.keys(exportWorkflows.workflows).length).to.be.greaterThan(0); + // Verify file operations were called + expect(writeFileStub.called).to.be.true; + expect(makeDirectoryStub.called).to.be.true; + }); + + it('should handle empty workflows and log NOT_FOUND', async () => { + mockStackClient.workflow.returns({ + fetchAll: sinon.stub().resolves({ + items: [], + count: 0 + }) + }); + + const writeFileStub = FsUtility.prototype.writeFile as sinon.SinonStub; + + exportWorkflows.workflows = {}; + await exportWorkflows.start(); + + // Verify writeFile was NOT called when workflows are empty + expect(writeFileStub.called).to.be.false; + }); + + it('should handle errors during export without throwing', async () => { + mockStackClient.workflow.returns({ + fetchAll: sinon.stub().rejects(new Error('Export failed')) + }); + + // Should complete without throwing + await exportWorkflows.start(); + }); + }); +}); +