decap-cms-backend-github 2.15.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,833 @@
1
+ import { Base64 } from 'js-base64';
2
+
3
+ import API from '../API';
4
+
5
+ global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
6
+
7
+ describe('github API', () => {
8
+ beforeEach(() => {
9
+ jest.resetAllMocks();
10
+ });
11
+
12
+ function mockAPI(api, responses) {
13
+ api.request = jest.fn().mockImplementation((path, options = {}) => {
14
+ const normalizedPath = path.indexOf('?') !== -1 ? path.slice(0, path.indexOf('?')) : path;
15
+ const response = responses[normalizedPath];
16
+ return typeof response === 'function'
17
+ ? Promise.resolve(response(options))
18
+ : Promise.reject(new Error(`No response for path '${normalizedPath}'`));
19
+ });
20
+ }
21
+
22
+ describe('editorialWorkflowGit', () => {
23
+ it('should create PR with correct base branch name when publishing with editorial workflow', () => {
24
+ let prBaseBranch = null;
25
+ let labels = null;
26
+ const api = new API({
27
+ branch: 'gh-pages',
28
+ repo: 'owner/my-repo',
29
+ initialWorkflowStatus: 'draft',
30
+ });
31
+ const responses = {
32
+ '/repos/owner/my-repo/branches/gh-pages': () => ({ commit: { sha: 'def' } }),
33
+ '/repos/owner/my-repo/git/trees/def': () => ({ tree: [] }),
34
+ '/repos/owner/my-repo/git/trees': () => ({}),
35
+ '/repos/owner/my-repo/git/commits': () => ({}),
36
+ '/repos/owner/my-repo/git/refs': () => ({}),
37
+ '/repos/owner/my-repo/pulls': req => {
38
+ prBaseBranch = JSON.parse(req.body).base;
39
+ return { head: { sha: 'cbd' }, labels: [], number: 1 };
40
+ },
41
+ '/repos/owner/my-repo/issues/1/labels': req => {
42
+ labels = JSON.parse(req.body).labels;
43
+ return {};
44
+ },
45
+ };
46
+ mockAPI(api, responses);
47
+
48
+ return expect(
49
+ api.editorialWorkflowGit([], { slug: 'entry', sha: 'abc' }, null, {}).then(() => ({
50
+ prBaseBranch,
51
+ labels,
52
+ })),
53
+ ).resolves.toEqual({ prBaseBranch: 'gh-pages', labels: ['decap-cms/draft'] });
54
+ });
55
+
56
+ it('should create PR with correct base branch name with custom prefix when publishing with editorial workflow', () => {
57
+ let prBaseBranch = null;
58
+ let labels = null;
59
+ const api = new API({
60
+ branch: 'gh-pages',
61
+ repo: 'owner/my-repo',
62
+ initialWorkflowStatus: 'draft',
63
+ cmsLabelPrefix: 'other/',
64
+ });
65
+ const responses = {
66
+ '/repos/owner/my-repo/branches/gh-pages': () => ({ commit: { sha: 'def' } }),
67
+ '/repos/owner/my-repo/git/trees/def': () => ({ tree: [] }),
68
+ '/repos/owner/my-repo/git/trees': () => ({}),
69
+ '/repos/owner/my-repo/git/commits': () => ({}),
70
+ '/repos/owner/my-repo/git/refs': () => ({}),
71
+ '/repos/owner/my-repo/pulls': req => {
72
+ prBaseBranch = JSON.parse(req.body).base;
73
+ return { head: { sha: 'cbd' }, labels: [], number: 1 };
74
+ },
75
+ '/repos/owner/my-repo/issues/1/labels': req => {
76
+ labels = JSON.parse(req.body).labels;
77
+ return {};
78
+ },
79
+ };
80
+ mockAPI(api, responses);
81
+
82
+ return expect(
83
+ api.editorialWorkflowGit([], { slug: 'entry', sha: 'abc' }, null, {}).then(() => ({
84
+ prBaseBranch,
85
+ labels,
86
+ })),
87
+ ).resolves.toEqual({ prBaseBranch: 'gh-pages', labels: ['other/draft'] });
88
+ });
89
+ });
90
+
91
+ describe('updateTree', () => {
92
+ it('should create tree with nested paths', async () => {
93
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
94
+
95
+ api.createTree = jest.fn().mockImplementation(() => Promise.resolve({ sha: 'newTreeSha' }));
96
+
97
+ const files = [
98
+ { path: '/static/media/new-image.jpeg', sha: null },
99
+ { path: 'content/posts/new-post.md', sha: 'new-post.md' },
100
+ ];
101
+
102
+ const baseTreeSha = 'baseTreeSha';
103
+
104
+ await expect(api.updateTree(baseTreeSha, files)).resolves.toEqual({
105
+ sha: 'newTreeSha',
106
+ parentSha: baseTreeSha,
107
+ });
108
+
109
+ expect(api.createTree).toHaveBeenCalledTimes(1);
110
+ expect(api.createTree).toHaveBeenCalledWith(baseTreeSha, [
111
+ {
112
+ path: 'static/media/new-image.jpeg',
113
+ mode: '100644',
114
+ type: 'blob',
115
+ sha: null,
116
+ },
117
+ {
118
+ path: 'content/posts/new-post.md',
119
+ mode: '100644',
120
+ type: 'blob',
121
+ sha: 'new-post.md',
122
+ },
123
+ ]);
124
+ });
125
+ });
126
+
127
+ describe('request', () => {
128
+ beforeEach(() => {
129
+ const fetch = jest.fn();
130
+ global.fetch = fetch;
131
+ });
132
+
133
+ afterEach(() => {
134
+ jest.resetAllMocks();
135
+ });
136
+
137
+ it('should fetch url with authorization header', async () => {
138
+ const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
139
+
140
+ fetch.mockResolvedValue({
141
+ text: jest.fn().mockResolvedValue('some response'),
142
+ ok: true,
143
+ status: 200,
144
+ headers: { get: () => '' },
145
+ });
146
+ const result = await api.request('/some-path');
147
+ expect(result).toEqual('some response');
148
+ expect(fetch).toHaveBeenCalledTimes(1);
149
+ expect(fetch).toHaveBeenCalledWith('https://api.github.com/some-path', {
150
+ cache: 'no-cache',
151
+ headers: {
152
+ Authorization: 'token token',
153
+ 'Content-Type': 'application/json; charset=utf-8',
154
+ },
155
+ signal: expect.any(AbortSignal),
156
+ });
157
+ });
158
+
159
+ it('should throw error on not ok response', async () => {
160
+ const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
161
+
162
+ fetch.mockResolvedValue({
163
+ text: jest.fn().mockResolvedValue({ message: 'some error' }),
164
+ ok: false,
165
+ status: 404,
166
+ headers: { get: () => '' },
167
+ });
168
+
169
+ await expect(api.request('some-path')).rejects.toThrow(
170
+ expect.objectContaining({
171
+ message: 'some error',
172
+ name: 'API_ERROR',
173
+ status: 404,
174
+ api: 'GitHub',
175
+ }),
176
+ );
177
+ });
178
+
179
+ it('should allow overriding requestHeaders to return a promise ', async () => {
180
+ const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
181
+
182
+ api.requestHeaders = jest.fn().mockResolvedValue({
183
+ Authorization: 'promise-token',
184
+ 'Content-Type': 'application/json; charset=utf-8',
185
+ });
186
+
187
+ fetch.mockResolvedValue({
188
+ text: jest.fn().mockResolvedValue('some response'),
189
+ ok: true,
190
+ status: 200,
191
+ headers: { get: () => '' },
192
+ });
193
+ const result = await api.request('/some-path');
194
+ expect(result).toEqual('some response');
195
+ expect(fetch).toHaveBeenCalledTimes(1);
196
+ expect(fetch).toHaveBeenCalledWith('https://api.github.com/some-path', {
197
+ cache: 'no-cache',
198
+ headers: {
199
+ Authorization: 'promise-token',
200
+ 'Content-Type': 'application/json; charset=utf-8',
201
+ },
202
+ signal: expect.any(AbortSignal),
203
+ });
204
+ });
205
+ });
206
+
207
+ describe('persistFiles', () => {
208
+ it('should update tree, commit and patch branch when useWorkflow is false', async () => {
209
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
210
+
211
+ const responses = {
212
+ // upload the file
213
+ '/repos/owner/repo/git/blobs': () => ({ sha: 'new-file-sha' }),
214
+
215
+ // get the branch
216
+ '/repos/owner/repo/branches/master': () => ({ commit: { sha: 'root' } }),
217
+
218
+ // create new tree
219
+ '/repos/owner/repo/git/trees': options => {
220
+ const data = JSON.parse(options.body);
221
+ return { sha: data.base_tree };
222
+ },
223
+
224
+ // update the commit with the tree
225
+ '/repos/owner/repo/git/commits': () => ({ sha: 'commit-sha' }),
226
+
227
+ // patch the branch
228
+ '/repos/owner/repo/git/refs/heads/master': () => ({}),
229
+ };
230
+ mockAPI(api, responses);
231
+
232
+ const entry = {
233
+ dataFiles: [
234
+ {
235
+ slug: 'entry',
236
+ sha: 'abc',
237
+ path: 'content/posts/new-post.md',
238
+ raw: 'content',
239
+ },
240
+ ],
241
+ assets: [],
242
+ };
243
+ await api.persistFiles(entry.dataFiles, entry.assets, { commitMessage: 'commitMessage' });
244
+
245
+ expect(api.request).toHaveBeenCalledTimes(5);
246
+
247
+ expect(api.request.mock.calls[0]).toEqual([
248
+ '/repos/owner/repo/git/blobs',
249
+ {
250
+ method: 'POST',
251
+ body: JSON.stringify({
252
+ content: Base64.encode(entry.dataFiles[0].raw),
253
+ encoding: 'base64',
254
+ }),
255
+ },
256
+ ]);
257
+
258
+ expect(api.request.mock.calls[1]).toEqual(['/repos/owner/repo/branches/master']);
259
+
260
+ expect(api.request.mock.calls[2]).toEqual([
261
+ '/repos/owner/repo/git/trees',
262
+ {
263
+ body: JSON.stringify({
264
+ base_tree: 'root',
265
+ tree: [
266
+ {
267
+ path: 'content/posts/new-post.md',
268
+ mode: '100644',
269
+ type: 'blob',
270
+ sha: 'new-file-sha',
271
+ },
272
+ ],
273
+ }),
274
+ method: 'POST',
275
+ },
276
+ ]);
277
+
278
+ expect(api.request.mock.calls[3]).toEqual([
279
+ '/repos/owner/repo/git/commits',
280
+ {
281
+ body: JSON.stringify({
282
+ message: 'commitMessage',
283
+ tree: 'root',
284
+ parents: ['root'],
285
+ }),
286
+ method: 'POST',
287
+ },
288
+ ]);
289
+
290
+ expect(api.request.mock.calls[4]).toEqual([
291
+ '/repos/owner/repo/git/refs/heads/master',
292
+ {
293
+ body: JSON.stringify({
294
+ sha: 'commit-sha',
295
+ force: false,
296
+ }),
297
+ method: 'PATCH',
298
+ },
299
+ ]);
300
+ });
301
+
302
+ it('should call editorialWorkflowGit when useWorkflow is true', async () => {
303
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
304
+
305
+ api.uploadBlob = jest.fn();
306
+ api.editorialWorkflowGit = jest.fn();
307
+
308
+ const entry = {
309
+ dataFiles: [
310
+ {
311
+ slug: 'entry',
312
+ sha: 'abc',
313
+ path: 'content/posts/new-post.md',
314
+ raw: 'content',
315
+ },
316
+ ],
317
+ assets: [
318
+ {
319
+ path: '/static/media/image-1.png',
320
+ sha: 'image-1.png',
321
+ },
322
+ {
323
+ path: '/static/media/image-2.png',
324
+ sha: 'image-2.png',
325
+ },
326
+ ],
327
+ };
328
+
329
+ await api.persistFiles(entry.dataFiles, entry.assets, { useWorkflow: true });
330
+
331
+ expect(api.uploadBlob).toHaveBeenCalledTimes(3);
332
+ expect(api.uploadBlob).toHaveBeenCalledWith(entry.dataFiles[0]);
333
+ expect(api.uploadBlob).toHaveBeenCalledWith(entry.assets[0]);
334
+ expect(api.uploadBlob).toHaveBeenCalledWith(entry.assets[1]);
335
+
336
+ expect(api.editorialWorkflowGit).toHaveBeenCalledTimes(1);
337
+
338
+ expect(api.editorialWorkflowGit).toHaveBeenCalledWith(
339
+ entry.assets.concat(entry.dataFiles),
340
+ entry.dataFiles[0].slug,
341
+ [
342
+ { path: 'static/media/image-1.png', sha: 'image-1.png' },
343
+ { path: 'static/media/image-2.png', sha: 'image-2.png' },
344
+ ],
345
+ { useWorkflow: true },
346
+ );
347
+ });
348
+ });
349
+
350
+ describe('migratePullRequest', () => {
351
+ it('should migrate to pull request labels when no version', async () => {
352
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
353
+
354
+ const pr = {
355
+ head: { ref: 'cms/2019-11-11-post-title' },
356
+ title: 'pr title',
357
+ number: 1,
358
+ labels: [],
359
+ };
360
+ const metadata = { type: 'PR' };
361
+ api.retrieveMetadataOld = jest.fn().mockResolvedValue(metadata);
362
+ const newBranch = 'cms/posts/2019-11-11-post-title';
363
+ const migrateToVersion1Result = {
364
+ metadata: { ...metadata, branch: newBranch, version: '1' },
365
+ pullRequest: { ...pr, number: 2 },
366
+ };
367
+ api.migrateToVersion1 = jest.fn().mockResolvedValue(migrateToVersion1Result);
368
+ api.migrateToPullRequestLabels = jest.fn();
369
+
370
+ await api.migratePullRequest(pr);
371
+
372
+ expect(api.migrateToVersion1).toHaveBeenCalledTimes(1);
373
+ expect(api.migrateToVersion1).toHaveBeenCalledWith(pr, metadata);
374
+
375
+ expect(api.migrateToPullRequestLabels).toHaveBeenCalledTimes(1);
376
+ expect(api.migrateToPullRequestLabels).toHaveBeenCalledWith(
377
+ migrateToVersion1Result.pullRequest,
378
+ migrateToVersion1Result.metadata,
379
+ );
380
+
381
+ expect(api.retrieveMetadataOld).toHaveBeenCalledTimes(1);
382
+ expect(api.retrieveMetadataOld).toHaveBeenCalledWith('2019-11-11-post-title');
383
+ });
384
+
385
+ it('should migrate to pull request labels when version is 1', async () => {
386
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
387
+
388
+ api.migrateToVersion1 = jest.fn();
389
+ const pr = {
390
+ head: { ref: 'cms/posts/2019-11-11-post-title' },
391
+ title: 'pr title',
392
+ number: 1,
393
+ labels: [],
394
+ };
395
+ const metadata = { type: 'PR', version: '1' };
396
+ api.retrieveMetadataOld = jest.fn().mockResolvedValue(metadata);
397
+ api.migrateToPullRequestLabels = jest.fn().mockResolvedValue(pr, metadata);
398
+
399
+ await api.migratePullRequest(pr);
400
+
401
+ expect(api.migrateToVersion1).toHaveBeenCalledTimes(0);
402
+
403
+ expect(api.migrateToPullRequestLabels).toHaveBeenCalledTimes(1);
404
+ expect(api.migrateToPullRequestLabels).toHaveBeenCalledWith(pr, metadata);
405
+
406
+ expect(api.retrieveMetadataOld).toHaveBeenCalledTimes(1);
407
+ expect(api.retrieveMetadataOld).toHaveBeenCalledWith('posts/2019-11-11-post-title');
408
+ });
409
+ });
410
+
411
+ describe('migrateToVersion1', () => {
412
+ it('should migrate to version 1', async () => {
413
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
414
+
415
+ const pr = {
416
+ head: { ref: 'cms/2019-11-11-post-title', sha: 'pr_head' },
417
+ title: 'pr title',
418
+ number: 1,
419
+ labels: [],
420
+ };
421
+
422
+ const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' };
423
+ api.createBranch = jest.fn().mockResolvedValue(newBranch);
424
+ api.getBranch = jest.fn().mockRejectedValue(new Error('Branch not found'));
425
+
426
+ const newPr = { ...pr, number: 2 };
427
+ api.createPR = jest.fn().mockResolvedValue(newPr);
428
+ api.getPullRequests = jest.fn().mockResolvedValue([]);
429
+
430
+ api.storeMetadata = jest.fn();
431
+ api.closePR = jest.fn();
432
+ api.deleteBranch = jest.fn();
433
+ api.deleteMetadata = jest.fn();
434
+
435
+ const branch = 'cms/2019-11-11-post-title';
436
+ const metadata = {
437
+ branch,
438
+ type: 'PR',
439
+ pr: { head: pr.head.sha },
440
+ commitMessage: 'commitMessage',
441
+ collection: 'posts',
442
+ };
443
+
444
+ const expectedMetadata = {
445
+ type: 'PR',
446
+ pr: { head: newPr.head.sha, number: 2 },
447
+ commitMessage: 'commitMessage',
448
+ collection: 'posts',
449
+ branch: 'cms/posts/2019-11-11-post-title',
450
+ version: '1',
451
+ };
452
+ await expect(api.migrateToVersion1(pr, metadata)).resolves.toEqual({
453
+ metadata: expectedMetadata,
454
+ pullRequest: newPr,
455
+ });
456
+
457
+ expect(api.getBranch).toHaveBeenCalledTimes(1);
458
+ expect(api.getBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title');
459
+ expect(api.createBranch).toHaveBeenCalledTimes(1);
460
+ expect(api.createBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title', 'pr_head');
461
+
462
+ expect(api.getPullRequests).toHaveBeenCalledTimes(1);
463
+ expect(api.getPullRequests).toHaveBeenCalledWith(
464
+ 'cms/posts/2019-11-11-post-title',
465
+ 'all',
466
+ expect.any(Function),
467
+ );
468
+ expect(api.createPR).toHaveBeenCalledTimes(1);
469
+ expect(api.createPR).toHaveBeenCalledWith('pr title', 'cms/posts/2019-11-11-post-title');
470
+
471
+ expect(api.storeMetadata).toHaveBeenCalledTimes(1);
472
+ expect(api.storeMetadata).toHaveBeenCalledWith(
473
+ 'posts/2019-11-11-post-title',
474
+ expectedMetadata,
475
+ );
476
+
477
+ expect(api.closePR).toHaveBeenCalledTimes(1);
478
+ expect(api.closePR).toHaveBeenCalledWith(pr.number);
479
+
480
+ expect(api.deleteBranch).toHaveBeenCalledTimes(1);
481
+ expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title');
482
+
483
+ expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
484
+ expect(api.deleteMetadata).toHaveBeenCalledWith('2019-11-11-post-title');
485
+ });
486
+
487
+ it('should not create new branch if exists', async () => {
488
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
489
+
490
+ const pr = {
491
+ head: { ref: 'cms/2019-11-11-post-title', sha: 'pr_head' },
492
+ title: 'pr title',
493
+ number: 1,
494
+ labels: [],
495
+ };
496
+
497
+ const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' };
498
+ api.createBranch = jest.fn();
499
+ api.getBranch = jest.fn().mockResolvedValue(newBranch);
500
+
501
+ const newPr = { ...pr, number: 2 };
502
+ api.createPR = jest.fn().mockResolvedValue(newPr);
503
+ api.getPullRequests = jest.fn().mockResolvedValue([]);
504
+
505
+ api.storeMetadata = jest.fn();
506
+ api.closePR = jest.fn();
507
+ api.deleteBranch = jest.fn();
508
+ api.deleteMetadata = jest.fn();
509
+
510
+ const branch = 'cms/2019-11-11-post-title';
511
+ const metadata = {
512
+ branch,
513
+ type: 'PR',
514
+ pr: { head: pr.head.sha },
515
+ commitMessage: 'commitMessage',
516
+ collection: 'posts',
517
+ };
518
+
519
+ const expectedMetadata = {
520
+ type: 'PR',
521
+ pr: { head: newPr.head.sha, number: 2 },
522
+ commitMessage: 'commitMessage',
523
+ collection: 'posts',
524
+ branch: 'cms/posts/2019-11-11-post-title',
525
+ version: '1',
526
+ };
527
+ await expect(api.migrateToVersion1(pr, metadata)).resolves.toEqual({
528
+ metadata: expectedMetadata,
529
+ pullRequest: newPr,
530
+ });
531
+
532
+ expect(api.getBranch).toHaveBeenCalledTimes(1);
533
+ expect(api.getBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title');
534
+ expect(api.createBranch).toHaveBeenCalledTimes(0);
535
+
536
+ expect(api.getPullRequests).toHaveBeenCalledTimes(1);
537
+ expect(api.getPullRequests).toHaveBeenCalledWith(
538
+ 'cms/posts/2019-11-11-post-title',
539
+ 'all',
540
+ expect.any(Function),
541
+ );
542
+ expect(api.createPR).toHaveBeenCalledTimes(1);
543
+ expect(api.createPR).toHaveBeenCalledWith('pr title', 'cms/posts/2019-11-11-post-title');
544
+
545
+ expect(api.storeMetadata).toHaveBeenCalledTimes(1);
546
+ expect(api.storeMetadata).toHaveBeenCalledWith(
547
+ 'posts/2019-11-11-post-title',
548
+ expectedMetadata,
549
+ );
550
+
551
+ expect(api.closePR).toHaveBeenCalledTimes(1);
552
+ expect(api.closePR).toHaveBeenCalledWith(pr.number);
553
+
554
+ expect(api.deleteBranch).toHaveBeenCalledTimes(1);
555
+ expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title');
556
+
557
+ expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
558
+ expect(api.deleteMetadata).toHaveBeenCalledWith('2019-11-11-post-title');
559
+ });
560
+
561
+ it('should not create new pr if exists', async () => {
562
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
563
+
564
+ const pr = {
565
+ head: { ref: 'cms/2019-11-11-post-title', sha: 'pr_head' },
566
+ title: 'pr title',
567
+ number: 1,
568
+ labels: [],
569
+ };
570
+
571
+ const newBranch = { ref: 'refs/heads/cms/posts/2019-11-11-post-title' };
572
+ api.createBranch = jest.fn();
573
+ api.getBranch = jest.fn().mockResolvedValue(newBranch);
574
+
575
+ const newPr = { ...pr, number: 2 };
576
+ api.createPR = jest.fn();
577
+ api.getPullRequests = jest.fn().mockResolvedValue([newPr]);
578
+
579
+ api.storeMetadata = jest.fn();
580
+ api.closePR = jest.fn();
581
+ api.deleteBranch = jest.fn();
582
+ api.deleteMetadata = jest.fn();
583
+
584
+ const branch = 'cms/2019-11-11-post-title';
585
+ const metadata = {
586
+ branch,
587
+ type: 'PR',
588
+ pr: { head: pr.head.sha },
589
+ commitMessage: 'commitMessage',
590
+ collection: 'posts',
591
+ };
592
+
593
+ const expectedMetadata = {
594
+ type: 'PR',
595
+ pr: { head: newPr.head.sha, number: 2 },
596
+ commitMessage: 'commitMessage',
597
+ collection: 'posts',
598
+ branch: 'cms/posts/2019-11-11-post-title',
599
+ version: '1',
600
+ };
601
+ await expect(api.migrateToVersion1(pr, metadata)).resolves.toEqual({
602
+ metadata: expectedMetadata,
603
+ pullRequest: newPr,
604
+ });
605
+
606
+ expect(api.getBranch).toHaveBeenCalledTimes(1);
607
+ expect(api.getBranch).toHaveBeenCalledWith('cms/posts/2019-11-11-post-title');
608
+ expect(api.createBranch).toHaveBeenCalledTimes(0);
609
+
610
+ expect(api.getPullRequests).toHaveBeenCalledTimes(1);
611
+ expect(api.getPullRequests).toHaveBeenCalledWith(
612
+ 'cms/posts/2019-11-11-post-title',
613
+ 'all',
614
+ expect.any(Function),
615
+ );
616
+ expect(api.createPR).toHaveBeenCalledTimes(0);
617
+
618
+ expect(api.storeMetadata).toHaveBeenCalledTimes(1);
619
+ expect(api.storeMetadata).toHaveBeenCalledWith(
620
+ 'posts/2019-11-11-post-title',
621
+ expectedMetadata,
622
+ );
623
+
624
+ expect(api.closePR).toHaveBeenCalledTimes(1);
625
+ expect(api.closePR).toHaveBeenCalledWith(pr.number);
626
+
627
+ expect(api.deleteBranch).toHaveBeenCalledTimes(1);
628
+ expect(api.deleteBranch).toHaveBeenCalledWith('cms/2019-11-11-post-title');
629
+
630
+ expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
631
+ expect(api.deleteMetadata).toHaveBeenCalledWith('2019-11-11-post-title');
632
+ });
633
+ });
634
+
635
+ describe('migrateToPullRequestLabels', () => {
636
+ it('should migrate to pull request labels', async () => {
637
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
638
+
639
+ const pr = {
640
+ head: { ref: 'cms/posts/2019-11-11-post-title', sha: 'pr_head' },
641
+ title: 'pr title',
642
+ number: 1,
643
+ labels: [],
644
+ };
645
+
646
+ api.setPullRequestStatus = jest.fn();
647
+ api.deleteMetadata = jest.fn();
648
+
649
+ const metadata = {
650
+ branch: pr.head.ref,
651
+ type: 'PR',
652
+ pr: { head: pr.head.sha },
653
+ commitMessage: 'commitMessage',
654
+ collection: 'posts',
655
+ status: 'pending_review',
656
+ };
657
+
658
+ await api.migrateToPullRequestLabels(pr, metadata);
659
+
660
+ expect(api.setPullRequestStatus).toHaveBeenCalledTimes(1);
661
+ expect(api.setPullRequestStatus).toHaveBeenCalledWith(pr, 'pending_review');
662
+
663
+ expect(api.deleteMetadata).toHaveBeenCalledTimes(1);
664
+ expect(api.deleteMetadata).toHaveBeenCalledWith('posts/2019-11-11-post-title');
665
+ });
666
+ });
667
+
668
+ describe('rebaseSingleCommit', () => {
669
+ it('should create updated tree and commit', async () => {
670
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
671
+
672
+ api.getDifferences = jest.fn().mockResolvedValueOnce({
673
+ files: [
674
+ { filename: 'removed.md', status: 'removed', sha: 'removed_sha' },
675
+ {
676
+ filename: 'renamed.md',
677
+ status: 'renamed',
678
+ previous_filename: 'previous_filename.md',
679
+ sha: 'renamed_sha',
680
+ },
681
+ { filename: 'added.md', status: 'added', sha: 'added_sha' },
682
+ ],
683
+ });
684
+
685
+ const newTree = { sha: 'new_tree_sha' };
686
+ api.updateTree = jest.fn().mockResolvedValueOnce(newTree);
687
+
688
+ const newCommit = { sha: 'newCommit' };
689
+ api.createCommit = jest.fn().mockResolvedValueOnce(newCommit);
690
+
691
+ const baseCommit = { sha: 'base_commit_sha' };
692
+ const commit = {
693
+ sha: 'sha',
694
+ parents: [{ sha: 'parent_sha' }],
695
+ commit: {
696
+ message: 'message',
697
+ author: { name: 'author' },
698
+ committer: { name: 'committer' },
699
+ },
700
+ };
701
+
702
+ await expect(api.rebaseSingleCommit(baseCommit, commit)).resolves.toBe(newCommit);
703
+
704
+ expect(api.getDifferences).toHaveBeenCalledTimes(1);
705
+ expect(api.getDifferences).toHaveBeenCalledWith('parent_sha', 'sha');
706
+
707
+ expect(api.updateTree).toHaveBeenCalledTimes(1);
708
+ expect(api.updateTree).toHaveBeenCalledWith('base_commit_sha', [
709
+ { path: 'removed.md', sha: null },
710
+ { path: 'previous_filename.md', sha: null },
711
+ { path: 'renamed.md', sha: 'renamed_sha' },
712
+ { path: 'added.md', sha: 'added_sha' },
713
+ ]);
714
+
715
+ expect(api.createCommit).toHaveBeenCalledTimes(1);
716
+ expect(api.createCommit).toHaveBeenCalledWith(
717
+ 'message',
718
+ newTree.sha,
719
+ [baseCommit.sha],
720
+ { name: 'author' },
721
+ { name: 'committer' },
722
+ );
723
+ });
724
+ });
725
+
726
+ describe('listFiles', () => {
727
+ it('should get files by depth', async () => {
728
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
729
+
730
+ const tree = [
731
+ {
732
+ path: 'post.md',
733
+ type: 'blob',
734
+ },
735
+ {
736
+ path: 'dir1',
737
+ type: 'tree',
738
+ },
739
+ {
740
+ path: 'dir1/nested-post.md',
741
+ type: 'blob',
742
+ },
743
+ {
744
+ path: 'dir1/dir2',
745
+ type: 'tree',
746
+ },
747
+ {
748
+ path: 'dir1/dir2/nested-post.md',
749
+ type: 'blob',
750
+ },
751
+ ];
752
+ api.request = jest.fn().mockResolvedValue({ tree });
753
+
754
+ await expect(api.listFiles('posts', { depth: 1 })).resolves.toEqual([
755
+ {
756
+ path: 'posts/post.md',
757
+ type: 'blob',
758
+ name: 'post.md',
759
+ },
760
+ ]);
761
+ expect(api.request).toHaveBeenCalledTimes(1);
762
+ expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
763
+ params: {},
764
+ });
765
+
766
+ jest.clearAllMocks();
767
+ await expect(api.listFiles('posts', { depth: 2 })).resolves.toEqual([
768
+ {
769
+ path: 'posts/post.md',
770
+ type: 'blob',
771
+ name: 'post.md',
772
+ },
773
+ {
774
+ path: 'posts/dir1/nested-post.md',
775
+ type: 'blob',
776
+ name: 'nested-post.md',
777
+ },
778
+ ]);
779
+ expect(api.request).toHaveBeenCalledTimes(1);
780
+ expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
781
+ params: { recursive: 1 },
782
+ });
783
+
784
+ jest.clearAllMocks();
785
+ await expect(api.listFiles('posts', { depth: 3 })).resolves.toEqual([
786
+ {
787
+ path: 'posts/post.md',
788
+ type: 'blob',
789
+ name: 'post.md',
790
+ },
791
+ {
792
+ path: 'posts/dir1/nested-post.md',
793
+ type: 'blob',
794
+ name: 'nested-post.md',
795
+ },
796
+ {
797
+ path: 'posts/dir1/dir2/nested-post.md',
798
+ type: 'blob',
799
+ name: 'nested-post.md',
800
+ },
801
+ ]);
802
+ expect(api.request).toHaveBeenCalledTimes(1);
803
+ expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
804
+ params: { recursive: 1 },
805
+ });
806
+ });
807
+ });
808
+
809
+ test('should get preview statuses', async () => {
810
+ const api = new API({ repo: 'repo' });
811
+
812
+ const statuses = [
813
+ { context: 'deploy', state: 'success', target_url: 'deploy-url' },
814
+ { context: 'build', state: 'error' },
815
+ ];
816
+
817
+ api.request = jest.fn(() => Promise.resolve({ statuses }));
818
+ const sha = 'sha';
819
+ api.getBranchPullRequest = jest.fn(() => Promise.resolve({ head: { sha } }));
820
+
821
+ const collection = 'collection';
822
+ const slug = 'slug';
823
+ await expect(api.getStatuses(collection, slug)).resolves.toEqual([
824
+ { context: 'deploy', state: 'success', target_url: 'deploy-url' },
825
+ { context: 'build', state: 'other' },
826
+ ]);
827
+
828
+ expect(api.getBranchPullRequest).toHaveBeenCalledTimes(1);
829
+ expect(api.getBranchPullRequest).toHaveBeenCalledWith('cms/collection/slug');
830
+ expect(api.request).toHaveBeenCalledTimes(1);
831
+ expect(api.request).toHaveBeenCalledWith(`/repos/repo/commits/${sha}/status`);
832
+ });
833
+ });