-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathassets.test.ts
More file actions
370 lines (341 loc) · 15.5 KB
/
assets.test.ts
File metadata and controls
370 lines (341 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
import fs from 'fs';
import { resolve } from 'path';
import { expect } from 'chai';
import fancy from 'fancy-test';
import Sinon from 'sinon';
import { cliux } from '@contentstack/cli-utilities';
import config from '../../../src/config';
import { $t, auditMsg } from '../../../src/messages';
import Assets from '../../../src/modules/assets';
import { ModuleConstructorParam, CtConstructorParam } from '../../../src/types';
import { mockLogger } from '../mock-logger';
const mockContentsPath = resolve(__dirname, '..', 'mock', 'contents');
describe('Assets module', () => {
let constructorParam: ModuleConstructorParam & CtConstructorParam;
beforeEach(() => {
constructorParam = {
moduleName: 'assets',
ctSchema: [] as any,
gfSchema: {} as any,
config: Object.assign(config, {
basePath: mockContentsPath,
flags: {} as any,
}),
};
Sinon.stub(require('@contentstack/cli-utilities'), 'log').value(mockLogger);
});
afterEach(() => {
Sinon.restore();
});
describe('constructor and validateModules', () => {
it('should set moduleName, folderPath and fileName when module is in config', () => {
const instance = new Assets(constructorParam);
expect(instance.moduleName).to.eql('assets');
expect(instance.fileName).to.eql('assets.json');
expect(instance.folderPath).to.include('assets');
});
it('should default moduleName to assets when module not in config', () => {
const instance = new Assets({
...constructorParam,
moduleName: 'invalid' as any,
});
expect(instance.moduleName).to.eql('assets');
});
});
describe('run()', () => {
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should return empty result and print NOT_VALID_PATH when path does not exist', async () => {
const instance = new Assets({
...constructorParam,
config: { ...constructorParam.config, basePath: resolve(__dirname, '..', 'mock', 'nonexistent') },
});
const result = await instance.run(false);
expect(result).to.eql({});
});
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should return [] when returnFixSchema true and path does not exist', async () => {
const instance = new Assets({
...constructorParam,
config: { ...constructorParam.config, basePath: resolve(__dirname, '..', 'mock', 'nonexistent') },
});
const result = await instance.run(true);
expect(result).to.eql([]);
});
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.stub(Assets.prototype, 'prerequisiteData', Sinon.stub().resolves())
.stub(Assets.prototype, 'lookForReference', Sinon.stub().resolves())
.it('should return missingEnvLocales and call completeProgress when path exists', async () => {
const instance = new Assets(constructorParam);
(instance as any).missingEnvLocales = { uid1: [{ publish_locale: 'en', publish_environment: 'e1' }] };
const completeSpy = Sinon.spy(Assets.prototype as any, 'completeProgress');
const result = await instance.run(false);
expect(result).to.have.property('uid1');
expect(completeSpy.calledWith(true)).to.be.true;
});
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.stub(Assets.prototype, 'prerequisiteData', Sinon.stub().resolves())
.stub(Assets.prototype, 'lookForReference', Sinon.stub().resolves())
.it('should create progress and updateStatus when totalCount provided', async () => {
const progressStub = { updateStatus: Sinon.stub() };
const createProgressStub = Sinon.stub(Assets.prototype as any, 'createSimpleProgress').returns(progressStub as any);
const instance = new Assets(constructorParam);
await instance.run(false, 5);
expect(createProgressStub.calledWith('assets', 5)).to.be.true;
expect(progressStub.updateStatus.calledWith('Validating asset references...')).to.be.true;
});
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.stub(Assets.prototype, 'prerequisiteData', Sinon.stub().resolves())
.stub(Assets.prototype, 'lookForReference', Sinon.stub().resolves())
.it('should return schema (empty array) when returnFixSchema is true', async () => {
const instance = new Assets(constructorParam);
const result = await instance.run(true);
expect(result).to.eql([]);
});
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.stub(Assets.prototype, 'prerequisiteData', Sinon.stub().resolves())
.stub(Assets.prototype, 'lookForReference', Sinon.stub().callsFake(function (this: Assets) {
(this as any).missingEnvLocales['someUid'] = [];
}))
.it('should cleanup empty missingEnvLocales entries', async () => {
const instance = new Assets(constructorParam);
const result = await instance.run(false);
expect(result).to.not.have.property('someUid');
});
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.stub(Assets.prototype, 'prerequisiteData', Sinon.stub().resolves())
.stub(Assets.prototype, 'lookForReference', Sinon.stub().rejects(new Error('lookForReference failed')))
.it('should call completeProgress(false) and rethrow on error', async () => {
const instance = new Assets(constructorParam);
const completeSpy = Sinon.spy(instance as any, 'completeProgress');
try {
await instance.run(false);
} catch (e: any) {
expect(completeSpy.calledWith(false, 'lookForReference failed')).to.be.true;
expect(e.message).to.eql('lookForReference failed');
}
});
});
describe('prerequisiteData()', () => {
it('should load locales and environments when all files present', async () => {
const instance = new Assets(constructorParam);
await instance.prerequisiteData();
expect(instance.locales).to.be.an('array');
expect(instance.locales).to.include('en-us');
expect(instance.environments).to.be.an('array');
expect(instance.environments).to.include('env1');
expect(instance.environments).to.include('env2');
});
it('should map locales to .code', async () => {
const instance = new Assets(constructorParam);
await instance.prerequisiteData();
expect(instance.locales.every((l: string) => typeof l === 'string')).to.be.true;
expect(instance.locales).to.include('en-us');
});
fancy
.stdout({ print: false })
.stub(fs, 'existsSync', Sinon.stub().callThrough().withArgs(Sinon.match(/master-locale\.json/)).returns(false))
.it('should have locales from locales.json only when no master-locale', async () => {
const instance = new Assets(constructorParam);
await instance.prerequisiteData();
Sinon.restore();
expect(instance.locales).to.be.an('array');
});
fancy
.stdout({ print: false })
.stub(fs, 'existsSync', Sinon.stub().callThrough().withArgs(Sinon.match(/environments\.json/)).returns(false))
.it('should have empty environments when environments file missing', async () => {
const instance = new Assets(constructorParam);
await instance.prerequisiteData();
Sinon.restore();
expect(instance.environments).to.eql([]);
});
});
describe('writeFixContent()', () => {
it('should not call writeFileSync when fix is false', async () => {
const instance = new Assets({ ...constructorParam, fix: false });
const writeStub = Sinon.stub(fs, 'writeFileSync');
await instance.writeFixContent('/some/path', { a: {} } as any);
expect(writeStub.called).to.be.false;
writeStub.restore();
});
fancy
.stdout({ print: false })
.stub(cliux, 'confirm', Sinon.stub().resolves(true))
.it('should write file when fix true and user confirms', async () => {
const instance = new Assets({ ...constructorParam, fix: true });
const writeStub = Sinon.stub(fs, 'writeFileSync');
await instance.writeFixContent('/tmp/out.json', { uid1: { title: 'A' } } as any);
expect(writeStub.calledOnce).to.be.true;
expect(writeStub.firstCall.args[0]).to.eql('/tmp/out.json');
expect(JSON.parse(String(writeStub.firstCall.args[1]))).to.deep.include({ uid1: { title: 'A' } });
writeStub.restore();
Sinon.restore();
});
fancy
.stdout({ print: false })
.stub(cliux, 'confirm', Sinon.stub().resolves(false))
.it('should not write when fix true and user declines', async () => {
const instance = new Assets({
...constructorParam,
fix: true,
config: { ...constructorParam.config, flags: { yes: false } as any },
});
const writeStub = Sinon.stub(fs, 'writeFileSync');
await instance.writeFixContent('/tmp/out.json', {});
expect(writeStub.called).to.be.false;
writeStub.restore();
Sinon.restore();
});
fancy
.stdout({ print: false })
.it('should write without confirm when flags.yes is true', async () => {
const instance = new Assets({
...constructorParam,
fix: true,
config: { ...constructorParam.config, flags: { yes: true } as any },
});
const writeStub = Sinon.stub(fs, 'writeFileSync');
const confirmSpy = Sinon.spy(cliux, 'confirm');
await instance.writeFixContent('/tmp/out.json', { x: {} } as any);
expect(writeStub.calledOnce).to.be.true;
expect(confirmSpy.called).to.be.false;
writeStub.restore();
Sinon.restore();
});
fancy
.stdout({ print: false })
.it('should skip confirm when flags.copy-dir is true', async () => {
const instance = new Assets({
...constructorParam,
fix: true,
config: { ...constructorParam.config, flags: { 'copy-dir': true } as any },
});
const writeStub = Sinon.stub(fs, 'writeFileSync');
const confirmSpy = Sinon.spy(cliux, 'confirm');
await instance.writeFixContent('/tmp/out.json', { x: {} } as any);
expect(writeStub.calledOnce).to.be.true;
expect(confirmSpy.called).to.be.false;
writeStub.restore();
Sinon.restore();
});
fancy
.stdout({ print: false })
.it('should skip confirm when external-config.skipConfirm is true', async () => {
const instance = new Assets({
...constructorParam,
fix: true,
config: {
...constructorParam.config,
flags: { 'external-config': { skipConfirm: true } } as any,
},
});
const writeStub = Sinon.stub(fs, 'writeFileSync');
const confirmSpy = Sinon.spy(cliux, 'confirm');
await instance.writeFixContent('/tmp/out.json', { x: {} } as any);
expect(writeStub.calledOnce).to.be.true;
expect(confirmSpy.called).to.be.false;
writeStub.restore();
Sinon.restore();
});
});
describe('lookForReference()', () => {
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should process assets and populate missingEnvLocales for invalid publish_details', async () => {
const instance = new Assets(constructorParam);
await instance.prerequisiteData();
await instance.lookForReference();
const missing = (instance as any).missingEnvLocales;
expect(missing).to.have.property('asset_uid_invalid');
expect(missing.asset_uid_invalid).to.have.lengthOf(1);
expect(missing.asset_uid_two_invalid).to.have.lengthOf(2);
});
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should call progressManager.tick when progress manager exists', async () => {
const instance = new Assets(constructorParam);
await instance.prerequisiteData();
const tickStub = Sinon.stub();
(instance as any).progressManager = { tick: tickStub };
await instance.lookForReference();
expect(tickStub.callCount).to.be.greaterThan(0);
});
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should call writeFixContent when fix is true and asset has invalid pd', async () => {
const instance = new Assets({ ...constructorParam, fix: true });
await instance.prerequisiteData();
const writeFixSpy = Sinon.stub(Assets.prototype, 'writeFixContent').resolves();
await instance.lookForReference();
expect(writeFixSpy.called).to.be.true;
});
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should print ASSET_NOT_EXIST when publish_details is not an array', async () => {
const assetsPath = resolve(mockContentsPath, 'assets');
const chunkPath = resolve(assetsPath, 'chunk0-assets.json');
const original = fs.readFileSync(chunkPath, 'utf8');
const badChunk = {
asset_bad_pd: {
uid: 'asset_bad_pd',
publish_details: 'not-array',
},
};
fs.writeFileSync(chunkPath, JSON.stringify(badChunk));
try {
const instance = new Assets(constructorParam);
await instance.prerequisiteData();
const printStub = Sinon.stub(cliux, 'print');
await instance.lookForReference();
expect(printStub.called).to.be.true;
const assertMsg = $t(auditMsg.ASSET_NOT_EXIST, { uid: 'asset_bad_pd' });
expect(printStub.calledWith(assertMsg, { color: 'red' })).to.be.true;
Sinon.restore();
} finally {
fs.writeFileSync(chunkPath, typeof original === 'string' ? original : String(original));
}
});
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should log scan success message exactly once per asset', async () => {
const infoSpy = Sinon.spy();
Sinon.stub(require('@contentstack/cli-utilities'), 'log').value({
...mockLogger,
info: infoSpy,
});
const instance = new Assets(constructorParam);
await instance.prerequisiteData();
await instance.lookForReference();
const successMsgCalls = infoSpy.getCalls().filter(
(call: Sinon.SinonSpyCall) =>
typeof call.args[0] === 'string' && call.args[0].includes("Successfully completed the scanning of Asset with UID"),
);
const expectedAssetUids = ['asset_uid_1', 'asset_uid_invalid', 'asset_uid_two_invalid'];
expect(successMsgCalls).to.have.lengthOf(expectedAssetUids.length);
expectedAssetUids.forEach((uid) => {
const forUid = successMsgCalls.filter((c: Sinon.SinonSpyCall) => c.args[0].includes(uid));
expect(forUid).to.have.lengthOf(1, `expected exactly one success log for asset ${uid}`);
});
});
});
describe('integration-style run with real FsUtility', () => {
fancy
.stdout({ print: process.env.PRINT === 'true' || false })
.it('should return missingEnvLocales shape from full run with mocked progress', async () => {
const instance = new Assets(constructorParam);
const result = await instance.run(false, 5);
expect(result).to.be.an('object');
expect(result).to.have.property('asset_uid_invalid');
expect(result).to.have.property('asset_uid_two_invalid');
expect((result as any).asset_uid_invalid).to.have.lengthOf(1);
expect((result as any).asset_uid_two_invalid).to.have.lengthOf(2);
});
});
});