Skip to content

Commit 77ce3f0

Browse files
author
naman-contentstack
committed
chore: add test cases for publishing rules
1 parent fde3605 commit 77ce3f0

5 files changed

Lines changed: 406 additions & 16 deletions

File tree

.talismanrc

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
11
fileignoreconfig:
2-
- filename: packages/contentstack-import/src/config/index.ts
3-
checksum: 541136c45d6a74010e4999389dd9c06a0e6b7b74bea62087d66c50b17e8e5d11
4-
- filename: packages/contentstack-variants/src/types/export-config.ts
5-
checksum: 5707cf07e6777d31d4730567d861300a94b16b8a97e47fb3d804efb67f214aab
6-
- filename: packages/contentstack-import/src/types/index.ts
7-
checksum: 068079597f11b6460cca4005ca2345d7c2a954dc516a14c51782c631652cd5a3
8-
- filename: packages/contentstack-export/src/config/index.ts
9-
checksum: 2c811d2bd7b6657b567fd120cfaf05306ca751109c070bf7b49c18d06b211764
10-
- filename: packages/contentstack-export/src/types/index.ts
11-
checksum: 173b811f93b12da873d5860275a85438d2f78a1c4e387c77ec5246f1c0231da2
12-
- filename: packages/contentstack-export/src/types/default-config.ts
13-
checksum: 0cc62919207384ec710ea3f8a3445d6e5bd78cc2c5306b9e74aaeec688eb028d
14-
- filename: packages/contentstack-import/src/types/default-config.ts
15-
checksum: 0db51c83ce44e31d51ae881a0f0bfc2cd39cb15bd333fe3b1e9f19292d575d91
16-
- filename: packages/contentstack-import/src/import/modules/publishing-rules.ts
17-
checksum: 429a803bc18e691db93bae3df1714071d0face6441b82cb938a83e8bf94ae14c
2+
- filename: packages/contentstack-import/test/unit/utils/extension-helper.test.ts
3+
checksum: c5aad4f28fbab6610d955490f061e647714b88f7b8f8d9cc4dbc3b4a10b9e59a
4+
- filename: packages/contentstack-import/test/unit/import/modules/locales.test.ts
5+
checksum: 0085926464eeb17cda0b9fb5a16853ad9c9aec9a42b54161107d0c808347498f
6+
- filename: packages/contentstack-import/test/unit/import/modules/publishing-rules.test.ts
7+
checksum: 0fcbff5dab2f9e594fe2a316c3c96e8d86bcd5d72e7c1f9eb35c0e3458f87817
188
version: '1.0'

packages/contentstack-import/test/unit/import/modules/locales.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ describe('ImportLocales', () => {
5151
webhooks: { dirName: 'webhooks', fileName: 'webhooks.json' },
5252
releases: { dirName: 'releases', fileName: 'releases.json', invalidKeys: ['uid'] },
5353
workflows: { dirName: 'workflows', fileName: 'workflows.json', invalidKeys: ['uid'] },
54+
'publishing-rules': {
55+
dirName: 'workflows',
56+
fileName: 'publishing-rules.json',
57+
invalidKeys: ['uid'],
58+
},
5459
assets: {
5560
dirName: 'assets',
5661
assetBatchLimit: 10,
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import { expect } from 'chai';
2+
import sinon from 'sinon';
3+
import { join } from 'node:path';
4+
import ImportPublishingRules from '../../../../src/import/modules/publishing-rules';
5+
import { ImportConfig } from '../../../../src/types';
6+
describe('ImportPublishingRules', () => {
7+
const BACKUP = '/test/backup';
8+
const rulesFile = join(BACKUP, 'workflows', 'publishing-rules.json');
9+
const workflowsExportFile = join(BACKUP, 'workflows', 'workflows.json');
10+
const workflowMapperFile = join(BACKUP, 'mapper', 'workflows', 'uid-mapping.json');
11+
const envMapperFile = join(BACKUP, 'mapper', 'environments', 'uid-mapping.json');
12+
const publishingMapperFile = join(BACKUP, 'mapper', 'publishing-rules', 'uid-mapping.json');
13+
14+
let importPublishingRules: ImportPublishingRules;
15+
let mockStackClient: any;
16+
let mockImportConfig: ImportConfig;
17+
let fsUtilStub: any;
18+
let fileHelperStub: any;
19+
let makeConcurrentCallStub: sinon.SinonStub;
20+
let logStub: { info: sinon.SinonStub; debug: sinon.SinonStub; success: sinon.SinonStub; error: sinon.SinonStub; warn: sinon.SinonStub };
21+
beforeEach(() => {
22+
fsUtilStub = {
23+
readFile: sinon.stub(),
24+
writeFile: sinon.stub(),
25+
makeDirectory: sinon.stub().resolves(),
26+
};
27+
28+
fileHelperStub = {
29+
fileExistsSync: sinon.stub(),
30+
};
31+
32+
sinon.replace(require('../../../../src/utils'), 'fileHelper', fileHelperStub);
33+
sinon.replaceGetter(require('../../../../src/utils'), 'fsUtil', () => fsUtilStub);
34+
35+
const fetchWorkflowStub = sinon.stub().resolves({
36+
workflow_stages: [{ uid: 'stage-new', name: 'Review' }],
37+
});
38+
mockStackClient = {
39+
workflow: sinon.stub().returns({
40+
fetch: fetchWorkflowStub,
41+
}),
42+
};
43+
44+
mockImportConfig = {
45+
apiKey: 'test',
46+
backupDir: BACKUP,
47+
data: '/test/content',
48+
contentVersion: 1,
49+
region: 'us',
50+
fetchConcurrency: 2,
51+
context: {
52+
command: 'cm:stacks:import',
53+
module: 'publishing-rules',
54+
userId: 'user-123',
55+
email: 'test@example.com',
56+
sessionId: 'session-123',
57+
apiKey: 'test',
58+
orgId: 'org-123',
59+
authenticationMethod: 'Basic Auth',
60+
},
61+
modules: {
62+
workflows: {
63+
dirName: 'workflows',
64+
fileName: 'workflows.json',
65+
invalidKeys: ['uid'],
66+
},
67+
'publishing-rules': {
68+
dirName: 'workflows',
69+
fileName: 'publishing-rules.json',
70+
invalidKeys: ['uid'],
71+
},
72+
},
73+
} as any;
74+
75+
importPublishingRules = new ImportPublishingRules({
76+
importConfig: mockImportConfig as any,
77+
stackAPIClient: mockStackClient,
78+
moduleName: 'publishing-rules',
79+
});
80+
81+
makeConcurrentCallStub = sinon.stub(importPublishingRules as any, 'makeConcurrentCall').resolves();
82+
83+
const cliUtilities = require('@contentstack/cli-utilities');
84+
logStub = {
85+
info: sinon.stub(),
86+
debug: sinon.stub(),
87+
success: sinon.stub(),
88+
error: sinon.stub(),
89+
warn: sinon.stub(),
90+
};
91+
sinon.stub(cliUtilities, 'log').value(logStub);
92+
});
93+
94+
afterEach(() => {
95+
sinon.restore();
96+
});
97+
98+
describe('Constructor', () => {
99+
it('sets context.module to publishing-rules and derives exact paths from backupDir and config', () => {
100+
expect(mockImportConfig.context.module).to.equal('publishing-rules');
101+
expect(importPublishingRules['mapperDirPath']).to.equal(join(BACKUP, 'mapper', 'publishing-rules'));
102+
expect(importPublishingRules['publishingRulesFolderPath']).to.equal(join(BACKUP, 'workflows'));
103+
expect(importPublishingRules['publishingRulesUidMapperPath']).to.equal(publishingMapperFile);
104+
expect(importPublishingRules['createdPublishingRulesPath']).to.equal(
105+
join(BACKUP, 'mapper', 'publishing-rules', 'success.json'),
106+
);
107+
expect(importPublishingRules['failedPublishingRulesPath']).to.equal(
108+
join(BACKUP, 'mapper', 'publishing-rules', 'fails.json'),
109+
);
110+
});
111+
112+
it('initializes empty rules, mappers, and result arrays', () => {
113+
expect(importPublishingRules['publishingRules']).to.deep.equal({});
114+
expect(importPublishingRules['publishingRulesUidMapper']).to.deep.equal({});
115+
expect(importPublishingRules['createdPublishingRules']).to.deep.equal([]);
116+
expect(importPublishingRules['failedPublishingRules']).to.deep.equal([]);
117+
expect(importPublishingRules['envUidMapper']).to.deep.equal({});
118+
expect(importPublishingRules['workflowUidMapper']).to.deep.equal({});
119+
expect(importPublishingRules['stageUidMapper']).to.deep.equal({});
120+
});
121+
});
122+
123+
describe('start()', () => {
124+
it('returns undefined and logs missing file path when rules file does not exist', async () => {
125+
fileHelperStub.fileExistsSync.withArgs(rulesFile).returns(false);
126+
127+
const result = await importPublishingRules.start();
128+
129+
expect(result).to.equal(undefined);
130+
expect(makeConcurrentCallStub.called).to.be.false;
131+
expect(logStub.info.firstCall.args[0]).to.include(rulesFile);
132+
});
133+
134+
it('returns undefined when rules file exists but payload is empty; arrays stay empty', async () => {
135+
fileHelperStub.fileExistsSync.withArgs(rulesFile).returns(true);
136+
fsUtilStub.readFile.withArgs(rulesFile, true).returns({});
137+
138+
const result = await importPublishingRules.start();
139+
140+
expect(result).to.equal(undefined);
141+
expect(makeConcurrentCallStub.called).to.be.false;
142+
expect(importPublishingRules['createdPublishingRules']).to.deep.equal([]);
143+
expect(importPublishingRules['failedPublishingRules']).to.deep.equal([]);
144+
});
145+
146+
it('passes one apiContent item per rule and binds serializeData to serializePublishingRules', async () => {
147+
const rules = {
148+
r1: { uid: 'r1', name: 'Rule 1' },
149+
r2: { uid: 'r2', name: 'Rule 2' },
150+
};
151+
fileHelperStub.fileExistsSync.callsFake((p: string) => {
152+
if (p === rulesFile) return true;
153+
if (p === workflowsExportFile || p === workflowMapperFile || p === envMapperFile || p === publishingMapperFile) {
154+
return false;
155+
}
156+
return false;
157+
});
158+
fsUtilStub.readFile.callsFake((p: string) => {
159+
if (p === rulesFile) return rules;
160+
return {};
161+
});
162+
163+
await importPublishingRules.start();
164+
165+
expect(makeConcurrentCallStub.calledOnce).to.be.true;
166+
const callArgs = makeConcurrentCallStub.firstCall.args[0];
167+
expect(callArgs.apiContent).to.have.length(2);
168+
expect(callArgs.processName).to.equal('import publishing rules');
169+
expect(callArgs.apiParams.entity).to.equal('create-publishing-rule');
170+
const serialized = callArgs.apiParams.serializeData({
171+
apiData: { uid: 'r1', name: 'Rule 1' },
172+
entity: 'create-publishing-rule',
173+
});
174+
expect(serialized.apiData).to.deep.include({ name: 'Rule 1', uid: 'r1' });
175+
expect(serialized.entity).to.equal('create-publishing-rule');
176+
});
177+
178+
it('builds stageUidMapper from exported workflows and fetched target workflow stages by name', async () => {
179+
fileHelperStub.fileExistsSync.callsFake((p: string) => {
180+
if (p === rulesFile) return true;
181+
if (p === workflowsExportFile) return true;
182+
if (p === workflowMapperFile) return true;
183+
if (p === envMapperFile || p === publishingMapperFile) return false;
184+
return false;
185+
});
186+
fsUtilStub.readFile.callsFake((p: string) => {
187+
if (p === rulesFile) return { r1: { uid: 'r1', name: 'R' } };
188+
if (p === workflowsExportFile) {
189+
return {
190+
expWf: { workflow_stages: [{ uid: 'stage-old', name: 'Review' }] },
191+
};
192+
}
193+
if (p === workflowMapperFile) return { oldWf: 'newWf' };
194+
return {};
195+
});
196+
197+
await importPublishingRules.start();
198+
199+
expect(importPublishingRules['stageUidMapper']).to.deep.equal({ 'stage-old': 'stage-new' });
200+
expect(mockStackClient.workflow.calledWith('newWf')).to.be.true;
201+
});
202+
203+
it('returns { noSuccessMsg: true } when a rule fails to import (non-duplicate error)', async () => {
204+
fileHelperStub.fileExistsSync.callsFake((p: string) => p === rulesFile);
205+
fsUtilStub.readFile.callsFake((p: string) => (p === rulesFile ? { r1: { uid: 'r1', name: 'R' } } : {}));
206+
207+
makeConcurrentCallStub.callsFake(async (env: any) => {
208+
const { apiParams, apiContent } = env;
209+
for (const element of apiContent) {
210+
apiParams.apiData = element;
211+
let opts = { ...apiParams, apiData: { ...element } };
212+
opts = apiParams.serializeData(opts);
213+
if (opts.entity) {
214+
await apiParams.reject({ error: new Error('network'), apiData: opts.apiData });
215+
}
216+
}
217+
});
218+
219+
const result = await importPublishingRules.start();
220+
221+
expect(result).to.deep.equal({ noSuccessMsg: true });
222+
expect(importPublishingRules['failedPublishingRules']).to.have.length(1);
223+
expect(importPublishingRules['failedPublishingRules'][0].uid).to.equal('r1');
224+
expect(String(logStub.error.firstCall?.args[0] ?? '')).to.include('could not be imported');
225+
});
226+
227+
it('returns undefined when import succeeds with no failures', async () => {
228+
fileHelperStub.fileExistsSync.callsFake((p: string) => p === rulesFile);
229+
fsUtilStub.readFile.callsFake((p: string) => (p === rulesFile ? { r1: { uid: 'r1', name: 'R' } } : {}));
230+
231+
makeConcurrentCallStub.callsFake(async (env: any) => {
232+
const { apiParams, apiContent } = env;
233+
for (const element of apiContent) {
234+
apiParams.apiData = element;
235+
let opts = { ...apiParams, apiData: { ...element } };
236+
opts = apiParams.serializeData(opts);
237+
if (opts.entity) {
238+
await apiParams.resolve({ response: { uid: 'new-r1' }, apiData: opts.apiData });
239+
}
240+
}
241+
});
242+
243+
const result = await importPublishingRules.start();
244+
245+
expect(result).to.equal(undefined);
246+
expect(importPublishingRules['failedPublishingRules']).to.deep.equal([]);
247+
expect(importPublishingRules['publishingRulesUidMapper']).to.deep.equal({ r1: 'new-r1' });
248+
expect(logStub.success.calledWith('Publishing rules have been imported successfully!', mockImportConfig.context)).to.be
249+
.true;
250+
});
251+
});
252+
253+
describe('serializePublishingRules', () => {
254+
it('clears entity when rule uid already in mapper; leaves apiData.uid unchanged', () => {
255+
importPublishingRules['publishingRulesUidMapper'] = { 'rule-1': 'mapped-1' };
256+
257+
const apiOptions: any = {
258+
apiData: { uid: 'rule-1', name: 'N' },
259+
entity: 'create-publishing-rule',
260+
};
261+
262+
const out = importPublishingRules.serializePublishingRules(apiOptions);
263+
264+
expect(out.entity).to.equal(undefined);
265+
expect(out.apiData).to.deep.equal({ uid: 'rule-1', name: 'N' });
266+
expect(String(logStub.info.firstCall?.args[0] ?? '')).to.match(/already exists\. Skipping/);
267+
});
268+
269+
it('remaps workflow, environment, workflow_stage and strips approvers; apiData carries uid for completion handler', () => {
270+
const pr = importPublishingRules as any;
271+
pr.workflowUidMapper = { wfOld: 'wfNew' };
272+
pr.envUidMapper = { envOld: 'envNew' };
273+
Object.keys(pr.stageUidMapper).forEach((k) => delete pr.stageUidMapper[k]);
274+
pr.stageUidMapper.stOld = 'stNew';
275+
276+
const apiOptions: any = {
277+
apiData: {
278+
uid: 'pr-1',
279+
name: 'PR',
280+
workflow: 'wfOld',
281+
environment: 'envOld',
282+
workflow_stage: 'stOld',
283+
approvers: { roles: ['r1'], users: ['u1'] },
284+
},
285+
entity: 'create-publishing-rule',
286+
};
287+
288+
const out = importPublishingRules.serializePublishingRules(apiOptions);
289+
290+
expect(out.entity).to.equal('create-publishing-rule');
291+
expect(out.apiData).to.deep.equal({
292+
uid: 'pr-1',
293+
name: 'PR',
294+
workflow: 'wfNew',
295+
environment: 'envNew',
296+
workflow_stage: 'stNew',
297+
approvers: { roles: [], users: [] },
298+
});
299+
const infoArgs = logStub.info.getCalls().map((c) => c.args[0]);
300+
expect(infoArgs.some((msg) => String(msg).includes('Skipping import of publish rule approver'))).to.be.true;
301+
});
302+
});
303+
304+
describe('importPublishingRules callbacks', () => {
305+
beforeEach(() => {
306+
importPublishingRules['publishingRules'] = { r1: { uid: 'r1', name: 'R' } };
307+
});
308+
309+
it('onSuccess updates mapper and persists uid-mapping.json with expected payload', async () => {
310+
await (importPublishingRules as any).importPublishingRules();
311+
312+
const onSuccess = makeConcurrentCallStub.firstCall.args[0].apiParams.resolve;
313+
await onSuccess({ response: { uid: 'created-uid', name: 'R' }, apiData: { uid: 'r1', name: 'R' } });
314+
315+
expect(importPublishingRules['createdPublishingRules']).to.deep.equal([{ uid: 'created-uid', name: 'R' }]);
316+
expect(importPublishingRules['publishingRulesUidMapper']).to.deep.equal({ r1: 'created-uid' });
317+
expect(fsUtilStub.writeFile.calledWith(publishingMapperFile, { r1: 'created-uid' })).to.be.true;
318+
});
319+
320+
it('onReject for duplicate error does not append to failedPublishingRules', async () => {
321+
await (importPublishingRules as any).importPublishingRules();
322+
323+
const onReject = makeConcurrentCallStub.firstCall.args[0].apiParams.reject;
324+
await onReject({
325+
error: { errors: { name: 'taken' } },
326+
apiData: { uid: 'r1', name: 'R' },
327+
});
328+
329+
expect(importPublishingRules['failedPublishingRules']).to.deep.equal([]);
330+
expect(logStub.info.calledWith(`Publishing rule 'r1' already exists`, mockImportConfig.context)).to.be.true;
331+
});
332+
});
333+
});

packages/contentstack-import/test/unit/utils/extension-helper.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ describe('Extension Helper', () => {
5454
webhooks: { dirName: 'webhooks', fileName: 'webhooks.json' },
5555
releases: { dirName: 'releases', fileName: 'releases.json', invalidKeys: ['uid'] },
5656
workflows: { dirName: 'workflows', fileName: 'workflows.json', invalidKeys: ['uid'] },
57+
'publishing-rules': {
58+
dirName: 'workflows',
59+
fileName: 'publishing-rules.json',
60+
invalidKeys: ['uid'],
61+
},
5762
assets: {
5863
dirName: 'assets',
5964
assetBatchLimit: 10,

0 commit comments

Comments
 (0)