decap-cms-backend-gitea 3.0.4

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,388 @@
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('gitea API', () => {
8
+ beforeEach(() => {
9
+ jest.clearAllMocks();
10
+ });
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ function mockAPI(api, responses) {
14
+ api.request = jest.fn().mockImplementation((path, options = {}) => {
15
+ const normalizedPath = path.indexOf('?') !== -1 ? path.slice(0, path.indexOf('?')) : path;
16
+ const response = responses[normalizedPath];
17
+ return typeof response === 'function'
18
+ ? Promise.resolve(response(options))
19
+ : Promise.reject(new Error(`No response for path '${normalizedPath}'`));
20
+ });
21
+ }
22
+
23
+ describe('request', () => {
24
+ const fetch = jest.fn();
25
+ beforeEach(() => {
26
+ global.fetch = fetch;
27
+ });
28
+
29
+ afterEach(() => {
30
+ jest.clearAllMocks();
31
+ });
32
+
33
+ it('should fetch url with authorization header', async () => {
34
+ const api = new API({ branch: 'gh-pages', repo: 'my-repo', token: 'token' });
35
+
36
+ fetch.mockResolvedValue({
37
+ text: jest.fn().mockResolvedValue('some response'),
38
+ ok: true,
39
+ status: 200,
40
+ headers: { get: () => '' },
41
+ });
42
+ const result = await api.request('/some-path');
43
+ expect(result).toEqual('some response');
44
+ expect(fetch).toHaveBeenCalledTimes(1);
45
+ expect(fetch).toHaveBeenCalledWith('https://try.gitea.io/api/v1/some-path', {
46
+ cache: 'no-cache',
47
+ headers: {
48
+ Authorization: 'token token',
49
+ 'Content-Type': 'application/json; charset=utf-8',
50
+ },
51
+ signal: expect.any(AbortSignal),
52
+ });
53
+ });
54
+
55
+ it('should throw error on not ok response', async () => {
56
+ const api = new API({ branch: 'gt-pages', repo: 'my-repo', token: 'token' });
57
+
58
+ fetch.mockResolvedValue({
59
+ text: jest.fn().mockResolvedValue({ message: 'some error' }),
60
+ ok: false,
61
+ status: 404,
62
+ headers: { get: () => '' },
63
+ });
64
+
65
+ await expect(api.request('some-path')).rejects.toThrow(
66
+ expect.objectContaining({
67
+ message: 'some error',
68
+ name: 'API_ERROR',
69
+ status: 404,
70
+ api: 'Gitea',
71
+ }),
72
+ );
73
+ });
74
+
75
+ it('should allow overriding requestHeaders to return a promise ', async () => {
76
+ const api = new API({ branch: 'gt-pages', repo: 'my-repo', token: 'token' });
77
+
78
+ api.requestHeaders = jest.fn().mockResolvedValue({
79
+ Authorization: 'promise-token',
80
+ 'Content-Type': 'application/json; charset=utf-8',
81
+ });
82
+
83
+ fetch.mockResolvedValue({
84
+ text: jest.fn().mockResolvedValue('some response'),
85
+ ok: true,
86
+ status: 200,
87
+ headers: { get: () => '' },
88
+ });
89
+ const result = await api.request('/some-path');
90
+ expect(result).toEqual('some response');
91
+ expect(fetch).toHaveBeenCalledTimes(1);
92
+ expect(fetch).toHaveBeenCalledWith('https://try.gitea.io/api/v1/some-path', {
93
+ cache: 'no-cache',
94
+ headers: {
95
+ Authorization: 'promise-token',
96
+ 'Content-Type': 'application/json; charset=utf-8',
97
+ },
98
+ signal: expect.any(AbortSignal),
99
+ });
100
+ });
101
+ });
102
+
103
+ describe('persistFiles', () => {
104
+ it('should create a new commit', async () => {
105
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
106
+
107
+ const responses = {
108
+ '/repos/owner/repo/git/trees/master:content%2Fposts': () => {
109
+ return { tree: [{ path: 'update-post.md', sha: 'old-sha' }] };
110
+ },
111
+
112
+ '/repos/owner/repo/contents': () => ({
113
+ commit: { sha: 'new-sha' },
114
+ files: [
115
+ {
116
+ path: 'content/posts/new-post.md',
117
+ },
118
+ {
119
+ path: 'content/posts/update-post.md',
120
+ },
121
+ ],
122
+ }),
123
+ };
124
+ mockAPI(api, responses);
125
+
126
+ const entry = {
127
+ dataFiles: [
128
+ {
129
+ slug: 'entry',
130
+ path: 'content/posts/new-post.md',
131
+ raw: 'content',
132
+ },
133
+ {
134
+ slug: 'entry',
135
+ sha: 'old-sha',
136
+ path: 'content/posts/update-post.md',
137
+ raw: 'content',
138
+ },
139
+ ],
140
+ assets: [],
141
+ };
142
+ await expect(
143
+ api.persistFiles(entry.dataFiles, entry.assets, {
144
+ commitMessage: 'commitMessage',
145
+ newEntry: true,
146
+ }),
147
+ ).resolves.toEqual({
148
+ commit: { sha: 'new-sha' },
149
+ files: [
150
+ {
151
+ path: 'content/posts/new-post.md',
152
+ },
153
+ {
154
+ path: 'content/posts/update-post.md',
155
+ },
156
+ ],
157
+ });
158
+
159
+ expect(api.request).toHaveBeenCalledTimes(3);
160
+
161
+ expect(api.request.mock.calls[0]).toEqual([
162
+ '/repos/owner/repo/git/trees/master:content%2Fposts',
163
+ ]);
164
+
165
+ expect(api.request.mock.calls[1]).toEqual([
166
+ '/repos/owner/repo/git/trees/master:content%2Fposts',
167
+ ]);
168
+
169
+ expect(api.request.mock.calls[2]).toEqual([
170
+ '/repos/owner/repo/contents',
171
+ {
172
+ method: 'POST',
173
+ body: JSON.stringify({
174
+ branch: 'master',
175
+ files: [
176
+ {
177
+ operation: 'create',
178
+ content: Base64.encode(entry.dataFiles[0].raw),
179
+ path: entry.dataFiles[0].path,
180
+ },
181
+ {
182
+ operation: 'update',
183
+ content: Base64.encode(entry.dataFiles[1].raw),
184
+ path: entry.dataFiles[1].path,
185
+ sha: entry.dataFiles[1].sha,
186
+ },
187
+ ],
188
+ message: 'commitMessage',
189
+ }),
190
+ },
191
+ ]);
192
+ });
193
+ });
194
+
195
+ describe('deleteFiles', () => {
196
+ it('should check if files exist and delete them', async () => {
197
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
198
+
199
+ const responses = {
200
+ '/repos/owner/repo/git/trees/master:content%2Fposts': () => {
201
+ return {
202
+ tree: [
203
+ { path: 'delete-post-1.md', sha: 'old-sha-1' },
204
+ { path: 'delete-post-2.md', sha: 'old-sha-2' },
205
+ ],
206
+ };
207
+ },
208
+
209
+ '/repos/owner/repo/contents': () => ({
210
+ commit: { sha: 'new-sha' },
211
+ files: [
212
+ {
213
+ path: 'content/posts/delete-post-1.md',
214
+ },
215
+ {
216
+ path: 'content/posts/delete-post-2.md',
217
+ },
218
+ ],
219
+ }),
220
+ };
221
+ mockAPI(api, responses);
222
+
223
+ const deleteFiles = ['content/posts/delete-post-1.md', 'content/posts/delete-post-2.md'];
224
+
225
+ await api.deleteFiles(deleteFiles, 'commitMessage');
226
+
227
+ expect(api.request).toHaveBeenCalledTimes(3);
228
+
229
+ expect(api.request.mock.calls[0]).toEqual([
230
+ '/repos/owner/repo/git/trees/master:content%2Fposts',
231
+ ]);
232
+
233
+ expect(api.request.mock.calls[1]).toEqual([
234
+ '/repos/owner/repo/git/trees/master:content%2Fposts',
235
+ ]);
236
+
237
+ expect(api.request.mock.calls[2]).toEqual([
238
+ '/repos/owner/repo/contents',
239
+ {
240
+ method: 'POST',
241
+ body: JSON.stringify({
242
+ branch: 'master',
243
+ files: [
244
+ {
245
+ operation: 'delete',
246
+ path: deleteFiles[0],
247
+ sha: 'old-sha-1',
248
+ },
249
+ {
250
+ operation: 'delete',
251
+ path: deleteFiles[1],
252
+ sha: 'old-sha-2',
253
+ },
254
+ ],
255
+ message: 'commitMessage',
256
+ }),
257
+ },
258
+ ]);
259
+ });
260
+ });
261
+
262
+ describe('listFiles', () => {
263
+ it('should get files by depth', async () => {
264
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
265
+
266
+ const tree = [
267
+ {
268
+ path: 'post.md',
269
+ type: 'blob',
270
+ },
271
+ {
272
+ path: 'dir1',
273
+ type: 'tree',
274
+ },
275
+ {
276
+ path: 'dir1/nested-post.md',
277
+ type: 'blob',
278
+ },
279
+ {
280
+ path: 'dir1/dir2',
281
+ type: 'tree',
282
+ },
283
+ {
284
+ path: 'dir1/dir2/nested-post.md',
285
+ type: 'blob',
286
+ },
287
+ ];
288
+ api.request = jest.fn().mockResolvedValue({ tree });
289
+
290
+ await expect(api.listFiles('posts', { depth: 1 })).resolves.toEqual([
291
+ {
292
+ path: 'posts/post.md',
293
+ type: 'blob',
294
+ name: 'post.md',
295
+ },
296
+ ]);
297
+ expect(api.request).toHaveBeenCalledTimes(1);
298
+ expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
299
+ params: {},
300
+ });
301
+
302
+ jest.clearAllMocks();
303
+ await expect(api.listFiles('posts', { depth: 2 })).resolves.toEqual([
304
+ {
305
+ path: 'posts/post.md',
306
+ type: 'blob',
307
+ name: 'post.md',
308
+ },
309
+ {
310
+ path: 'posts/dir1/nested-post.md',
311
+ type: 'blob',
312
+ name: 'nested-post.md',
313
+ },
314
+ ]);
315
+ expect(api.request).toHaveBeenCalledTimes(1);
316
+ expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
317
+ params: { recursive: 1 },
318
+ });
319
+
320
+ jest.clearAllMocks();
321
+ await expect(api.listFiles('posts', { depth: 3 })).resolves.toEqual([
322
+ {
323
+ path: 'posts/post.md',
324
+ type: 'blob',
325
+ name: 'post.md',
326
+ },
327
+ {
328
+ path: 'posts/dir1/nested-post.md',
329
+ type: 'blob',
330
+ name: 'nested-post.md',
331
+ },
332
+ {
333
+ path: 'posts/dir1/dir2/nested-post.md',
334
+ type: 'blob',
335
+ name: 'nested-post.md',
336
+ },
337
+ ]);
338
+ expect(api.request).toHaveBeenCalledTimes(1);
339
+ expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:posts', {
340
+ params: { recursive: 1 },
341
+ });
342
+ });
343
+ it('should get files and folders', async () => {
344
+ const api = new API({ branch: 'master', repo: 'owner/repo' });
345
+
346
+ const tree = [
347
+ {
348
+ path: 'image.png',
349
+ type: 'blob',
350
+ },
351
+ {
352
+ path: 'dir1',
353
+ type: 'tree',
354
+ },
355
+ {
356
+ path: 'dir1/nested-image.png',
357
+ type: 'blob',
358
+ },
359
+ {
360
+ path: 'dir1/dir2',
361
+ type: 'tree',
362
+ },
363
+ {
364
+ path: 'dir1/dir2/nested-image.png',
365
+ type: 'blob',
366
+ },
367
+ ];
368
+ api.request = jest.fn().mockResolvedValue({ tree });
369
+
370
+ await expect(api.listFiles('media', {}, true)).resolves.toEqual([
371
+ {
372
+ path: 'media/image.png',
373
+ type: 'blob',
374
+ name: 'image.png',
375
+ },
376
+ {
377
+ path: 'media/dir1',
378
+ type: 'tree',
379
+ name: 'dir1',
380
+ },
381
+ ]);
382
+ expect(api.request).toHaveBeenCalledTimes(1);
383
+ expect(api.request).toHaveBeenCalledWith('/repos/owner/repo/git/trees/master:media', {
384
+ params: {},
385
+ });
386
+ });
387
+ });
388
+ });
@@ -0,0 +1,284 @@
1
+ import { Cursor, CURSOR_COMPATIBILITY_SYMBOL } from 'decap-cms-lib-util';
2
+
3
+ import GiteaImplementation from '../implementation';
4
+
5
+ jest.spyOn(console, 'error').mockImplementation(() => {});
6
+
7
+ describe('gitea backend implementation', () => {
8
+ const config = {
9
+ backend: {
10
+ repo: 'owner/repo',
11
+ api_root: 'https://try.gitea.io/api/v1',
12
+ },
13
+ };
14
+
15
+ const createObjectURL = jest.fn();
16
+ global.URL = {
17
+ createObjectURL,
18
+ };
19
+
20
+ createObjectURL.mockReturnValue('displayURL');
21
+
22
+ beforeAll(() => {
23
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
24
+ });
25
+
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+ });
29
+
30
+ afterAll(() => {
31
+ jest.restoreAllMocks();
32
+ });
33
+
34
+ describe('persistMedia', () => {
35
+ const persistFiles = jest.fn();
36
+ const mockAPI = {
37
+ persistFiles,
38
+ };
39
+
40
+ persistFiles.mockImplementation((_, files) => {
41
+ files.forEach((file, index) => {
42
+ file.sha = index;
43
+ });
44
+ });
45
+
46
+ it('should persist media file', async () => {
47
+ const giteaImplementation = new GiteaImplementation(config);
48
+ giteaImplementation.api = mockAPI;
49
+
50
+ const mediaFile = {
51
+ fileObj: { size: 100, name: 'image.png' },
52
+ path: '/media/image.png',
53
+ };
54
+
55
+ expect.assertions(5);
56
+ await expect(
57
+ giteaImplementation.persistMedia(mediaFile, { commitMessage: 'Persisting media' }),
58
+ ).resolves.toEqual({
59
+ id: 0,
60
+ name: 'image.png',
61
+ size: 100,
62
+ displayURL: 'displayURL',
63
+ path: 'media/image.png',
64
+ });
65
+
66
+ expect(persistFiles).toHaveBeenCalledTimes(1);
67
+ expect(persistFiles).toHaveBeenCalledWith([], [mediaFile], {
68
+ commitMessage: 'Persisting media',
69
+ });
70
+ expect(createObjectURL).toHaveBeenCalledTimes(1);
71
+ expect(createObjectURL).toHaveBeenCalledWith(mediaFile.fileObj);
72
+ });
73
+
74
+ it('should log and throw error on "persistFiles" error', async () => {
75
+ const giteaImplementation = new GiteaImplementation(config);
76
+ giteaImplementation.api = mockAPI;
77
+
78
+ const error = new Error('failed to persist files');
79
+ persistFiles.mockRejectedValue(error);
80
+
81
+ const mediaFile = {
82
+ fileObj: { size: 100 },
83
+ path: '/media/image.png',
84
+ };
85
+
86
+ expect.assertions(5);
87
+ await expect(
88
+ giteaImplementation.persistMedia(mediaFile, { commitMessage: 'Persisting media' }),
89
+ ).rejects.toThrowError(error);
90
+
91
+ expect(persistFiles).toHaveBeenCalledTimes(1);
92
+ expect(createObjectURL).toHaveBeenCalledTimes(0);
93
+ expect(console.error).toHaveBeenCalledTimes(1);
94
+ expect(console.error).toHaveBeenCalledWith(error);
95
+ });
96
+ });
97
+
98
+ describe('entriesByFolder', () => {
99
+ const listFiles = jest.fn();
100
+ const readFile = jest.fn();
101
+ const readFileMetadata = jest.fn(() => Promise.resolve({ author: '', updatedOn: '' }));
102
+
103
+ const mockAPI = {
104
+ listFiles,
105
+ readFile,
106
+ readFileMetadata,
107
+ originRepoURL: 'originRepoURL',
108
+ };
109
+
110
+ it('should return entries and cursor', async () => {
111
+ const giteaImplementation = new GiteaImplementation(config);
112
+ giteaImplementation.api = mockAPI;
113
+
114
+ const files = [];
115
+ const count = 1501;
116
+ for (let i = 0; i < count; i++) {
117
+ const id = `${i}`.padStart(`${count}`.length, '0');
118
+ files.push({
119
+ id,
120
+ path: `posts/post-${id}.md`,
121
+ });
122
+ }
123
+
124
+ listFiles.mockResolvedValue(files);
125
+ readFile.mockImplementation((_path, id) => Promise.resolve(`${id}`));
126
+
127
+ const expectedEntries = files
128
+ .slice(0, 20)
129
+ .map(({ id, path }) => ({ data: id, file: { path, id, author: '', updatedOn: '' } }));
130
+
131
+ const expectedCursor = Cursor.create({
132
+ actions: ['next', 'last'],
133
+ meta: { page: 1, count, pageSize: 20, pageCount: 76 },
134
+ data: { files },
135
+ });
136
+
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ expectedEntries[CURSOR_COMPATIBILITY_SYMBOL] = expectedCursor;
139
+
140
+ const result = await giteaImplementation.entriesByFolder('posts', 'md', 1);
141
+
142
+ expect(result).toEqual(expectedEntries);
143
+ expect(listFiles).toHaveBeenCalledTimes(1);
144
+ expect(listFiles).toHaveBeenCalledWith('posts', { depth: 1, repoURL: 'originRepoURL' });
145
+ expect(readFile).toHaveBeenCalledTimes(20);
146
+ });
147
+ });
148
+
149
+ describe('traverseCursor', () => {
150
+ const listFiles = jest.fn();
151
+ const readFile = jest.fn((_path, id) => Promise.resolve(`${id}`));
152
+ const readFileMetadata = jest.fn(() => Promise.resolve({}));
153
+
154
+ const mockAPI = {
155
+ listFiles,
156
+ readFile,
157
+ originRepoURL: 'originRepoURL',
158
+ readFileMetadata,
159
+ };
160
+
161
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
+ const files = [];
163
+ const count = 1501;
164
+ for (let i = 0; i < count; i++) {
165
+ const id = `${i}`.padStart(`${count}`.length, '0');
166
+ files.push({
167
+ id,
168
+ path: `posts/post-${id}.md`,
169
+ });
170
+ }
171
+
172
+ it('should handle next action', async () => {
173
+ const giteaImplementation = new GiteaImplementation(config);
174
+ giteaImplementation.api = mockAPI;
175
+
176
+ const cursor = Cursor.create({
177
+ actions: ['next', 'last'],
178
+ meta: { page: 1, count, pageSize: 20, pageCount: 76 },
179
+ data: { files },
180
+ });
181
+
182
+ const expectedEntries = files
183
+ .slice(20, 40)
184
+ .map(({ id, path }) => ({ data: id, file: { path, id } }));
185
+
186
+ const expectedCursor = Cursor.create({
187
+ actions: ['prev', 'first', 'next', 'last'],
188
+ meta: { page: 2, count, pageSize: 20, pageCount: 76 },
189
+ data: { files },
190
+ });
191
+
192
+ const result = await giteaImplementation.traverseCursor(cursor, 'next');
193
+
194
+ expect(result).toEqual({
195
+ entries: expectedEntries,
196
+ cursor: expectedCursor,
197
+ });
198
+ });
199
+
200
+ it('should handle prev action', async () => {
201
+ const giteaImplementation = new GiteaImplementation(config);
202
+ giteaImplementation.api = mockAPI;
203
+
204
+ const cursor = Cursor.create({
205
+ actions: ['prev', 'first', 'next', 'last'],
206
+ meta: { page: 2, count, pageSize: 20, pageCount: 76 },
207
+ data: { files },
208
+ });
209
+
210
+ const expectedEntries = files
211
+ .slice(0, 20)
212
+ .map(({ id, path }) => ({ data: id, file: { path, id } }));
213
+
214
+ const expectedCursor = Cursor.create({
215
+ actions: ['next', 'last'],
216
+ meta: { page: 1, count, pageSize: 20, pageCount: 76 },
217
+ data: { files },
218
+ });
219
+
220
+ const result = await giteaImplementation.traverseCursor(cursor, 'prev');
221
+
222
+ expect(result).toEqual({
223
+ entries: expectedEntries,
224
+ cursor: expectedCursor,
225
+ });
226
+ });
227
+
228
+ it('should handle last action', async () => {
229
+ const giteaImplementation = new GiteaImplementation(config);
230
+ giteaImplementation.api = mockAPI;
231
+
232
+ const cursor = Cursor.create({
233
+ actions: ['next', 'last'],
234
+ meta: { page: 1, count, pageSize: 20, pageCount: 76 },
235
+ data: { files },
236
+ });
237
+
238
+ const expectedEntries = files
239
+ .slice(1500)
240
+ .map(({ id, path }) => ({ data: id, file: { path, id } }));
241
+
242
+ const expectedCursor = Cursor.create({
243
+ actions: ['prev', 'first'],
244
+ meta: { page: 76, count, pageSize: 20, pageCount: 76 },
245
+ data: { files },
246
+ });
247
+
248
+ const result = await giteaImplementation.traverseCursor(cursor, 'last');
249
+
250
+ expect(result).toEqual({
251
+ entries: expectedEntries,
252
+ cursor: expectedCursor,
253
+ });
254
+ });
255
+
256
+ it('should handle first action', async () => {
257
+ const giteaImplementation = new GiteaImplementation(config);
258
+ giteaImplementation.api = mockAPI;
259
+
260
+ const cursor = Cursor.create({
261
+ actions: ['prev', 'first'],
262
+ meta: { page: 76, count, pageSize: 20, pageCount: 76 },
263
+ data: { files },
264
+ });
265
+
266
+ const expectedEntries = files
267
+ .slice(0, 20)
268
+ .map(({ id, path }) => ({ data: id, file: { path, id } }));
269
+
270
+ const expectedCursor = Cursor.create({
271
+ actions: ['next', 'last'],
272
+ meta: { page: 1, count, pageSize: 20, pageCount: 76 },
273
+ data: { files },
274
+ });
275
+
276
+ const result = await giteaImplementation.traverseCursor(cursor, 'first');
277
+
278
+ expect(result).toEqual({
279
+ entries: expectedEntries,
280
+ cursor: expectedCursor,
281
+ });
282
+ });
283
+ });
284
+ });