@bestend/confluence-cli 1.15.1

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/llms.txt ADDED
@@ -0,0 +1,46 @@
1
+ # Confluence CLI
2
+
3
+ > A powerful command-line interface for Atlassian Confluence that allows you to read, search, and manage your Confluence content from the terminal.
4
+
5
+ This CLI tool provides a comprehensive set of commands for interacting with Confluence. Key features include creating, reading, updating, and searching for pages. It supports various content formats like markdown and HTML.
6
+
7
+ To get started, install the tool via npm and initialize the configuration:
8
+ ```sh
9
+ npm install -g confluence-cli
10
+ confluence init
11
+ ```
12
+
13
+ ## Documentation
14
+
15
+ - [README.md](./README.md): Main documentation with installation, usage, and command reference.
16
+
17
+ ## Core Source Code
18
+
19
+ - [bin/confluence.js](./bin/confluence.js): The main entry point for the CLI, defines commands and argument parsing.
20
+ - [lib/confluence-client.js](./lib/confluence-client.js): The client library for interacting with the Confluence REST API.
21
+ - [package.json](./package.json): Project metadata, dependencies, and scripts.
22
+
23
+ ## Recent Changes & Fixes
24
+
25
+ This section summarizes recent improvements to align the codebase with its documentation and fix bugs.
26
+
27
+ ### `update` Command Logic and Documentation:
28
+ - **Inconsistency:** The `README.md` suggested that updating a page's title without changing its content was possible. However, the implementation in `bin/confluence.js` incorrectly threw an error if the `--content` or `--file` flags were not provided, making title-only updates impossible.
29
+ - **Fix:**
30
+ - Modified `updatePage` in `lib/confluence-client.js` to fetch and re-use existing page content if no new content is provided.
31
+ - Adjusted validation in `bin/confluence.js` for the `update` command to only require one of `--title`, `--content`, or `--file`.
32
+ - Updated `README.md` with an example of a title-only update.
33
+
34
+ ### Incomplete `README.md` Command Reference:
35
+ - **Inconsistency:** The main command table in `README.md` was missing several commands (`create`, `create-child`, `update`, `edit`, `find`).
36
+ - **Fix:** Expanded the command table in `README.md` to include all available commands.
37
+
38
+ ### Misleading `read` Command URL Examples:
39
+ - **Inconsistency:** The documentation for the `read` command used "display" or "pretty" URLs, which are not supported by the `extractPageId` function.
40
+ - **Fix:** Removed incorrect URL examples and clarified that only URLs with a `pageId` query parameter are supported.
41
+
42
+ ## Next Steps
43
+
44
+ - Refer to [README.md](./README.md) for detailed usage instructions and advanced configuration.
45
+ - For troubleshooting or to contribute, visit the project's GitHub repository at https://github.com/pchuri/confluence-cli.
46
+ - If you encounter issues, open an issue or check the FAQ section in the documentation.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@bestend/confluence-cli",
3
+ "version": "1.15.1",
4
+ "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "confluence": "bin/index.js",
8
+ "confluence-cli": "bin/index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/confluence.js",
12
+ "test": "jest",
13
+ "lint": "eslint .",
14
+ "lint:fix": "eslint . --fix"
15
+ },
16
+ "keywords": [
17
+ "confluence",
18
+ "atlassian",
19
+ "cli",
20
+ "wiki",
21
+ "documentation"
22
+ ],
23
+ "author": "pchuri",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "axios": "^1.12.0",
27
+ "chalk": "^4.1.2",
28
+ "commander": "^11.1.0",
29
+ "html-to-text": "^9.0.5",
30
+ "inquirer": "^8.2.6",
31
+ "markdown-it": "^14.1.0",
32
+ "ora": "^5.4.1"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^20.10.0",
36
+ "axios-mock-adapter": "^2.1.0",
37
+ "eslint": "^9.39.2",
38
+ "jest": "^29.7.0"
39
+ },
40
+ "overrides": {
41
+ "js-yaml": "^4.1.1",
42
+ "@istanbuljs/load-nyc-config": {
43
+ "js-yaml": "^3.14.2"
44
+ }
45
+ },
46
+ "engines": {
47
+ "node": ">=14.0.0"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/bestend/confluence-cli.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/bestend/confluence-cli/issues"
55
+ },
56
+ "homepage": "https://github.com/bestend/confluence-cli#readme"
57
+ }
@@ -0,0 +1,459 @@
1
+ const ConfluenceClient = require('../lib/confluence-client');
2
+ const MockAdapter = require('axios-mock-adapter');
3
+
4
+ describe('ConfluenceClient', () => {
5
+ let client;
6
+
7
+ beforeEach(() => {
8
+ client = new ConfluenceClient({
9
+ domain: 'test.atlassian.net',
10
+ token: 'test-token'
11
+ });
12
+ });
13
+
14
+ describe('api path handling', () => {
15
+ test('defaults to /rest/api when path is not provided', () => {
16
+ const defaultClient = new ConfluenceClient({
17
+ domain: 'example.com',
18
+ token: 'no-path-token'
19
+ });
20
+
21
+ expect(defaultClient.baseURL).toBe('https://example.com/rest/api');
22
+ });
23
+
24
+ test('normalizes custom api paths', () => {
25
+ const customClient = new ConfluenceClient({
26
+ domain: 'cloud.example',
27
+ token: 'custom-path',
28
+ apiPath: 'wiki/rest/api/'
29
+ });
30
+
31
+ expect(customClient.baseURL).toBe('https://cloud.example/wiki/rest/api');
32
+ });
33
+ });
34
+
35
+ describe('authentication setup', () => {
36
+ test('uses bearer token headers by default', () => {
37
+ const bearerClient = new ConfluenceClient({
38
+ domain: 'test.atlassian.net',
39
+ token: 'bearer-token'
40
+ });
41
+
42
+ expect(bearerClient.client.defaults.headers.Authorization).toBe('Bearer bearer-token');
43
+ });
44
+
45
+ test('builds basic auth headers when email is provided', () => {
46
+ const basicClient = new ConfluenceClient({
47
+ domain: 'test.atlassian.net',
48
+ token: 'basic-token',
49
+ authType: 'basic',
50
+ email: 'user@example.com'
51
+ });
52
+
53
+ const encoded = Buffer.from('user@example.com:basic-token').toString('base64');
54
+ expect(basicClient.client.defaults.headers.Authorization).toBe(`Basic ${encoded}`);
55
+ });
56
+
57
+ test('throws when basic auth is missing an email', () => {
58
+ expect(() => new ConfluenceClient({
59
+ domain: 'test.atlassian.net',
60
+ token: 'missing-email',
61
+ authType: 'basic'
62
+ })).toThrow('Basic authentication requires an email address.');
63
+ });
64
+ });
65
+
66
+ describe('extractPageId', () => {
67
+ test('should return numeric page ID as is', async () => {
68
+ expect(await client.extractPageId('123456789')).toBe('123456789');
69
+ expect(await client.extractPageId(123456789)).toBe(123456789);
70
+ });
71
+
72
+ test('should extract page ID from URL with pageId parameter', async () => {
73
+ const url = 'https://test.atlassian.net/wiki/spaces/TEST/pages/123456789/Page+Title';
74
+ expect(await client.extractPageId(url + '?pageId=987654321')).toBe('987654321');
75
+ });
76
+
77
+ test('should resolve display URLs', async () => {
78
+ // Mock the API response for display URL resolution
79
+ const mock = new MockAdapter(client.client);
80
+
81
+ mock.onGet('/content').reply(200, {
82
+ results: [{
83
+ id: '12345',
84
+ title: 'Page Title',
85
+ _links: { webui: '/display/TEST/Page+Title' }
86
+ }]
87
+ });
88
+
89
+ const displayUrl = 'https://test.atlassian.net/display/TEST/Page+Title';
90
+ expect(await client.extractPageId(displayUrl)).toBe('12345');
91
+
92
+ mock.restore();
93
+ });
94
+
95
+ test('should resolve nested display URLs', async () => {
96
+ // Mock the API response for display URL resolution
97
+ const mock = new MockAdapter(client.client);
98
+
99
+ mock.onGet('/content').reply(200, {
100
+ results: [{
101
+ id: '67890',
102
+ title: 'Child Page',
103
+ _links: { webui: '/display/TEST/Parent/Child+Page' }
104
+ }]
105
+ });
106
+
107
+ const displayUrl = 'https://test.atlassian.net/display/TEST/Parent/Child+Page';
108
+ expect(await client.extractPageId(displayUrl)).toBe('67890');
109
+
110
+ mock.restore();
111
+ });
112
+
113
+ test('should throw error when display URL cannot be resolved', async () => {
114
+ const mock = new MockAdapter(client.client);
115
+
116
+ // Mock empty result
117
+ mock.onGet('/content').reply(200, {
118
+ results: []
119
+ });
120
+
121
+ const displayUrl = 'https://test.atlassian.net/display/TEST/NonExistentPage';
122
+ await expect(client.extractPageId(displayUrl)).rejects.toThrow(/Could not resolve page ID/);
123
+
124
+ mock.restore();
125
+ });
126
+ });
127
+
128
+ describe('markdownToStorage', () => {
129
+ test('should convert basic markdown to native Confluence storage format', () => {
130
+ const markdown = '# Hello World\n\nThis is a **test** page with *italic* text.';
131
+ const result = client.markdownToStorage(markdown);
132
+
133
+ expect(result).toContain('<h1>Hello World</h1>');
134
+ expect(result).toContain('<p>This is a <strong>test</strong> page with <em>italic</em> text.</p>');
135
+ expect(result).not.toContain('<ac:structured-macro ac:name="html">');
136
+ });
137
+
138
+ test('should convert code blocks to Confluence code macro', () => {
139
+ const markdown = '```javascript\nconsole.log("Hello World");\n```';
140
+ const result = client.markdownToStorage(markdown);
141
+
142
+ expect(result).toContain('<ac:structured-macro ac:name="code">');
143
+ expect(result).toContain('<ac:parameter ac:name="language">javascript</ac:parameter>');
144
+ expect(result).toContain('console.log(&quot;Hello World&quot;);');
145
+ });
146
+
147
+ test('should convert lists to native Confluence format', () => {
148
+ const markdown = '- Item 1\n- Item 2\n\n1. First\n2. Second';
149
+ const result = client.markdownToStorage(markdown);
150
+
151
+ expect(result).toContain('<ul>');
152
+ expect(result).toContain('<li><p>Item 1</p></li>');
153
+ expect(result).toContain('<ol>');
154
+ expect(result).toContain('<li><p>First</p></li>');
155
+ });
156
+
157
+ test('should convert Confluence admonitions', () => {
158
+ const markdown = '[!info]\nThis is an info message';
159
+ const result = client.markdownToStorage(markdown);
160
+
161
+ expect(result).toContain('<ac:structured-macro ac:name="info">');
162
+ expect(result).toContain('This is an info message');
163
+ });
164
+
165
+ test('should convert tables to native Confluence format', () => {
166
+ const markdown = '| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |';
167
+ const result = client.markdownToStorage(markdown);
168
+
169
+ expect(result).toContain('<table>');
170
+ expect(result).toContain('<th><p>Header 1</p></th>');
171
+ expect(result).toContain('<td><p>Cell 1</p></td>');
172
+ });
173
+
174
+ test('should convert links to Confluence link format', () => {
175
+ const markdown = '[Example Link](https://example.com)';
176
+ const result = client.markdownToStorage(markdown);
177
+
178
+ expect(result).toContain('<ac:link>');
179
+ expect(result).toContain('ri:value="https://example.com"');
180
+ expect(result).toContain('Example Link');
181
+ });
182
+ });
183
+
184
+ describe('markdownToNativeStorage', () => {
185
+ test('should act as an alias to htmlToConfluenceStorage via markdown render', () => {
186
+ const markdown = '# Native Storage Test';
187
+ const result = client.markdownToNativeStorage(markdown);
188
+
189
+ expect(result).toContain('<h1>Native Storage Test</h1>');
190
+ });
191
+
192
+ test('should handle code blocks correctly', () => {
193
+ const markdown = '```javascript\nconst a = 1;\n```';
194
+ const result = client.markdownToNativeStorage(markdown);
195
+
196
+ expect(result).toContain('<ac:structured-macro ac:name="code">');
197
+ expect(result).toContain('const a = 1;');
198
+ });
199
+ });
200
+
201
+ describe('storageToMarkdown', () => {
202
+ test('should convert Confluence storage format to markdown', () => {
203
+ const storage = '<h1>Hello World</h1><p>This is a <strong>test</strong> page.</p>';
204
+ const result = client.storageToMarkdown(storage);
205
+
206
+ expect(result).toContain('# Hello World');
207
+ expect(result).toContain('**test**');
208
+ });
209
+
210
+ test('should convert Confluence code macro to markdown', () => {
211
+ const storage = '<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">javascript</ac:parameter><ac:plain-text-body><![CDATA[console.log("Hello");]]></ac:plain-text-body></ac:structured-macro>';
212
+ const result = client.storageToMarkdown(storage);
213
+
214
+ expect(result).toContain('```javascript');
215
+ expect(result).toContain('console.log("Hello");');
216
+ expect(result).toContain('```');
217
+ });
218
+
219
+ test('should convert Confluence macros to admonitions', () => {
220
+ const storage = '<ac:structured-macro ac:name="info"><ac:rich-text-body><p>This is info</p></ac:rich-text-body></ac:structured-macro>';
221
+ const result = client.storageToMarkdown(storage);
222
+
223
+ expect(result).toContain('[!info]');
224
+ expect(result).toContain('This is info');
225
+ });
226
+
227
+ test('should convert Confluence links to markdown', () => {
228
+ const storage = '<ac:link><ri:url ri:value="https://example.com" /><ac:plain-text-link-body><![CDATA[Example]]></ac:plain-text-link-body></ac:link>';
229
+ const result = client.storageToMarkdown(storage);
230
+
231
+ expect(result).toContain('[Example](https://example.com)');
232
+ });
233
+ });
234
+
235
+ describe('htmlToMarkdown', () => {
236
+ test('should convert basic HTML to markdown', () => {
237
+ const html = '<h2>Title</h2><p>Some <strong>bold</strong> and <em>italic</em> text.</p>';
238
+ const result = client.htmlToMarkdown(html);
239
+
240
+ expect(result).toContain('## Title');
241
+ expect(result).toContain('**bold**');
242
+ expect(result).toContain('*italic*');
243
+ });
244
+
245
+ test('should convert HTML lists to markdown', () => {
246
+ const html = '<ul><li><p>Item 1</p></li><li><p>Item 2</p></li></ul>';
247
+ const result = client.htmlToMarkdown(html);
248
+
249
+ expect(result).toContain('- Item 1');
250
+ expect(result).toContain('- Item 2');
251
+ });
252
+
253
+ test('should convert HTML tables to markdown', () => {
254
+ const html = '<table><tr><th><p>Header</p></th></tr><tr><td><p>Cell</p></td></tr></table>';
255
+ const result = client.htmlToMarkdown(html);
256
+
257
+ expect(result).toContain('| Header |');
258
+ expect(result).toContain('| --- |');
259
+ expect(result).toContain('| Cell |');
260
+ });
261
+ });
262
+
263
+ describe('page creation and updates', () => {
264
+ test('should have required methods for page management', () => {
265
+ expect(typeof client.createPage).toBe('function');
266
+ expect(typeof client.updatePage).toBe('function');
267
+ expect(typeof client.getPageForEdit).toBe('function');
268
+ expect(typeof client.createChildPage).toBe('function');
269
+ expect(typeof client.findPageByTitle).toBe('function');
270
+ expect(typeof client.deletePage).toBe('function');
271
+ });
272
+ });
273
+
274
+ describe('deletePage', () => {
275
+ test('should delete a page by ID', async () => {
276
+ const mock = new MockAdapter(client.client);
277
+ mock.onDelete('/content/123456789').reply(204);
278
+
279
+ await expect(client.deletePage('123456789')).resolves.toEqual({ id: '123456789' });
280
+
281
+ mock.restore();
282
+ });
283
+
284
+ test('should delete a page by URL', async () => {
285
+ const mock = new MockAdapter(client.client);
286
+ mock.onDelete('/content/987654321').reply(204);
287
+
288
+ await expect(
289
+ client.deletePage('https://test.atlassian.net/wiki/viewpage.action?pageId=987654321')
290
+ ).resolves.toEqual({ id: '987654321' });
291
+
292
+ mock.restore();
293
+ });
294
+ });
295
+
296
+ describe('page tree operations', () => {
297
+ test('should have required methods for tree operations', () => {
298
+ expect(typeof client.getChildPages).toBe('function');
299
+ expect(typeof client.getAllDescendantPages).toBe('function');
300
+ expect(typeof client.copyPageTree).toBe('function');
301
+ expect(typeof client.buildPageTree).toBe('function');
302
+ expect(typeof client.shouldExcludePage).toBe('function');
303
+ });
304
+
305
+ test('should correctly exclude pages based on patterns', () => {
306
+ const patterns = ['temp*', 'test*', '*draft*'];
307
+
308
+ expect(client.shouldExcludePage('temporary document', patterns)).toBe(true);
309
+ expect(client.shouldExcludePage('test page', patterns)).toBe(true);
310
+ expect(client.shouldExcludePage('my draft page', patterns)).toBe(true);
311
+ expect(client.shouldExcludePage('normal document', patterns)).toBe(false);
312
+ expect(client.shouldExcludePage('production page', patterns)).toBe(false);
313
+ });
314
+
315
+ test('should handle empty exclude patterns', () => {
316
+ expect(client.shouldExcludePage('any page', [])).toBe(false);
317
+ expect(client.shouldExcludePage('any page', null)).toBe(false);
318
+ expect(client.shouldExcludePage('any page', undefined)).toBe(false);
319
+ });
320
+
321
+ test('globToRegExp should escape regex metacharacters and match case-insensitively', () => {
322
+ const patterns = [
323
+ 'file.name*', // dot should be literal
324
+ '[draft]?', // brackets should be literal
325
+ 'Plan (Q1)?', // parentheses literal, ? wildcard
326
+ 'DATA*SET', // case-insensitive
327
+ ];
328
+ const rx = patterns.map(p => client.globToRegExp(p));
329
+ expect('file.name.v1').toMatch(rx[0]);
330
+ expect('filexname').not.toMatch(rx[0]);
331
+ expect('[draft]1').toMatch(rx[1]);
332
+ expect('[draft]AB').not.toMatch(rx[1]);
333
+ expect('Plan (Q1)A').toMatch(rx[2]);
334
+ expect('Plan Q1A').not.toMatch(rx[2]);
335
+ expect('data big set').toMatch(rx[3]);
336
+ });
337
+
338
+ test('buildPageTree should link children by parentId and collect orphans at root', () => {
339
+ const rootId = 'root';
340
+ const pages = [
341
+ { id: 'a', title: 'A', parentId: rootId },
342
+ { id: 'b', title: 'B', parentId: 'a' },
343
+ { id: 'c', title: 'C', parentId: 'missing' }, // orphan
344
+ ];
345
+ const tree = client.buildPageTree(pages, rootId);
346
+ // tree should contain A and C at top-level (B is child of A)
347
+ const topTitles = tree.map(n => n.title).sort();
348
+ expect(topTitles).toEqual(['A', 'C']);
349
+ const a = tree.find(n => n.title === 'A');
350
+ expect(a.children.map(n => n.title)).toEqual(['B']);
351
+ });
352
+
353
+ test('exclude parser should tolerate spaces and empty items', () => {
354
+ const raw = ' temp* , , *draft* ,,test? ';
355
+ const patterns = raw.split(',').map(p => p.trim()).filter(Boolean);
356
+ expect(patterns).toEqual(['temp*', '*draft*', 'test?']);
357
+ expect(client.shouldExcludePage('temp file', patterns)).toBe(true);
358
+ expect(client.shouldExcludePage('my draft page', patterns)).toBe(true);
359
+ expect(client.shouldExcludePage('test1', patterns)).toBe(true);
360
+ expect(client.shouldExcludePage('production', patterns)).toBe(false);
361
+ });
362
+ });
363
+
364
+ describe('comments', () => {
365
+ test('should list comments with location filter', async () => {
366
+ const mock = new MockAdapter(client.client);
367
+ mock.onGet('/content/123/child/comment').reply(config => {
368
+ expect(config.params.location).toBe('inline');
369
+ expect(config.params.expand).toContain('body.storage');
370
+ expect(config.params.expand).toContain('ancestors');
371
+ return [200, {
372
+ results: [
373
+ {
374
+ id: 'c1',
375
+ status: 'current',
376
+ body: { storage: { value: '<p>Hello</p>' } },
377
+ history: { createdBy: { displayName: 'Ada' }, createdDate: '2025-01-01' },
378
+ version: { number: 1 },
379
+ ancestors: [{ id: 'c0', type: 'comment' }],
380
+ extensions: {
381
+ location: 'inline',
382
+ inlineProperties: { selection: 'Hello', originalSelection: 'Hello' },
383
+ resolution: { status: 'open' }
384
+ }
385
+ }
386
+ ],
387
+ _links: { next: '/rest/api/content/123/child/comment?start=2' }
388
+ }];
389
+ });
390
+
391
+ const page = await client.listComments('123', { location: 'inline' });
392
+ expect(page.results).toHaveLength(1);
393
+ expect(page.results[0].location).toBe('inline');
394
+ expect(page.results[0].resolution).toBe('open');
395
+ expect(page.results[0].parentId).toBe('c0');
396
+ expect(page.nextStart).toBe(2);
397
+
398
+ mock.restore();
399
+ });
400
+
401
+ test('should create inline comment with inline properties', async () => {
402
+ const mock = new MockAdapter(client.client);
403
+ mock.onPost('/content').reply(config => {
404
+ const payload = JSON.parse(config.data);
405
+ expect(payload.type).toBe('comment');
406
+ expect(payload.container.id).toBe('123');
407
+ expect(payload.body.storage.value).toBe('<p>Hi</p>');
408
+ expect(payload.ancestors[0].id).toBe('c0');
409
+ expect(payload.extensions.location).toBe('inline');
410
+ expect(payload.extensions.inlineProperties.originalSelection).toBe('Hi');
411
+ expect(payload.extensions.inlineProperties.markerRef).toBe('comment-1');
412
+ return [200, { id: 'c1', type: 'comment' }];
413
+ });
414
+
415
+ await client.createComment('123', '<p>Hi</p>', 'storage', {
416
+ parentId: 'c0',
417
+ location: 'inline',
418
+ inlineProperties: {
419
+ selection: 'Hi',
420
+ originalSelection: 'Hi',
421
+ markerRef: 'comment-1'
422
+ }
423
+ });
424
+
425
+ mock.restore();
426
+ });
427
+
428
+ test('should delete a comment by ID', async () => {
429
+ const mock = new MockAdapter(client.client);
430
+ mock.onDelete('/content/456').reply(204);
431
+
432
+ await expect(client.deleteComment('456')).resolves.toEqual({ id: '456' });
433
+
434
+ mock.restore();
435
+ });
436
+ });
437
+
438
+ describe('attachments', () => {
439
+ test('should have required methods for attachment handling', () => {
440
+ expect(typeof client.listAttachments).toBe('function');
441
+ expect(typeof client.getAllAttachments).toBe('function');
442
+ expect(typeof client.downloadAttachment).toBe('function');
443
+ });
444
+
445
+ test('matchesPattern should respect glob patterns', () => {
446
+ expect(client.matchesPattern('report.png', '*.png')).toBe(true);
447
+ expect(client.matchesPattern('report.png', '*.jpg')).toBe(false);
448
+ expect(client.matchesPattern('report.png', ['*.jpg', 'report.*'])).toBe(true);
449
+ expect(client.matchesPattern('report.png', null)).toBe(true);
450
+ expect(client.matchesPattern('report.png', [])).toBe(true);
451
+ });
452
+
453
+ test('parseNextStart should read start query param when present', () => {
454
+ expect(client.parseNextStart('/rest/api/content/1/child/attachment?start=25')).toBe(25);
455
+ expect(client.parseNextStart('/rest/api/content/1/child/attachment?limit=50')).toBeNull();
456
+ expect(client.parseNextStart(null)).toBeNull();
457
+ });
458
+ });
459
+ });