Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,7 @@ jobs:
- name: Run tests for Contentstack Bulk Operations
working-directory: ./packages/contentstack-bulk-operations
run: npm test

- name: Run tests for Contentstack Variants
working-directory: ./packages/contentstack-variants
run: npm run test
22 changes: 12 additions & 10 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
fileignoreconfig:
- filename: pnpm-lock.yaml
checksum: 2b0f2461ea1bb240a9210b9cf99dc403a756199712b7270f9792a590480451bd
- filename: packages/contentstack-import/test/unit/import/modules/base-class.test.ts
checksum: fe372852d5f2f3f57ef62c603406c30ccecdb444c17133ac0b21dda399b962c0
- filename: packages/contentstack-bulk-operations/test/unit/commands/bulk-am-assets.test.ts
checksum: f8d21db7db0ca2eebe7cc40af0a59f02e74e1689efb6d50a1072dc5ca3e03e9b
- filename: packages/contentstack-export/src/export/modules/taxonomies.ts
checksum: b6d077118280bc88385405f504f921468a9fd490ac37a4a21f741be729fd1ca3
- filename: packages/contentstack-export/test/unit/export/modules/taxonomies.test.ts
checksum: cab2ad4d897d23f04f988c1f018a9583ab7f0ee1815994d7bc9fce23dea70073
- filename: pnpm-lock.yaml
checksum: acb2fc21dd3481f162aedaccecbe45d565b3297957345044a12aa50d2850ac4e
- filename: packages/contentstack-variants/test/unit/import/experiences.test.ts
checksum: 6142418bafea6454a72b313d933deb494ce4ea1d8cead7ef918c10e283c2c603
- filename: packages/contentstack-variants/test/unit/export/variant-entries.test.ts
checksum: e150faeefa7b3586b70a6f454c1f68efe05526f582977dba28243b1e47606a42
- filename: packages/contentstack-variants/test/unit/export/experiences.test.ts
checksum: eb9c989dd14373a90e8866ba3350245d3c06e8ba43cfebb24f40c1106b2e6b95
- filename: packages/contentstack-variants/test/unit/utils/personalization-api-adapter.test.ts
checksum: d729c9586d3a19e321d79e490790b9d0aa345f81917376199d962de68317fae1
- filename: packages/contentstack-variants/test/unit/import/variant-entries.test.ts
checksum: bdcd7df1ee9a835ea0fecde7731184097cc397100e58df22b5e6381f6a67e62a
version: '1.0'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
8 changes: 8 additions & 0 deletions packages/contentstack-variants/test/helpers/init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const path = require('path');

process.env.TS_NODE_PROJECT = path.resolve('tsconfig.json');
process.env.TS_NODE_TRANSPILE_ONLY = 'true';
process.env.NODE_ENV = 'test';

global.oclif = global.oclif || {};
global.oclif.columns = 80;
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { expect } from 'chai';
import { ExportExperiences, ExportConfig } from '../../../src';

const makeExportConfig = (branchName?: string): ExportConfig =>
({
modules: {
personalize: {
baseURL: { na: 'https://personalize.na-api.contentstack.com' },
dirName: 'personalize',
},
},
region: { name: 'na', cma: 'https://api.contentstack.io' },
project_id: 'TEST-PROJECT-001',
apiKey: 'TEST-STACK-API-KEY',
exportDir: '/tmp/test-export',
context: {},
...(branchName ? { branchName } : {}),
} as unknown as ExportConfig);

describe('ExportExperiences — branch header', () => {
describe('constructor (cmaConfig headers)', () => {
it('includes branch header in cmaConfig.headers when branchName is set', () => {
const instance = new ExportExperiences(makeExportConfig('feature-branch'));
expect((instance as any).adapterConfig.cmaConfig.headers.branch).to.equal('feature-branch');
});

it('does NOT include branch header in cmaConfig.headers when branchName is absent', () => {
const instance = new ExportExperiences(makeExportConfig());
expect((instance as any).adapterConfig.cmaConfig.headers.branch).to.be.undefined;
});

it('always includes api_key in cmaConfig.headers regardless of branchName', () => {
const instance = new ExportExperiences(makeExportConfig('staging'));
expect((instance as any).adapterConfig.cmaConfig.headers.api_key).to.equal('TEST-STACK-API-KEY');
});

it('sets correct cmaConfig baseURL from region', () => {
const instance = new ExportExperiences(makeExportConfig('dev'));
expect((instance as any).adapterConfig.cmaConfig.baseURL).to.equal('https://api.contentstack.io/v3');
});

it('branch header value matches branchName exactly', () => {
const instance = new ExportExperiences(makeExportConfig('eu-branch-2025'));
expect((instance as any).adapterConfig.cmaConfig.headers.branch).to.equal('eu-branch-2025');
});

it('cmaConfig.headers has only api_key when branchName is not set', () => {
const instance = new ExportExperiences(makeExportConfig());
const headers = (instance as any).adapterConfig.cmaConfig.headers;
expect(Object.keys(headers)).to.deep.equal(['api_key']);
});

it('cmaConfig.headers has api_key and branch when branchName is set', () => {
const instance = new ExportExperiences(makeExportConfig('main'));
const headers = (instance as any).adapterConfig.cmaConfig.headers;
expect(headers).to.deep.equal({ api_key: 'TEST-STACK-API-KEY', branch: 'main' });
});
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from '@oclif/test';
import sinon from 'sinon';
import { expect } from 'chai';
import { FsUtility } from '@contentstack/cli-utilities';
import { fancy } from '@contentstack/cli-dev-dependencies';

import exportConf from '../mock/export-config.json';
import { Export, ExportConfig, VariantHttpClient, VariantsOption } from '../../../src';
Expand All @@ -14,80 +14,96 @@ describe('Variant Entries Export', () => {
entries: [{ uid: 'E-UID-1', title: 'Entry 1' }],
};

const test = fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.stub(FsUtility.prototype, 'completeFile', () => {})
.stub(FsUtility.prototype, 'writeIntoFile', () => {})
.stub(FsUtility.prototype, 'createFolderIfNotExist', () => {});

beforeEach(() => {
config = exportConf as unknown as ExportConfig;
});

afterEach(() => sinon.restore());

describe('path construction', () => {
test.it('should use exportDir as base path (no branch segment in path)', () => {
const configWithExportDir = {
...config,
exportDir: '/base/export',
branchName: 'dev',
} as ExportConfig;
const instance = new Export.VariantEntries(configWithExportDir);
it('should use exportDir as base path (no branch segment in path)', () => {
const instance = new Export.VariantEntries({
...config, exportDir: '/base/export', branchName: 'dev',
} as ExportConfig);
expect(instance.entriesDirPath).to.not.include('dev');
expect(instance.entriesDirPath).to.include('entries');
});
});

describe('branch header', () => {
const getHeaders = (instance: any) => instance.variantInstance.adapterConfig.headers;

it('sets branch header in adapter headers when branchName is configured', () => {
const instance = new Export.VariantEntries({
...config, apiKey: 'TEST-KEY', branchName: 'feature-branch', org_uid: 'TEST-ORG', project_id: 'TEST-PROJECT',
} as ExportConfig);
expect(getHeaders(instance).branch).to.equal('feature-branch');
});

it('branch header is undefined when branchName is not set', () => {
const instance = new Export.VariantEntries({
...config, apiKey: 'TEST-KEY', org_uid: 'TEST-ORG', project_id: 'TEST-PROJECT',
} as ExportConfig);
expect(getHeaders(instance).branch).to.be.undefined;
});

it('always sets api_key in adapter headers', () => {
const instance = new Export.VariantEntries({
...config, apiKey: 'TEST-STACK-API-KEY', branchName: 'staging', org_uid: 'TEST-ORG', project_id: 'TEST-PROJECT',
} as ExportConfig);
expect(getHeaders(instance).api_key).to.equal('TEST-STACK-API-KEY');
});

it('branch header value matches branchName exactly', () => {
const instance = new Export.VariantEntries({
...config, apiKey: 'TEST-KEY', branchName: 'eu-release-2025', org_uid: 'TEST-ORG', project_id: 'TEST-PROJECT',
} as ExportConfig);
expect(getHeaders(instance).branch).to.equal('eu-release-2025');
});
});

describe('exportVariantEntry method', () => {
test
.stub(VariantHttpClient.prototype, 'variantEntries', async () => {})
.spy(VariantHttpClient.prototype, 'variantEntries')
.spy(FsUtility.prototype, 'completeFile')
.spy(FsUtility.prototype, 'createFolderIfNotExist')
.it('should call export variant entry method (API call)', async ({ spy }) => {
let entryVariantInstace = new Export.VariantEntries(config);
await entryVariantInstace.exportVariantEntry(exportEntryData);

expect(spy.variantEntries.callCount).to.be.equals(1);
expect(spy.completeFile.callCount).to.be.equals(1);
expect(spy.createFolderIfNotExist.callCount).to.be.equals(1);
expect(spy.completeFile.alwaysCalledWith(true)).to.be.true;
beforeEach(() => {
sinon.stub(VariantHttpClient.prototype, 'init').resolves();
});

it('should call variantEntries once per entry', async () => {
const variantEntriesStub = sinon.stub(VariantHttpClient.prototype, 'variantEntries' as any).resolves();
sinon.stub(FsUtility.prototype, 'completeFile' as any);
sinon.stub(FsUtility.prototype, 'writeIntoFile' as any);

const instance = new Export.VariantEntries(config);
await instance.exportVariantEntry(exportEntryData);

expect(variantEntriesStub.callCount).to.equal(1);
expect(variantEntriesStub.firstCall.args[0]).to.include({ entry_uid: 'E-UID-1', locale: 'en-us' });
});

it('should write data in files when callback is invoked with entries', async () => {
sinon.stub(VariantHttpClient.prototype, 'variantEntries' as any).callsFake(async (opts: VariantsOption) => {
if (opts.callback) opts.callback([{ uid: 'E-UID-1', title: 'Entry 1' }]);
});
const writeIntoFileStub = sinon.stub(FsUtility.prototype, 'writeIntoFile' as any);

test
.stub(VariantHttpClient.prototype, 'variantEntries', async (...args: any) => {
const { callback } = args[0] as VariantsOption;
if (callback) {
callback([{ uid: 'E-UID-1', title: 'Entry 1' }]);
}
})
.spy(FsUtility.prototype, 'writeIntoFile')
.it('should write data in files (As chunk)', async ({ spy }) => {
let entryVariantInstace = new Export.VariantEntries(config);
await entryVariantInstace.exportVariantEntry(exportEntryData);

expect(spy.writeIntoFile.callCount).to.be.equals(1);
expect(spy.writeIntoFile.alwaysCalledWith([{ uid: 'E-UID-1', title: 'Entry 1' }])).to.be.true;
const instance = new Export.VariantEntries(config);
await instance.exportVariantEntry(exportEntryData);

expect(writeIntoFileStub.callCount).to.equal(1);
expect(writeIntoFileStub.alwaysCalledWith([{ uid: 'E-UID-1', title: 'Entry 1' }])).to.be.true;
});

it('should skip write when callback returns empty array; default chunk size to 1MB', async () => {
const variantEntriesStub = sinon.stub(VariantHttpClient.prototype, 'variantEntries' as any).callsFake(async (opts: VariantsOption) => {
if (opts.callback) opts.callback([]);
});
const writeIntoFileStub = sinon.stub(FsUtility.prototype, 'writeIntoFile' as any);

test
.stub(VariantHttpClient.prototype, 'variantEntries', async (...args: any) => {
const { callback } = args[0] as VariantsOption;
if (callback) {
callback([]); // NOTE API callback with empty response
}
})
.spy(FsUtility.prototype, 'writeIntoFile')
.spy(VariantHttpClient.prototype, 'variantEntries')
.it(
'should skip write data in files (Empty data check validation), should set default file chunk 1MB if chunk size is not passed in config',
async ({ spy }) => {
config.modules.variantEntry.chunkFileSize = null as any;
let entryVariantInstace = new Export.VariantEntries(config, () => {});
await entryVariantInstace.exportVariantEntry(exportEntryData);

expect(spy.writeIntoFile.callCount).to.be.equals(0);
expect(spy.variantEntries.callCount).to.be.equals(1);
},
);
config.modules.variantEntry.chunkFileSize = null as any;
const instance = new Export.VariantEntries(config, () => {});
await instance.exportVariantEntry(exportEntryData);

expect(writeIntoFileStub.callCount).to.equal(0);
expect(variantEntriesStub.callCount).to.equal(1);
});
});
});
Loading
Loading