decap-cms-backend-forgejo 3.4.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.
- package/CHANGELOG.md +10 -0
- package/LICENSE +22 -0
- package/dist/decap-cms-backend-forgejo.js +12 -0
- package/dist/decap-cms-backend-forgejo.js.LICENSE.txt +18 -0
- package/dist/decap-cms-backend-forgejo.js.map +1 -0
- package/dist/esm/API.js +821 -0
- package/dist/esm/AuthenticationPage.js +233 -0
- package/dist/esm/implementation.js +539 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/types.js +1 -0
- package/package.json +38 -0
- package/src/API.ts +1054 -0
- package/src/AuthenticationPage.js +207 -0
- package/src/__tests__/API.spec.js +1479 -0
- package/src/__tests__/implementation.spec.js +530 -0
- package/src/implementation.tsx +652 -0
- package/src/index.ts +10 -0
- package/src/types.ts +344 -0
- package/webpack.config.js +3 -0
|
@@ -0,0 +1,1479 @@
|
|
|
1
|
+
import { Base64 } from 'js-base64';
|
|
2
|
+
import { APIError, EditorialWorkflowError } from 'decap-cms-lib-util';
|
|
3
|
+
|
|
4
|
+
import APIClass, { MOCK_PULL_REQUEST } from '../API';
|
|
5
|
+
|
|
6
|
+
const TEST_API_ROOT = 'https://v14.next.forgejo.org/api/v1';
|
|
7
|
+
|
|
8
|
+
function API(config) {
|
|
9
|
+
return new APIClass({ apiRoot: TEST_API_ROOT, ...config });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
global.fetch = jest.fn().mockRejectedValue(new Error('should not call fetch inside tests'));
|
|
13
|
+
|
|
14
|
+
describe('forgejo API', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
function mockAPI(api, responses) {
|
|
21
|
+
api.request = jest.fn().mockImplementation((path, options = {}) => {
|
|
22
|
+
const normalizedPath = path.indexOf('?') !== -1 ? path.slice(0, path.indexOf('?')) : path;
|
|
23
|
+
const response = responses[normalizedPath];
|
|
24
|
+
return typeof response === 'function'
|
|
25
|
+
? Promise.resolve(response(options))
|
|
26
|
+
: Promise.reject(new Error(`No response for path '${normalizedPath}'`));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('generateContentKey and parseContentKey', () => {
|
|
31
|
+
it('should generate standard content key without OA', () => {
|
|
32
|
+
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
|
33
|
+
const key = api.generateContentKey('posts', 'my-post');
|
|
34
|
+
expect(key).toEqual('posts/my-post');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should generate OA content key with repo prefix', () => {
|
|
38
|
+
const api = new API({
|
|
39
|
+
branch: 'master',
|
|
40
|
+
repo: 'contributor/repo',
|
|
41
|
+
originRepo: 'owner/repo',
|
|
42
|
+
useOpenAuthoring: true,
|
|
43
|
+
});
|
|
44
|
+
const key = api.generateContentKey('posts', 'my-post');
|
|
45
|
+
expect(key).toEqual('contributor/repo/posts/my-post');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should parse standard content key without OA', () => {
|
|
49
|
+
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
|
50
|
+
const result = api.parseContentKey('posts/my-post');
|
|
51
|
+
expect(result).toEqual({ collection: 'posts', slug: 'my-post' });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should parse OA content key by stripping repo prefix', () => {
|
|
55
|
+
const api = new API({
|
|
56
|
+
branch: 'master',
|
|
57
|
+
repo: 'contributor/repo',
|
|
58
|
+
originRepo: 'owner/repo',
|
|
59
|
+
useOpenAuthoring: true,
|
|
60
|
+
});
|
|
61
|
+
const result = api.parseContentKey('contributor/repo/posts/my-post');
|
|
62
|
+
expect(result).toEqual({ collection: 'posts', slug: 'my-post' });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('getHeadReference', () => {
|
|
67
|
+
it('should return owner:branch format', async () => {
|
|
68
|
+
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
|
69
|
+
const ref = await api.getHeadReference('cms/posts/test');
|
|
70
|
+
expect(ref).toEqual('owner:cms/posts/test');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('editorialWorkflowGit', () => {
|
|
75
|
+
it('should create PR with correct branch when publishing with editorial workflow', async () => {
|
|
76
|
+
const api = new API({
|
|
77
|
+
branch: 'master',
|
|
78
|
+
repo: 'owner/my-repo',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Mock getBranch to indicate branch doesn't exist yet
|
|
82
|
+
api.getBranch = jest.fn().mockRejectedValue(new APIError('Branch not found', 404, 'Forgejo'));
|
|
83
|
+
api.createBranch = jest.fn().mockResolvedValue({ name: 'cms/posts/entry' });
|
|
84
|
+
|
|
85
|
+
const changeOperations = [{ operation: 'create', path: 'content.md', content: 'test' }];
|
|
86
|
+
api.getChangeFileOperations = jest.fn().mockResolvedValue(changeOperations);
|
|
87
|
+
api.changeFilesOnBranch = jest.fn().mockResolvedValue({});
|
|
88
|
+
|
|
89
|
+
const newPr = { number: 1, labels: [], head: { ref: 'cms/posts/entry' } };
|
|
90
|
+
api.createPR = jest.fn().mockResolvedValue(newPr);
|
|
91
|
+
api.setPullRequestStatus = jest.fn().mockResolvedValue();
|
|
92
|
+
|
|
93
|
+
const files = [{ path: 'content.md', raw: 'test content' }];
|
|
94
|
+
const options = { commitMessage: 'Add entry', status: 'draft' };
|
|
95
|
+
|
|
96
|
+
await api.editorialWorkflowGit(files, 'entry', 'posts', options);
|
|
97
|
+
|
|
98
|
+
expect(api.getBranch).toHaveBeenCalledWith('cms/posts/entry');
|
|
99
|
+
expect(api.createBranch).toHaveBeenCalledWith('cms/posts/entry', 'master');
|
|
100
|
+
expect(api.getChangeFileOperations).toHaveBeenCalledWith(files, 'cms/posts/entry');
|
|
101
|
+
expect(api.changeFilesOnBranch).toHaveBeenCalledWith(
|
|
102
|
+
changeOperations,
|
|
103
|
+
options,
|
|
104
|
+
'cms/posts/entry',
|
|
105
|
+
);
|
|
106
|
+
expect(api.createPR).toHaveBeenCalledWith('Add entry', 'cms/posts/entry');
|
|
107
|
+
expect(api.setPullRequestStatus).toHaveBeenCalledWith(newPr, 'draft');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should not create branch if it already exists', async () => {
|
|
111
|
+
const api = new API({
|
|
112
|
+
branch: 'master',
|
|
113
|
+
repo: 'owner/my-repo',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Mock getBranch to indicate branch already exists
|
|
117
|
+
api.getBranch = jest.fn().mockResolvedValue({ name: 'cms/posts/entry' });
|
|
118
|
+
api.createBranch = jest.fn();
|
|
119
|
+
|
|
120
|
+
const changeOperations = [{ operation: 'update', path: 'content.md', content: 'updated' }];
|
|
121
|
+
api.getChangeFileOperations = jest.fn().mockResolvedValue(changeOperations);
|
|
122
|
+
api.changeFilesOnBranch = jest.fn().mockResolvedValue({});
|
|
123
|
+
|
|
124
|
+
api.createPR = jest.fn();
|
|
125
|
+
api.setPullRequestStatus = jest.fn();
|
|
126
|
+
|
|
127
|
+
const files = [{ path: 'content.md', raw: 'updated content' }];
|
|
128
|
+
const options = { commitMessage: 'Update entry' };
|
|
129
|
+
|
|
130
|
+
await api.editorialWorkflowGit(files, 'entry', 'posts', options);
|
|
131
|
+
|
|
132
|
+
expect(api.getBranch).toHaveBeenCalledWith('cms/posts/entry');
|
|
133
|
+
expect(api.createBranch).not.toHaveBeenCalled();
|
|
134
|
+
expect(api.createPR).not.toHaveBeenCalled();
|
|
135
|
+
expect(api.setPullRequestStatus).not.toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should not create PR for open authoring (branch-only draft)', async () => {
|
|
139
|
+
const api = new API({
|
|
140
|
+
branch: 'master',
|
|
141
|
+
repo: 'contributor/repo',
|
|
142
|
+
originRepo: 'owner/repo',
|
|
143
|
+
useOpenAuthoring: true,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
api.getBranch = jest.fn().mockRejectedValue(new APIError('Branch not found', 404, 'Forgejo'));
|
|
147
|
+
api.createBranch = jest.fn().mockResolvedValue({ name: 'cms/contributor/repo/posts/entry' });
|
|
148
|
+
|
|
149
|
+
const changeOperations = [{ operation: 'create', path: 'content.md', content: 'test' }];
|
|
150
|
+
api.getChangeFileOperations = jest.fn().mockResolvedValue(changeOperations);
|
|
151
|
+
api.changeFilesOnBranch = jest.fn().mockResolvedValue({});
|
|
152
|
+
|
|
153
|
+
api.createPR = jest.fn();
|
|
154
|
+
api.setPullRequestStatus = jest.fn();
|
|
155
|
+
|
|
156
|
+
const files = [{ path: 'content.md', raw: 'test content' }];
|
|
157
|
+
const options = { commitMessage: 'Add entry', status: 'draft' };
|
|
158
|
+
|
|
159
|
+
await api.editorialWorkflowGit(files, 'entry', 'posts', options);
|
|
160
|
+
|
|
161
|
+
expect(api.createBranch).toHaveBeenCalled();
|
|
162
|
+
expect(api.createPR).not.toHaveBeenCalled();
|
|
163
|
+
expect(api.setPullRequestStatus).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('request', () => {
|
|
168
|
+
const fetch = jest.fn();
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
global.fetch = fetch;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
afterEach(() => {
|
|
174
|
+
jest.clearAllMocks();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should fetch url with authorization header', async () => {
|
|
178
|
+
const api = new API({ branch: 'gh-pages', repo: 'owner/my-repo', token: 'token' });
|
|
179
|
+
|
|
180
|
+
fetch.mockResolvedValue({
|
|
181
|
+
text: jest.fn().mockResolvedValue('some response'),
|
|
182
|
+
ok: true,
|
|
183
|
+
status: 200,
|
|
184
|
+
headers: { get: () => '' },
|
|
185
|
+
});
|
|
186
|
+
const result = await api.request('/some-path');
|
|
187
|
+
expect(result).toEqual('some response');
|
|
188
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
189
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
190
|
+
'https://v14.next.forgejo.org/api/v1/some-path',
|
|
191
|
+
expect.objectContaining({
|
|
192
|
+
cache: 'no-cache',
|
|
193
|
+
headers: {
|
|
194
|
+
Authorization: 'token token',
|
|
195
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should throw error on not ok response', async () => {
|
|
202
|
+
const api = new API({ branch: 'gt-pages', repo: 'owner/my-repo', token: 'token' });
|
|
203
|
+
|
|
204
|
+
fetch.mockResolvedValue({
|
|
205
|
+
text: jest.fn().mockResolvedValue({ message: 'some error' }),
|
|
206
|
+
ok: false,
|
|
207
|
+
status: 404,
|
|
208
|
+
headers: { get: () => '' },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await expect(api.request('some-path')).rejects.toThrow(
|
|
212
|
+
expect.objectContaining({
|
|
213
|
+
message: 'some error',
|
|
214
|
+
name: 'API_ERROR',
|
|
215
|
+
status: 404,
|
|
216
|
+
api: 'Forgejo',
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should allow overriding requestHeaders to return a promise ', async () => {
|
|
222
|
+
const api = new API({ branch: 'gt-pages', repo: 'owner/my-repo', token: 'token' });
|
|
223
|
+
|
|
224
|
+
api.requestHeaders = jest.fn().mockResolvedValue({
|
|
225
|
+
Authorization: 'promise-token',
|
|
226
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
fetch.mockResolvedValue({
|
|
230
|
+
text: jest.fn().mockResolvedValue('some response'),
|
|
231
|
+
ok: true,
|
|
232
|
+
status: 200,
|
|
233
|
+
headers: { get: () => '' },
|
|
234
|
+
});
|
|
235
|
+
const result = await api.request('/some-path');
|
|
236
|
+
expect(result).toEqual('some response');
|
|
237
|
+
expect(fetch).toHaveBeenCalledTimes(1);
|
|
238
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
239
|
+
'https://v14.next.forgejo.org/api/v1/some-path',
|
|
240
|
+
expect.objectContaining({
|
|
241
|
+
cache: 'no-cache',
|
|
242
|
+
headers: {
|
|
243
|
+
Authorization: 'promise-token',
|
|
244
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('persistFiles', () => {
|
|
252
|
+
it('should create a new commit', async () => {
|
|
253
|
+
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
|
254
|
+
|
|
255
|
+
const responses = {
|
|
256
|
+
'/repos/owner/repo/contents/content/posts/update-post.md': () => {
|
|
257
|
+
return { sha: 'old-sha' };
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
'/repos/owner/repo/contents': () => ({
|
|
261
|
+
commit: { sha: 'new-sha' },
|
|
262
|
+
files: [
|
|
263
|
+
{
|
|
264
|
+
path: 'content/posts/new-post.md',
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
path: 'content/posts/update-post.md',
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
mockAPI(api, responses);
|
|
273
|
+
|
|
274
|
+
const entry = {
|
|
275
|
+
dataFiles: [
|
|
276
|
+
{
|
|
277
|
+
slug: 'entry',
|
|
278
|
+
path: 'content/posts/new-post.md',
|
|
279
|
+
raw: 'content',
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
slug: 'entry',
|
|
283
|
+
sha: 'old-sha',
|
|
284
|
+
path: 'content/posts/update-post.md',
|
|
285
|
+
raw: 'content',
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
assets: [],
|
|
289
|
+
};
|
|
290
|
+
await expect(
|
|
291
|
+
api.persistFiles(entry.dataFiles, entry.assets, {
|
|
292
|
+
commitMessage: 'commitMessage',
|
|
293
|
+
newEntry: true,
|
|
294
|
+
}),
|
|
295
|
+
).resolves.toEqual({
|
|
296
|
+
commit: { sha: 'new-sha' },
|
|
297
|
+
files: [
|
|
298
|
+
{
|
|
299
|
+
path: 'content/posts/new-post.md',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
path: 'content/posts/update-post.md',
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(api.request).toHaveBeenCalledTimes(3);
|
|
308
|
+
|
|
309
|
+
expect(api.request.mock.calls[0]).toEqual([
|
|
310
|
+
'/repos/owner/repo/contents/content/posts/new-post.md',
|
|
311
|
+
{ params: { ref: 'master' } },
|
|
312
|
+
]);
|
|
313
|
+
|
|
314
|
+
expect(api.request.mock.calls[1]).toEqual([
|
|
315
|
+
'/repos/owner/repo/contents/content/posts/update-post.md',
|
|
316
|
+
{ params: { ref: 'master' } },
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
expect(api.request.mock.calls[2]).toEqual([
|
|
320
|
+
'/repos/owner/repo/contents',
|
|
321
|
+
{
|
|
322
|
+
method: 'POST',
|
|
323
|
+
body: JSON.stringify({
|
|
324
|
+
branch: 'master',
|
|
325
|
+
files: [
|
|
326
|
+
{
|
|
327
|
+
operation: 'create',
|
|
328
|
+
content: Base64.encode(entry.dataFiles[0].raw),
|
|
329
|
+
path: entry.dataFiles[0].path,
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
operation: 'update',
|
|
333
|
+
content: Base64.encode(entry.dataFiles[1].raw),
|
|
334
|
+
path: entry.dataFiles[1].path,
|
|
335
|
+
sha: entry.dataFiles[1].sha,
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
message: 'commitMessage',
|
|
339
|
+
}),
|
|
340
|
+
},
|
|
341
|
+
]);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('deleteFiles', () => {
|
|
346
|
+
it('should check if files exist and delete them', async () => {
|
|
347
|
+
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
|
348
|
+
|
|
349
|
+
const responses = {
|
|
350
|
+
'/repos/owner/repo/contents/content/posts/delete-post-1.md': () => {
|
|
351
|
+
return { sha: 'old-sha-1' };
|
|
352
|
+
},
|
|
353
|
+
'/repos/owner/repo/contents/content/posts/delete-post-2.md': () => {
|
|
354
|
+
return { sha: 'old-sha-2' };
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
'/repos/owner/repo/contents': () => ({
|
|
358
|
+
commit: { sha: 'new-sha' },
|
|
359
|
+
files: [
|
|
360
|
+
{
|
|
361
|
+
path: 'content/posts/delete-post-1.md',
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
path: 'content/posts/delete-post-2.md',
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
}),
|
|
368
|
+
};
|
|
369
|
+
mockAPI(api, responses);
|
|
370
|
+
|
|
371
|
+
const deleteFiles = ['content/posts/delete-post-1.md', 'content/posts/delete-post-2.md'];
|
|
372
|
+
|
|
373
|
+
await api.deleteFiles(deleteFiles, 'commitMessage');
|
|
374
|
+
|
|
375
|
+
expect(api.request).toHaveBeenCalledTimes(3);
|
|
376
|
+
|
|
377
|
+
expect(api.request.mock.calls[0]).toEqual([
|
|
378
|
+
'/repos/owner/repo/contents/content/posts/delete-post-1.md',
|
|
379
|
+
{ params: { ref: 'master' } },
|
|
380
|
+
]);
|
|
381
|
+
|
|
382
|
+
expect(api.request.mock.calls[1]).toEqual([
|
|
383
|
+
'/repos/owner/repo/contents/content/posts/delete-post-2.md',
|
|
384
|
+
{ params: { ref: 'master' } },
|
|
385
|
+
]);
|
|
386
|
+
|
|
387
|
+
expect(api.request.mock.calls[2]).toEqual([
|
|
388
|
+
'/repos/owner/repo/contents',
|
|
389
|
+
{
|
|
390
|
+
method: 'POST',
|
|
391
|
+
body: JSON.stringify({
|
|
392
|
+
branch: 'master',
|
|
393
|
+
files: [
|
|
394
|
+
{
|
|
395
|
+
operation: 'delete',
|
|
396
|
+
path: deleteFiles[0],
|
|
397
|
+
sha: 'old-sha-1',
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
operation: 'delete',
|
|
401
|
+
path: deleteFiles[1],
|
|
402
|
+
sha: 'old-sha-2',
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
message: 'commitMessage',
|
|
406
|
+
}),
|
|
407
|
+
},
|
|
408
|
+
]);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should reject delete for open authoring users', async () => {
|
|
412
|
+
const api = new API({
|
|
413
|
+
branch: 'master',
|
|
414
|
+
repo: 'contributor/repo',
|
|
415
|
+
originRepo: 'owner/repo',
|
|
416
|
+
useOpenAuthoring: true,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
await expect(api.deleteFiles(['content/posts/post.md'], 'delete post')).rejects.toMatchObject(
|
|
420
|
+
{
|
|
421
|
+
message: 'Cannot delete published entries as an Open Authoring user!',
|
|
422
|
+
status: 403,
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe('listFiles', () => {
|
|
429
|
+
it('should get files by depth', async () => {
|
|
430
|
+
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
|
431
|
+
|
|
432
|
+
const tree = [
|
|
433
|
+
{
|
|
434
|
+
path: 'posts/post.md',
|
|
435
|
+
sha: 'sha-post',
|
|
436
|
+
size: 10,
|
|
437
|
+
type: 'blob',
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
path: 'posts/dir1',
|
|
441
|
+
sha: 'sha-dir1',
|
|
442
|
+
size: 0,
|
|
443
|
+
type: 'tree',
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
path: 'posts/dir1/nested-post.md',
|
|
447
|
+
sha: 'sha-nested-1',
|
|
448
|
+
size: 20,
|
|
449
|
+
type: 'blob',
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
path: 'posts/dir1/dir2',
|
|
453
|
+
sha: 'sha-dir2',
|
|
454
|
+
size: 0,
|
|
455
|
+
type: 'tree',
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
path: 'posts/dir1/dir2/nested-post.md',
|
|
459
|
+
sha: 'sha-nested-2',
|
|
460
|
+
size: 30,
|
|
461
|
+
type: 'blob',
|
|
462
|
+
},
|
|
463
|
+
];
|
|
464
|
+
api.request = jest
|
|
465
|
+
.fn()
|
|
466
|
+
.mockResolvedValueOnce({ commit: { id: 'sha123' } })
|
|
467
|
+
.mockResolvedValueOnce({ tree });
|
|
468
|
+
|
|
469
|
+
await expect(api.listFiles('posts', { depth: 1 })).resolves.toEqual([
|
|
470
|
+
{
|
|
471
|
+
id: 'sha-post',
|
|
472
|
+
size: 10,
|
|
473
|
+
path: 'posts/post.md',
|
|
474
|
+
type: 'blob',
|
|
475
|
+
name: 'post.md',
|
|
476
|
+
},
|
|
477
|
+
]);
|
|
478
|
+
expect(api.request).toHaveBeenCalledTimes(2);
|
|
479
|
+
expect(api.request).toHaveBeenNthCalledWith(1, '/repos/owner/repo/branches/master');
|
|
480
|
+
expect(api.request).toHaveBeenNthCalledWith(2, '/repos/owner/repo/git/trees/sha123', {
|
|
481
|
+
params: { recursive: 1 },
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
jest.clearAllMocks();
|
|
485
|
+
api.request = jest
|
|
486
|
+
.fn()
|
|
487
|
+
.mockResolvedValueOnce({ commit: { id: 'sha123' } })
|
|
488
|
+
.mockResolvedValueOnce({ tree });
|
|
489
|
+
await expect(api.listFiles('posts', { depth: 2 })).resolves.toEqual([
|
|
490
|
+
{
|
|
491
|
+
id: 'sha-post',
|
|
492
|
+
size: 10,
|
|
493
|
+
path: 'posts/post.md',
|
|
494
|
+
type: 'blob',
|
|
495
|
+
name: 'post.md',
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
id: 'sha-nested-1',
|
|
499
|
+
size: 20,
|
|
500
|
+
path: 'posts/dir1/nested-post.md',
|
|
501
|
+
type: 'blob',
|
|
502
|
+
name: 'nested-post.md',
|
|
503
|
+
},
|
|
504
|
+
]);
|
|
505
|
+
expect(api.request).toHaveBeenCalledTimes(2);
|
|
506
|
+
expect(api.request).toHaveBeenNthCalledWith(1, '/repos/owner/repo/branches/master');
|
|
507
|
+
expect(api.request).toHaveBeenNthCalledWith(2, '/repos/owner/repo/git/trees/sha123', {
|
|
508
|
+
params: { recursive: 1 },
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
jest.clearAllMocks();
|
|
512
|
+
api.request = jest
|
|
513
|
+
.fn()
|
|
514
|
+
.mockResolvedValueOnce({ commit: { id: 'sha123' } })
|
|
515
|
+
.mockResolvedValueOnce({ tree });
|
|
516
|
+
await expect(api.listFiles('posts', { depth: 3 })).resolves.toEqual([
|
|
517
|
+
{
|
|
518
|
+
id: 'sha-post',
|
|
519
|
+
size: 10,
|
|
520
|
+
path: 'posts/post.md',
|
|
521
|
+
type: 'blob',
|
|
522
|
+
name: 'post.md',
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
id: 'sha-nested-1',
|
|
526
|
+
size: 20,
|
|
527
|
+
path: 'posts/dir1/nested-post.md',
|
|
528
|
+
type: 'blob',
|
|
529
|
+
name: 'nested-post.md',
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: 'sha-nested-2',
|
|
533
|
+
size: 30,
|
|
534
|
+
path: 'posts/dir1/dir2/nested-post.md',
|
|
535
|
+
type: 'blob',
|
|
536
|
+
name: 'nested-post.md',
|
|
537
|
+
},
|
|
538
|
+
]);
|
|
539
|
+
expect(api.request).toHaveBeenCalledTimes(2);
|
|
540
|
+
expect(api.request).toHaveBeenNthCalledWith(1, '/repos/owner/repo/branches/master');
|
|
541
|
+
expect(api.request).toHaveBeenNthCalledWith(2, '/repos/owner/repo/git/trees/sha123', {
|
|
542
|
+
params: { recursive: 1 },
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should exclude files outside the requested folder', async () => {
|
|
547
|
+
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
|
548
|
+
|
|
549
|
+
const tree = [
|
|
550
|
+
{
|
|
551
|
+
path: 'README.md',
|
|
552
|
+
sha: 'sha-readme',
|
|
553
|
+
size: 10,
|
|
554
|
+
type: 'blob',
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
path: 'posts/post.md',
|
|
558
|
+
sha: 'sha-post',
|
|
559
|
+
size: 20,
|
|
560
|
+
type: 'blob',
|
|
561
|
+
},
|
|
562
|
+
];
|
|
563
|
+
api.request = jest
|
|
564
|
+
.fn()
|
|
565
|
+
.mockResolvedValueOnce({ commit: { id: 'sha123' } })
|
|
566
|
+
.mockResolvedValueOnce({ tree });
|
|
567
|
+
|
|
568
|
+
await expect(api.listFiles('posts', { depth: 1 })).resolves.toEqual([
|
|
569
|
+
{
|
|
570
|
+
id: 'sha-post',
|
|
571
|
+
size: 20,
|
|
572
|
+
path: 'posts/post.md',
|
|
573
|
+
type: 'blob',
|
|
574
|
+
name: 'post.md',
|
|
575
|
+
},
|
|
576
|
+
]);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('should get files and folders', async () => {
|
|
580
|
+
const api = new API({ branch: 'master', repo: 'owner/repo' });
|
|
581
|
+
|
|
582
|
+
const tree = [
|
|
583
|
+
{
|
|
584
|
+
path: 'media/image.png',
|
|
585
|
+
sha: 'sha-image',
|
|
586
|
+
size: 50,
|
|
587
|
+
type: 'blob',
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
path: 'media/dir1',
|
|
591
|
+
sha: 'sha-media-dir1',
|
|
592
|
+
size: 0,
|
|
593
|
+
type: 'tree',
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
path: 'media/dir1/nested-image.png',
|
|
597
|
+
sha: 'sha-media-nested-1',
|
|
598
|
+
size: 60,
|
|
599
|
+
type: 'blob',
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
path: 'media/dir1/dir2',
|
|
603
|
+
sha: 'sha-media-dir2',
|
|
604
|
+
size: 0,
|
|
605
|
+
type: 'tree',
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
path: 'media/dir1/dir2/nested-image.png',
|
|
609
|
+
sha: 'sha-media-nested-2',
|
|
610
|
+
size: 70,
|
|
611
|
+
type: 'blob',
|
|
612
|
+
},
|
|
613
|
+
];
|
|
614
|
+
api.request = jest
|
|
615
|
+
.fn()
|
|
616
|
+
.mockResolvedValueOnce({ commit: { id: 'sha123' } })
|
|
617
|
+
.mockResolvedValueOnce({ tree });
|
|
618
|
+
|
|
619
|
+
await expect(api.listFiles('media', {}, true)).resolves.toEqual([
|
|
620
|
+
{
|
|
621
|
+
id: 'sha-image',
|
|
622
|
+
size: 50,
|
|
623
|
+
path: 'media/image.png',
|
|
624
|
+
type: 'blob',
|
|
625
|
+
name: 'image.png',
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
id: 'sha-media-dir1',
|
|
629
|
+
size: 0,
|
|
630
|
+
path: 'media/dir1',
|
|
631
|
+
type: 'tree',
|
|
632
|
+
name: 'dir1',
|
|
633
|
+
},
|
|
634
|
+
]);
|
|
635
|
+
expect(api.request).toHaveBeenCalledTimes(2);
|
|
636
|
+
expect(api.request).toHaveBeenNthCalledWith(1, '/repos/owner/repo/branches/master');
|
|
637
|
+
expect(api.request).toHaveBeenNthCalledWith(2, '/repos/owner/repo/git/trees/sha123', {
|
|
638
|
+
params: { recursive: 1 },
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('should create branch', async () => {
|
|
643
|
+
const api = new API({ branch: 'gh-pages', repo: 'owner/my-repo', token: 'token' });
|
|
644
|
+
api.request = jest.fn().mockResolvedValue({ name: 'cms/new-branch' });
|
|
645
|
+
|
|
646
|
+
await expect(api.createBranch('cms/new-branch', 'master')).resolves.toEqual({
|
|
647
|
+
name: 'cms/new-branch',
|
|
648
|
+
});
|
|
649
|
+
expect(api.request).toHaveBeenCalledWith('/repos/owner/my-repo/branches', {
|
|
650
|
+
method: 'POST',
|
|
651
|
+
body: JSON.stringify({
|
|
652
|
+
new_branch_name: 'cms/new-branch',
|
|
653
|
+
old_ref_name: 'master',
|
|
654
|
+
}),
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it('should create pull request with owner:branch head format', async () => {
|
|
659
|
+
const api = new API({ branch: 'gh-pages', repo: 'owner/my-repo', token: 'token' });
|
|
660
|
+
api.request = jest.fn().mockResolvedValue({ number: 1 });
|
|
661
|
+
|
|
662
|
+
await expect(
|
|
663
|
+
api.createPR('title', 'cms/new-branch', 'Check out the changes!'),
|
|
664
|
+
).resolves.toEqual({ number: 1 });
|
|
665
|
+
expect(api.request).toHaveBeenCalledWith('/repos/owner/my-repo/pulls', {
|
|
666
|
+
method: 'POST',
|
|
667
|
+
body: JSON.stringify({
|
|
668
|
+
title: 'title',
|
|
669
|
+
head: 'owner:cms/new-branch',
|
|
670
|
+
base: 'gh-pages',
|
|
671
|
+
body: 'Check out the changes!',
|
|
672
|
+
}),
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('should get pull requests', async () => {
|
|
677
|
+
const api = new API({ branch: 'gh-pages', repo: 'owner/my-repo', token: 'token' });
|
|
678
|
+
api.requestAllPages = jest.fn().mockResolvedValue([
|
|
679
|
+
{ number: 1, head: { label: 'head' } },
|
|
680
|
+
{ number: 2, head: { label: 'other-head' } },
|
|
681
|
+
]);
|
|
682
|
+
|
|
683
|
+
await expect(api.getPullRequests('open', 'head')).resolves.toEqual([
|
|
684
|
+
{ number: 1, head: { label: 'head' } },
|
|
685
|
+
]);
|
|
686
|
+
expect(api.requestAllPages).toHaveBeenCalledWith('/repos/owner/my-repo/pulls', {
|
|
687
|
+
params: { state: 'open', base: 'gh-pages', limit: 100 },
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it('should list unpublished branches (standard mode)', async () => {
|
|
692
|
+
const api = new API({
|
|
693
|
+
branch: 'gh-pages',
|
|
694
|
+
repo: 'owner/my-repo',
|
|
695
|
+
token: 'token',
|
|
696
|
+
cmsLabelPrefix: 'decap-cms/',
|
|
697
|
+
});
|
|
698
|
+
api.requestAllPages = jest.fn().mockResolvedValue([
|
|
699
|
+
{
|
|
700
|
+
head: { ref: 'cms/branch1', repo: { owner: { login: 'owner' } } },
|
|
701
|
+
labels: [{ name: 'decap-cms/draft' }],
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
head: { ref: 'other/branch', repo: { owner: { login: 'owner' } } },
|
|
705
|
+
labels: [{ name: 'decap-cms/draft' }],
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
head: { ref: 'cms/branch2', repo: { owner: { login: 'owner' } } },
|
|
709
|
+
labels: [{ name: 'decap-cms/pending_review' }],
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
head: { ref: 'cms/branch3', repo: { owner: { login: 'owner' } } },
|
|
713
|
+
labels: [{ name: 'other-label' }],
|
|
714
|
+
},
|
|
715
|
+
]);
|
|
716
|
+
|
|
717
|
+
await expect(api.listUnpublishedBranches()).resolves.toEqual(['cms/branch1', 'cms/branch2']);
|
|
718
|
+
expect(api.requestAllPages).toHaveBeenCalledWith('/repos/owner/my-repo/pulls', {
|
|
719
|
+
params: { state: 'open', base: 'gh-pages', limit: 100 },
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('should list unpublished branches (OA mode) from fork branches', async () => {
|
|
724
|
+
const api = new API({
|
|
725
|
+
branch: 'master',
|
|
726
|
+
repo: 'contributor/repo',
|
|
727
|
+
originRepo: 'owner/repo',
|
|
728
|
+
token: 'token',
|
|
729
|
+
useOpenAuthoring: true,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// Mock getOpenAuthoringBranches
|
|
733
|
+
api.getOpenAuthoringBranches = jest
|
|
734
|
+
.fn()
|
|
735
|
+
.mockResolvedValue([
|
|
736
|
+
{ name: 'cms/contributor/repo/posts/entry1' },
|
|
737
|
+
{ name: 'cms/contributor/repo/posts/entry2' },
|
|
738
|
+
]);
|
|
739
|
+
|
|
740
|
+
// Mock filterOpenAuthoringBranches to allow all
|
|
741
|
+
api.filterOpenAuthoringBranches = jest
|
|
742
|
+
.fn()
|
|
743
|
+
.mockImplementation(branch => Promise.resolve({ branch, filter: true }));
|
|
744
|
+
|
|
745
|
+
const result = await api.listUnpublishedBranches();
|
|
746
|
+
|
|
747
|
+
expect(result).toEqual([
|
|
748
|
+
'cms/contributor/repo/posts/entry1',
|
|
749
|
+
'cms/contributor/repo/posts/entry2',
|
|
750
|
+
]);
|
|
751
|
+
expect(api.getOpenAuthoringBranches).toHaveBeenCalled();
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it('should update pull request labels', async () => {
|
|
755
|
+
const api = new API({ branch: 'gh-pages', repo: 'owner/my-repo', token: 'token' });
|
|
756
|
+
api.request = jest.fn().mockResolvedValue([{ id: 1, name: 'label' }]);
|
|
757
|
+
|
|
758
|
+
await expect(api.updatePullRequestLabels(1, [1])).resolves.toEqual([
|
|
759
|
+
{ id: 1, name: 'label' },
|
|
760
|
+
]);
|
|
761
|
+
expect(api.request).toHaveBeenCalledWith('/repos/owner/my-repo/issues/1/labels', {
|
|
762
|
+
method: 'PUT',
|
|
763
|
+
body: JSON.stringify({ labels: [1] }),
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('should get labels', async () => {
|
|
768
|
+
const api = new API({ branch: 'gh-pages', repo: 'owner/my-repo', token: 'token' });
|
|
769
|
+
api.requestAllPages = jest.fn().mockResolvedValue([
|
|
770
|
+
{ id: 1, name: 'label1' },
|
|
771
|
+
{ id: 2, name: 'label2' },
|
|
772
|
+
]);
|
|
773
|
+
|
|
774
|
+
await expect(api.getLabels()).resolves.toEqual([
|
|
775
|
+
{ id: 1, name: 'label1' },
|
|
776
|
+
{ id: 2, name: 'label2' },
|
|
777
|
+
]);
|
|
778
|
+
expect(api.requestAllPages).toHaveBeenCalledWith('/repos/owner/my-repo/labels', {
|
|
779
|
+
params: { limit: 100 },
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it('should create label', async () => {
|
|
784
|
+
const api = new API({ branch: 'gh-pages', repo: 'owner/my-repo', token: 'token' });
|
|
785
|
+
api.request = jest.fn().mockResolvedValue({ id: 1, name: 'new-label', color: '0052cc' });
|
|
786
|
+
|
|
787
|
+
await expect(api.createLabel('new-label', '0052cc')).resolves.toEqual({
|
|
788
|
+
id: 1,
|
|
789
|
+
name: 'new-label',
|
|
790
|
+
color: '0052cc',
|
|
791
|
+
});
|
|
792
|
+
expect(api.request).toHaveBeenCalledWith('/repos/owner/my-repo/labels', {
|
|
793
|
+
method: 'POST',
|
|
794
|
+
body: JSON.stringify({ name: 'new-label', color: '0052cc' }),
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('should get or create label when label exists', async () => {
|
|
799
|
+
const api = new API({ branch: 'gh-pages', repo: 'owner/my-repo', token: 'token' });
|
|
800
|
+
const existingLabel = { id: 1, name: 'existing-label', color: '0052cc' };
|
|
801
|
+
api.getLabels = jest.fn().mockResolvedValue([existingLabel]);
|
|
802
|
+
api.createLabel = jest.fn();
|
|
803
|
+
|
|
804
|
+
await expect(api.getOrCreateLabel('existing-label')).resolves.toEqual(existingLabel);
|
|
805
|
+
expect(api.getLabels).toHaveBeenCalledTimes(1);
|
|
806
|
+
expect(api.createLabel).not.toHaveBeenCalled();
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('should get or create label when label does not exist', async () => {
|
|
810
|
+
const api = new API({ branch: 'gh-pages', repo: 'owner/my-repo', token: 'token' });
|
|
811
|
+
const newLabel = { id: 2, name: 'new-label', color: '0052cc' };
|
|
812
|
+
api.getLabels = jest.fn().mockResolvedValue([{ id: 1, name: 'other-label' }]);
|
|
813
|
+
api.createLabel = jest.fn().mockResolvedValue(newLabel);
|
|
814
|
+
|
|
815
|
+
await expect(api.getOrCreateLabel('new-label')).resolves.toEqual(newLabel);
|
|
816
|
+
expect(api.getLabels).toHaveBeenCalledTimes(1);
|
|
817
|
+
expect(api.createLabel).toHaveBeenCalledTimes(1);
|
|
818
|
+
expect(api.createLabel).toHaveBeenCalledWith('new-label');
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('should set pull request status', async () => {
|
|
822
|
+
const api = new API({
|
|
823
|
+
branch: 'gh-pages',
|
|
824
|
+
repo: 'owner/my-repo',
|
|
825
|
+
token: 'token',
|
|
826
|
+
cmsLabelPrefix: 'decap-cms/',
|
|
827
|
+
});
|
|
828
|
+
const pullRequest = {
|
|
829
|
+
number: 1,
|
|
830
|
+
labels: [
|
|
831
|
+
{ id: 1, name: 'decap-cms/draft' },
|
|
832
|
+
{ id: 2, name: 'other-label' },
|
|
833
|
+
],
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
const newLabel = { id: 3, name: 'decap-cms/pending_review' };
|
|
837
|
+
api.getOrCreateLabel = jest.fn().mockResolvedValue(newLabel);
|
|
838
|
+
api.updatePullRequestLabels = jest
|
|
839
|
+
.fn()
|
|
840
|
+
.mockResolvedValue([newLabel, { id: 2, name: 'other-label' }]);
|
|
841
|
+
|
|
842
|
+
await api.setPullRequestStatus(pullRequest, 'pending_review');
|
|
843
|
+
|
|
844
|
+
expect(api.getOrCreateLabel).toHaveBeenCalledTimes(1);
|
|
845
|
+
expect(api.getOrCreateLabel).toHaveBeenCalledWith('decap-cms/pending_review');
|
|
846
|
+
|
|
847
|
+
expect(api.updatePullRequestLabels).toHaveBeenCalledTimes(1);
|
|
848
|
+
expect(api.updatePullRequestLabels).toHaveBeenCalledWith(1, [2, 3]);
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
describe('retrieveUnpublishedEntryData', () => {
|
|
853
|
+
it('should retrieve unpublished entry data', async () => {
|
|
854
|
+
const api = new API({
|
|
855
|
+
branch: 'master',
|
|
856
|
+
repo: 'owner/repo',
|
|
857
|
+
token: 'token',
|
|
858
|
+
cmsLabelPrefix: 'decap-cms/',
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
const pullRequest = {
|
|
862
|
+
number: 1,
|
|
863
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
864
|
+
user: { login: 'testuser' },
|
|
865
|
+
labels: [{ id: 1, name: 'decap-cms/pending_review' }],
|
|
866
|
+
};
|
|
867
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(pullRequest);
|
|
868
|
+
|
|
869
|
+
const compareResult = {
|
|
870
|
+
files: [
|
|
871
|
+
{ filename: 'content/posts/test.md', status: 'added', sha: 'sha1' },
|
|
872
|
+
{ filename: 'static/img/test.jpg', status: 'modified', sha: 'sha2' },
|
|
873
|
+
],
|
|
874
|
+
commits: [],
|
|
875
|
+
total_commits: 1,
|
|
876
|
+
};
|
|
877
|
+
api.getDifferences = jest.fn().mockResolvedValue(compareResult);
|
|
878
|
+
|
|
879
|
+
const result = await api.retrieveUnpublishedEntryData('posts/test');
|
|
880
|
+
|
|
881
|
+
expect(api.getBranchPullRequest).toHaveBeenCalledWith('cms/posts/test');
|
|
882
|
+
expect(result).toEqual({
|
|
883
|
+
collection: 'posts',
|
|
884
|
+
slug: 'test',
|
|
885
|
+
status: 'pending_review',
|
|
886
|
+
diffs: [
|
|
887
|
+
{ path: 'content/posts/test.md', newFile: true, id: 'sha1' },
|
|
888
|
+
{ path: 'static/img/test.jpg', newFile: false, id: 'sha2' },
|
|
889
|
+
],
|
|
890
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
891
|
+
pullRequestAuthor: 'testuser',
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('should fall back to getPullRequestFiles when getDifferences fails', async () => {
|
|
896
|
+
const api = new API({
|
|
897
|
+
branch: 'master',
|
|
898
|
+
repo: 'owner/repo',
|
|
899
|
+
token: 'token',
|
|
900
|
+
cmsLabelPrefix: 'decap-cms/',
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
const pullRequest = {
|
|
904
|
+
number: 1,
|
|
905
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
906
|
+
user: { login: 'testuser' },
|
|
907
|
+
labels: [{ id: 1, name: 'decap-cms/pending_review' }],
|
|
908
|
+
};
|
|
909
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(pullRequest);
|
|
910
|
+
api.getDifferences = jest.fn().mockRejectedValue(new Error('compare failed'));
|
|
911
|
+
|
|
912
|
+
const files = [{ filename: 'content/posts/test.md', status: 'added' }];
|
|
913
|
+
api.getPullRequestFiles = jest.fn().mockResolvedValue(files);
|
|
914
|
+
|
|
915
|
+
const result = await api.retrieveUnpublishedEntryData('posts/test');
|
|
916
|
+
|
|
917
|
+
expect(result.diffs).toEqual([{ path: 'content/posts/test.md', newFile: true, id: '' }]);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it('should default to initialWorkflowStatus when no CMS label found', async () => {
|
|
921
|
+
const api = new API({
|
|
922
|
+
branch: 'master',
|
|
923
|
+
repo: 'owner/repo',
|
|
924
|
+
token: 'token',
|
|
925
|
+
cmsLabelPrefix: 'decap-cms/',
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
const pullRequest = {
|
|
929
|
+
number: 1,
|
|
930
|
+
updated_at: '2024-01-01T00:00:00Z',
|
|
931
|
+
user: { login: 'testuser' },
|
|
932
|
+
labels: [{ id: 2, name: 'other-label' }],
|
|
933
|
+
};
|
|
934
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(pullRequest);
|
|
935
|
+
api.getDifferences = jest
|
|
936
|
+
.fn()
|
|
937
|
+
.mockResolvedValue({ files: [], commits: [], total_commits: 0 });
|
|
938
|
+
|
|
939
|
+
const result = await api.retrieveUnpublishedEntryData('posts/test');
|
|
940
|
+
|
|
941
|
+
expect(result.status).toEqual('draft');
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
describe('updateUnpublishedEntryStatus', () => {
|
|
946
|
+
it('should update unpublished entry status (standard mode)', async () => {
|
|
947
|
+
const api = new API({ branch: 'master', repo: 'owner/repo', token: 'token' });
|
|
948
|
+
|
|
949
|
+
const pullRequest = {
|
|
950
|
+
number: 1,
|
|
951
|
+
labels: [{ id: 1, name: 'decap-cms/draft' }],
|
|
952
|
+
};
|
|
953
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(pullRequest);
|
|
954
|
+
api.setPullRequestStatus = jest.fn().mockResolvedValue();
|
|
955
|
+
|
|
956
|
+
await api.updateUnpublishedEntryStatus('posts', 'test', 'pending_review');
|
|
957
|
+
|
|
958
|
+
expect(api.getBranchPullRequest).toHaveBeenCalledWith('cms/posts/test');
|
|
959
|
+
expect(api.setPullRequestStatus).toHaveBeenCalledWith(pullRequest, 'pending_review');
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it('should reject pending_publish for open authoring', async () => {
|
|
963
|
+
const api = new API({
|
|
964
|
+
branch: 'master',
|
|
965
|
+
repo: 'contributor/repo',
|
|
966
|
+
originRepo: 'owner/repo',
|
|
967
|
+
useOpenAuthoring: true,
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
const pullRequest = { number: 1, state: 'open', labels: [] };
|
|
971
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(pullRequest);
|
|
972
|
+
|
|
973
|
+
await expect(
|
|
974
|
+
api.updateUnpublishedEntryStatus('posts', 'test', 'pending_publish'),
|
|
975
|
+
).rejects.toThrow('Open Authoring entries may not be set to the status "pending_publish".');
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it('should close PR when OA entry moves to draft', async () => {
|
|
979
|
+
const api = new API({
|
|
980
|
+
branch: 'master',
|
|
981
|
+
repo: 'contributor/repo',
|
|
982
|
+
originRepo: 'owner/repo',
|
|
983
|
+
useOpenAuthoring: true,
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
const pullRequest = { number: 5, state: 'open', labels: [] };
|
|
987
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(pullRequest);
|
|
988
|
+
api.closePR = jest.fn().mockResolvedValue({});
|
|
989
|
+
|
|
990
|
+
await api.updateUnpublishedEntryStatus('posts', 'test', 'draft');
|
|
991
|
+
|
|
992
|
+
expect(api.closePR).toHaveBeenCalledWith(5);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
it('should re-open PR when OA entry moves to pending_review', async () => {
|
|
996
|
+
const api = new API({
|
|
997
|
+
branch: 'master',
|
|
998
|
+
repo: 'contributor/repo',
|
|
999
|
+
originRepo: 'owner/repo',
|
|
1000
|
+
useOpenAuthoring: true,
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
const pullRequest = { number: 5, state: 'closed', labels: [] };
|
|
1004
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(pullRequest);
|
|
1005
|
+
api.updatePR = jest.fn().mockResolvedValue({});
|
|
1006
|
+
|
|
1007
|
+
await api.updateUnpublishedEntryStatus('posts', 'test', 'pending_review');
|
|
1008
|
+
|
|
1009
|
+
expect(api.updatePR).toHaveBeenCalledWith(5, 'open');
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it('should create PR from mock PR when OA entry moves to pending_review', async () => {
|
|
1013
|
+
const api = new API({
|
|
1014
|
+
branch: 'master',
|
|
1015
|
+
repo: 'contributor/repo',
|
|
1016
|
+
originRepo: 'owner/repo',
|
|
1017
|
+
useOpenAuthoring: true,
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
const mockPR = { number: MOCK_PULL_REQUEST, state: 'open', labels: [] };
|
|
1021
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(mockPR);
|
|
1022
|
+
api.getDifferences = jest.fn().mockResolvedValue({
|
|
1023
|
+
commits: [{ commit: { message: 'Add new post' } }],
|
|
1024
|
+
files: [],
|
|
1025
|
+
total_commits: 1,
|
|
1026
|
+
});
|
|
1027
|
+
api.createPR = jest.fn().mockResolvedValue({ number: 10 });
|
|
1028
|
+
|
|
1029
|
+
await api.updateUnpublishedEntryStatus('posts', 'test', 'pending_review');
|
|
1030
|
+
|
|
1031
|
+
expect(api.getDifferences).toHaveBeenCalled();
|
|
1032
|
+
expect(api.createPR).toHaveBeenCalledWith('Add new post', expect.stringContaining('cms/'));
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
describe('deleteUnpublishedEntry', () => {
|
|
1037
|
+
it('should delete unpublished entry by closing PR and deleting branch', async () => {
|
|
1038
|
+
const api = new API({ branch: 'master', repo: 'owner/repo', token: 'token' });
|
|
1039
|
+
|
|
1040
|
+
const pullRequest = { number: 1 };
|
|
1041
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(pullRequest);
|
|
1042
|
+
api.closePR = jest.fn().mockResolvedValue();
|
|
1043
|
+
api.deleteBranch = jest.fn().mockResolvedValue();
|
|
1044
|
+
|
|
1045
|
+
await api.deleteUnpublishedEntry('posts', 'test');
|
|
1046
|
+
|
|
1047
|
+
expect(api.getBranchPullRequest).toHaveBeenCalledWith('cms/posts/test');
|
|
1048
|
+
expect(api.closePR).toHaveBeenCalledWith(1);
|
|
1049
|
+
expect(api.deleteBranch).toHaveBeenCalledWith('cms/posts/test');
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
it('should delete branch even if PR does not exist', async () => {
|
|
1053
|
+
const api = new API({ branch: 'master', repo: 'owner/repo', token: 'token' });
|
|
1054
|
+
|
|
1055
|
+
api.getBranchPullRequest = jest
|
|
1056
|
+
.fn()
|
|
1057
|
+
.mockRejectedValue(new APIError('PR not found', 404, 'Forgejo'));
|
|
1058
|
+
api.closePR = jest.fn();
|
|
1059
|
+
api.deleteBranch = jest.fn().mockResolvedValue();
|
|
1060
|
+
|
|
1061
|
+
await api.deleteUnpublishedEntry('posts', 'test');
|
|
1062
|
+
|
|
1063
|
+
expect(api.closePR).not.toHaveBeenCalled();
|
|
1064
|
+
expect(api.deleteBranch).toHaveBeenCalledWith('cms/posts/test');
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
describe('publishUnpublishedEntry', () => {
|
|
1069
|
+
it('should publish unpublished entry by merging PR and deleting branch', async () => {
|
|
1070
|
+
const api = new API({ branch: 'master', repo: 'owner/repo', token: 'token' });
|
|
1071
|
+
|
|
1072
|
+
const pullRequest = { number: 1 };
|
|
1073
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(pullRequest);
|
|
1074
|
+
api.mergePR = jest.fn().mockResolvedValue();
|
|
1075
|
+
api.deleteBranch = jest.fn().mockResolvedValue();
|
|
1076
|
+
|
|
1077
|
+
await api.publishUnpublishedEntry('posts', 'test');
|
|
1078
|
+
|
|
1079
|
+
expect(api.getBranchPullRequest).toHaveBeenCalledWith('cms/posts/test');
|
|
1080
|
+
expect(api.mergePR).toHaveBeenCalledWith(pullRequest);
|
|
1081
|
+
expect(api.deleteBranch).toHaveBeenCalledWith('cms/posts/test');
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
describe('getBranchPullRequest', () => {
|
|
1086
|
+
it('should get open pull request with CMS labels for branch (standard mode)', async () => {
|
|
1087
|
+
const api = new API({
|
|
1088
|
+
branch: 'master',
|
|
1089
|
+
repo: 'owner/my-repo',
|
|
1090
|
+
token: 'token',
|
|
1091
|
+
cmsLabelPrefix: 'decap-cms/',
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
const openPR = {
|
|
1095
|
+
number: 1,
|
|
1096
|
+
head: { ref: 'cms/posts/test' },
|
|
1097
|
+
state: 'open',
|
|
1098
|
+
labels: [{ name: 'decap-cms/draft' }],
|
|
1099
|
+
};
|
|
1100
|
+
api.getPullRequests = jest.fn().mockResolvedValue([openPR]);
|
|
1101
|
+
|
|
1102
|
+
const result = await api.getBranchPullRequest('cms/posts/test');
|
|
1103
|
+
|
|
1104
|
+
expect(result).toEqual(openPR);
|
|
1105
|
+
expect(api.getPullRequests).toHaveBeenCalledWith('open', 'owner:cms/posts/test');
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
it('should throw EditorialWorkflowError if no CMS-labeled PR found (standard mode)', async () => {
|
|
1109
|
+
const api = new API({
|
|
1110
|
+
branch: 'master',
|
|
1111
|
+
repo: 'owner/my-repo',
|
|
1112
|
+
token: 'token',
|
|
1113
|
+
cmsLabelPrefix: 'decap-cms/',
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
// PR exists but has no CMS label
|
|
1117
|
+
const pr = {
|
|
1118
|
+
number: 1,
|
|
1119
|
+
head: { ref: 'cms/posts/test' },
|
|
1120
|
+
state: 'open',
|
|
1121
|
+
labels: [{ name: 'other-label' }],
|
|
1122
|
+
};
|
|
1123
|
+
api.getPullRequests = jest.fn().mockResolvedValue([pr]);
|
|
1124
|
+
|
|
1125
|
+
await expect(api.getBranchPullRequest('cms/posts/test')).rejects.toThrow(
|
|
1126
|
+
'content is not under editorial workflow',
|
|
1127
|
+
);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
it('should delegate to getOpenAuthoringPullRequest for OA mode', async () => {
|
|
1131
|
+
const api = new API({
|
|
1132
|
+
branch: 'master',
|
|
1133
|
+
repo: 'contributor/repo',
|
|
1134
|
+
originRepo: 'owner/repo',
|
|
1135
|
+
token: 'token',
|
|
1136
|
+
useOpenAuthoring: true,
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
const mockPR = {
|
|
1140
|
+
number: MOCK_PULL_REQUEST,
|
|
1141
|
+
state: 'open',
|
|
1142
|
+
labels: [],
|
|
1143
|
+
head: { ref: 'cms/test', sha: 'sha123' },
|
|
1144
|
+
};
|
|
1145
|
+
api.getPullRequests = jest.fn().mockResolvedValue([]);
|
|
1146
|
+
api.getOpenAuthoringPullRequest = jest.fn().mockResolvedValue({
|
|
1147
|
+
pullRequest: mockPR,
|
|
1148
|
+
branch: { commit: { id: 'sha123' } },
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
const result = await api.getBranchPullRequest('cms/test');
|
|
1152
|
+
|
|
1153
|
+
expect(result).toEqual(mockPR);
|
|
1154
|
+
expect(api.getPullRequests).toHaveBeenCalledWith('all', 'contributor:cms/test');
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
describe('mergePR', () => {
|
|
1159
|
+
it('should merge pull request', async () => {
|
|
1160
|
+
const api = new API({ branch: 'master', repo: 'owner/my-repo', token: 'token' });
|
|
1161
|
+
api.request = jest.fn().mockResolvedValue({});
|
|
1162
|
+
|
|
1163
|
+
const pullRequest = { number: 1 };
|
|
1164
|
+
await api.mergePR(pullRequest);
|
|
1165
|
+
|
|
1166
|
+
expect(api.request).toHaveBeenCalledWith('/repos/owner/my-repo/pulls/1/merge', {
|
|
1167
|
+
method: 'POST',
|
|
1168
|
+
body: JSON.stringify({
|
|
1169
|
+
Do: 'merge',
|
|
1170
|
+
MergeMessageField: 'Automatically generated. Merged on Decap CMS.',
|
|
1171
|
+
}),
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
describe('closePR', () => {
|
|
1177
|
+
it('should close pull request', async () => {
|
|
1178
|
+
const api = new API({ branch: 'master', repo: 'owner/my-repo', token: 'token' });
|
|
1179
|
+
api.updatePR = jest.fn().mockResolvedValue({ number: 1, state: 'closed' });
|
|
1180
|
+
|
|
1181
|
+
const result = await api.closePR(1);
|
|
1182
|
+
|
|
1183
|
+
expect(api.updatePR).toHaveBeenCalledWith(1, 'closed');
|
|
1184
|
+
expect(result).toEqual({ number: 1, state: 'closed' });
|
|
1185
|
+
});
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
describe('deleteBranch', () => {
|
|
1189
|
+
it('should delete branch', async () => {
|
|
1190
|
+
const api = new API({ branch: 'master', repo: 'owner/my-repo', token: 'token' });
|
|
1191
|
+
api.request = jest.fn().mockResolvedValue({});
|
|
1192
|
+
|
|
1193
|
+
await api.deleteBranch('cms/posts/test');
|
|
1194
|
+
|
|
1195
|
+
expect(api.request).toHaveBeenCalledWith('/repos/owner/my-repo/branches/cms%2Fposts%2Ftest', {
|
|
1196
|
+
method: 'DELETE',
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
describe('forkExists', () => {
|
|
1202
|
+
it('should return true when fork exists with matching parent', async () => {
|
|
1203
|
+
const api = new API({ branch: 'master', repo: 'user/repo', originRepo: 'owner/repo' });
|
|
1204
|
+
const mockRepo = {
|
|
1205
|
+
fork: true,
|
|
1206
|
+
parent: { full_name: 'owner/repo' },
|
|
1207
|
+
};
|
|
1208
|
+
api.request = jest.fn().mockResolvedValue(mockRepo);
|
|
1209
|
+
|
|
1210
|
+
const result = await api.forkExists();
|
|
1211
|
+
|
|
1212
|
+
expect(result).toBe(true);
|
|
1213
|
+
expect(api.request).toHaveBeenCalledWith('/repos/user/repo');
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
it('should return false when repo is not a fork', async () => {
|
|
1217
|
+
const api = new API({ branch: 'master', repo: 'user/repo', originRepo: 'owner/repo' });
|
|
1218
|
+
api.request = jest.fn().mockResolvedValue({ fork: false });
|
|
1219
|
+
|
|
1220
|
+
const result = await api.forkExists();
|
|
1221
|
+
|
|
1222
|
+
expect(result).toBe(false);
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
it('should return false when parent does not match origin repo', async () => {
|
|
1226
|
+
const api = new API({ branch: 'master', repo: 'user/repo', originRepo: 'owner/repo' });
|
|
1227
|
+
api.request = jest.fn().mockResolvedValue({
|
|
1228
|
+
fork: true,
|
|
1229
|
+
parent: { full_name: 'other/repo' },
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
const result = await api.forkExists();
|
|
1233
|
+
|
|
1234
|
+
expect(result).toBe(false);
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
it('should handle case-insensitive parent comparison', async () => {
|
|
1238
|
+
const api = new API({ branch: 'master', repo: 'user/repo', originRepo: 'owner/repo' });
|
|
1239
|
+
api.request = jest.fn().mockResolvedValue({
|
|
1240
|
+
fork: true,
|
|
1241
|
+
parent: { full_name: 'OWNER/REPO' },
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const result = await api.forkExists();
|
|
1245
|
+
|
|
1246
|
+
expect(result).toBe(true);
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
describe('createFork', () => {
|
|
1251
|
+
it('should create fork', async () => {
|
|
1252
|
+
const api = new API({ branch: 'master', repo: 'user/repo', originRepo: 'owner/repo' });
|
|
1253
|
+
api.request = jest.fn().mockResolvedValue({ full_name: 'user/repo' });
|
|
1254
|
+
|
|
1255
|
+
const result = await api.createFork();
|
|
1256
|
+
|
|
1257
|
+
expect(result).toEqual({ full_name: 'user/repo' });
|
|
1258
|
+
expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/forks', {
|
|
1259
|
+
method: 'POST',
|
|
1260
|
+
});
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
describe('getOpenAuthoringPullRequest', () => {
|
|
1265
|
+
it('should return mock PR with initial status label when no PR exists', async () => {
|
|
1266
|
+
const api = new API({ branch: 'master', repo: 'user/repo' });
|
|
1267
|
+
api.getBranch = jest.fn().mockResolvedValue({
|
|
1268
|
+
commit: { id: 'sha123' },
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
const result = await api.getOpenAuthoringPullRequest('cms/test', []);
|
|
1272
|
+
|
|
1273
|
+
expect(result.pullRequest.number).toBe(-1);
|
|
1274
|
+
expect(result.pullRequest.head.sha).toBe('sha123');
|
|
1275
|
+
// Default cmsLabelPrefix is '' which maps to 'decap-cms/' via getLabelPrefix
|
|
1276
|
+
expect(result.pullRequest.labels).toEqual(
|
|
1277
|
+
expect.arrayContaining([expect.objectContaining({ name: 'decap-cms/draft' })]),
|
|
1278
|
+
);
|
|
1279
|
+
expect(result.branch.commit.id).toBe('sha123');
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
it('should add synthetic pending_review label for open PR', async () => {
|
|
1283
|
+
const api = new API({
|
|
1284
|
+
branch: 'master',
|
|
1285
|
+
repo: 'user/repo',
|
|
1286
|
+
cmsLabelPrefix: 'decap-cms/',
|
|
1287
|
+
});
|
|
1288
|
+
const pullRequest = {
|
|
1289
|
+
number: 1,
|
|
1290
|
+
head: { sha: 'sha123' },
|
|
1291
|
+
state: 'open',
|
|
1292
|
+
labels: [
|
|
1293
|
+
{ id: 1, name: 'decap-cms/draft' },
|
|
1294
|
+
{ id: 2, name: 'bug' },
|
|
1295
|
+
],
|
|
1296
|
+
};
|
|
1297
|
+
api.getBranch = jest.fn().mockResolvedValue({
|
|
1298
|
+
commit: { id: 'sha123' },
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
const result = await api.getOpenAuthoringPullRequest('cms/test', [pullRequest]);
|
|
1302
|
+
|
|
1303
|
+
expect(result.pullRequest.number).toBe(1);
|
|
1304
|
+
// CMS labels filtered out, synthetic pending_review added, plus non-CMS labels kept
|
|
1305
|
+
expect(result.pullRequest.labels).toEqual([
|
|
1306
|
+
{ id: 2, name: 'bug' },
|
|
1307
|
+
{ name: 'decap-cms/pending_review' },
|
|
1308
|
+
]);
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
it('should add synthetic draft label for closed PR', async () => {
|
|
1312
|
+
const api = new API({
|
|
1313
|
+
branch: 'master',
|
|
1314
|
+
repo: 'user/repo',
|
|
1315
|
+
cmsLabelPrefix: 'decap-cms/',
|
|
1316
|
+
});
|
|
1317
|
+
const pullRequest = {
|
|
1318
|
+
number: 1,
|
|
1319
|
+
head: { sha: 'sha123' },
|
|
1320
|
+
state: 'closed',
|
|
1321
|
+
labels: [],
|
|
1322
|
+
};
|
|
1323
|
+
api.getBranch = jest.fn().mockResolvedValue({
|
|
1324
|
+
commit: { id: 'sha123' },
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
const result = await api.getOpenAuthoringPullRequest('cms/test', [pullRequest]);
|
|
1328
|
+
|
|
1329
|
+
expect(result.pullRequest.labels).toEqual([{ name: 'decap-cms/draft' }]);
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
describe('getDifferences', () => {
|
|
1334
|
+
it('should call compare endpoint', async () => {
|
|
1335
|
+
const api = new API({ branch: 'master', repo: 'owner/repo', token: 'token' });
|
|
1336
|
+
const compareResult = { files: [], commits: [], total_commits: 0 };
|
|
1337
|
+
api.request = jest.fn().mockResolvedValue(compareResult);
|
|
1338
|
+
|
|
1339
|
+
const result = await api.getDifferences('master', 'owner:cms/posts/test');
|
|
1340
|
+
|
|
1341
|
+
expect(result).toEqual(compareResult);
|
|
1342
|
+
expect(api.request).toHaveBeenCalledWith(
|
|
1343
|
+
'/repos/owner/repo/compare/master...owner%3Acms%2Fposts%2Ftest',
|
|
1344
|
+
);
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
it('should retry with origin repo for OA on failure', async () => {
|
|
1348
|
+
const api = new API({
|
|
1349
|
+
branch: 'master',
|
|
1350
|
+
repo: 'contributor/repo',
|
|
1351
|
+
originRepo: 'owner/repo',
|
|
1352
|
+
useOpenAuthoring: true,
|
|
1353
|
+
token: 'token',
|
|
1354
|
+
});
|
|
1355
|
+
const compareResult = { files: [], commits: [], total_commits: 0 };
|
|
1356
|
+
api.request = jest
|
|
1357
|
+
.fn()
|
|
1358
|
+
.mockRejectedValueOnce(new Error('not found'))
|
|
1359
|
+
.mockResolvedValueOnce(compareResult);
|
|
1360
|
+
|
|
1361
|
+
const result = await api.getDifferences('master', 'contributor:cms/test');
|
|
1362
|
+
|
|
1363
|
+
expect(result).toEqual(compareResult);
|
|
1364
|
+
expect(api.request).toHaveBeenCalledTimes(2);
|
|
1365
|
+
// First call to fork repo
|
|
1366
|
+
expect(api.request.mock.calls[0][0]).toContain('/repos/contributor/repo/compare/');
|
|
1367
|
+
// Second call to origin repo
|
|
1368
|
+
expect(api.request.mock.calls[1][0]).toContain('/repos/owner/repo/compare/');
|
|
1369
|
+
});
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
describe('filterOpenAuthoringBranches', () => {
|
|
1373
|
+
it('should filter out merged PRs and delete their branches', async () => {
|
|
1374
|
+
const api = new API({
|
|
1375
|
+
branch: 'master',
|
|
1376
|
+
repo: 'contributor/repo',
|
|
1377
|
+
originRepo: 'owner/repo',
|
|
1378
|
+
useOpenAuthoring: true,
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
const mergedPR = {
|
|
1382
|
+
number: 1,
|
|
1383
|
+
state: 'closed',
|
|
1384
|
+
merged_at: '2024-01-01T00:00:00Z',
|
|
1385
|
+
labels: [],
|
|
1386
|
+
};
|
|
1387
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(mergedPR);
|
|
1388
|
+
api.deleteBranch = jest.fn().mockResolvedValue();
|
|
1389
|
+
|
|
1390
|
+
const result = await api.filterOpenAuthoringBranches('cms/contributor/repo/posts/entry');
|
|
1391
|
+
|
|
1392
|
+
expect(result).toEqual({ branch: 'cms/contributor/repo/posts/entry', filter: false });
|
|
1393
|
+
expect(api.deleteBranch).toHaveBeenCalledWith('cms/contributor/repo/posts/entry');
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
it('should keep branches with unmerged PRs', async () => {
|
|
1397
|
+
const api = new API({
|
|
1398
|
+
branch: 'master',
|
|
1399
|
+
repo: 'contributor/repo',
|
|
1400
|
+
originRepo: 'owner/repo',
|
|
1401
|
+
useOpenAuthoring: true,
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
const openPR = { number: 1, state: 'open', merged_at: null, labels: [] };
|
|
1405
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(openPR);
|
|
1406
|
+
|
|
1407
|
+
const result = await api.filterOpenAuthoringBranches('cms/contributor/repo/posts/entry');
|
|
1408
|
+
|
|
1409
|
+
expect(result).toEqual({ branch: 'cms/contributor/repo/posts/entry', filter: true });
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
it('should keep branches with mock PRs (no real PR)', async () => {
|
|
1413
|
+
const api = new API({
|
|
1414
|
+
branch: 'master',
|
|
1415
|
+
repo: 'contributor/repo',
|
|
1416
|
+
originRepo: 'owner/repo',
|
|
1417
|
+
useOpenAuthoring: true,
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
const mockPR = { number: MOCK_PULL_REQUEST, state: 'open', merged_at: null, labels: [] };
|
|
1421
|
+
api.getBranchPullRequest = jest.fn().mockResolvedValue(mockPR);
|
|
1422
|
+
|
|
1423
|
+
const result = await api.filterOpenAuthoringBranches('cms/contributor/repo/posts/entry');
|
|
1424
|
+
|
|
1425
|
+
expect(result).toEqual({ branch: 'cms/contributor/repo/posts/entry', filter: true });
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
it('should filter out branches on 404 errors', async () => {
|
|
1429
|
+
const api = new API({
|
|
1430
|
+
branch: 'master',
|
|
1431
|
+
repo: 'contributor/repo',
|
|
1432
|
+
originRepo: 'owner/repo',
|
|
1433
|
+
useOpenAuthoring: true,
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
const notFoundError = new APIError('Not found', 404, 'Forgejo');
|
|
1437
|
+
api.getBranchPullRequest = jest.fn().mockRejectedValue(notFoundError);
|
|
1438
|
+
|
|
1439
|
+
const result = await api.filterOpenAuthoringBranches('cms/contributor/repo/posts/entry');
|
|
1440
|
+
|
|
1441
|
+
expect(result).toEqual({ branch: 'cms/contributor/repo/posts/entry', filter: false });
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
it('should filter out branches on EditorialWorkflowError', async () => {
|
|
1445
|
+
const api = new API({
|
|
1446
|
+
branch: 'master',
|
|
1447
|
+
repo: 'contributor/repo',
|
|
1448
|
+
originRepo: 'owner/repo',
|
|
1449
|
+
useOpenAuthoring: true,
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
const workflowError = new EditorialWorkflowError(
|
|
1453
|
+
'content is not under editorial workflow',
|
|
1454
|
+
true,
|
|
1455
|
+
);
|
|
1456
|
+
api.getBranchPullRequest = jest.fn().mockRejectedValue(workflowError);
|
|
1457
|
+
|
|
1458
|
+
const result = await api.filterOpenAuthoringBranches('cms/contributor/repo/posts/entry');
|
|
1459
|
+
|
|
1460
|
+
expect(result).toEqual({ branch: 'cms/contributor/repo/posts/entry', filter: false });
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
it('should keep branches on transient network errors', async () => {
|
|
1464
|
+
const api = new API({
|
|
1465
|
+
branch: 'master',
|
|
1466
|
+
repo: 'contributor/repo',
|
|
1467
|
+
originRepo: 'owner/repo',
|
|
1468
|
+
useOpenAuthoring: true,
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
const networkError = new APIError('Network error', 500, 'Forgejo');
|
|
1472
|
+
api.getBranchPullRequest = jest.fn().mockRejectedValue(networkError);
|
|
1473
|
+
|
|
1474
|
+
const result = await api.filterOpenAuthoringBranches('cms/contributor/repo/posts/entry');
|
|
1475
|
+
|
|
1476
|
+
expect(result).toEqual({ branch: 'cms/contributor/repo/posts/entry', filter: true });
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
});
|