@aaronshaf/confluence-cli 0.1.15
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/LICENSE +21 -0
- package/README.md +69 -0
- package/package.json +73 -0
- package/src/cli/commands/attachments.ts +113 -0
- package/src/cli/commands/clone.ts +188 -0
- package/src/cli/commands/comments.ts +56 -0
- package/src/cli/commands/create.ts +58 -0
- package/src/cli/commands/delete.ts +46 -0
- package/src/cli/commands/doctor.ts +161 -0
- package/src/cli/commands/duplicate-check.ts +89 -0
- package/src/cli/commands/file-rename.ts +113 -0
- package/src/cli/commands/folder-hierarchy.ts +241 -0
- package/src/cli/commands/info.ts +56 -0
- package/src/cli/commands/labels.ts +53 -0
- package/src/cli/commands/move.ts +23 -0
- package/src/cli/commands/open.ts +145 -0
- package/src/cli/commands/pull.ts +241 -0
- package/src/cli/commands/push-errors.ts +40 -0
- package/src/cli/commands/push.ts +699 -0
- package/src/cli/commands/search.ts +62 -0
- package/src/cli/commands/setup.ts +124 -0
- package/src/cli/commands/spaces.ts +42 -0
- package/src/cli/commands/status.ts +88 -0
- package/src/cli/commands/tree.ts +190 -0
- package/src/cli/help.ts +425 -0
- package/src/cli/index.ts +413 -0
- package/src/cli/utils/browser.ts +34 -0
- package/src/cli/utils/progress-reporter.ts +49 -0
- package/src/cli.ts +6 -0
- package/src/lib/config.ts +156 -0
- package/src/lib/confluence-client/attachment-operations.ts +221 -0
- package/src/lib/confluence-client/client.ts +653 -0
- package/src/lib/confluence-client/comment-operations.ts +60 -0
- package/src/lib/confluence-client/folder-operations.ts +203 -0
- package/src/lib/confluence-client/index.ts +47 -0
- package/src/lib/confluence-client/label-operations.ts +102 -0
- package/src/lib/confluence-client/page-operations.ts +270 -0
- package/src/lib/confluence-client/search-operations.ts +60 -0
- package/src/lib/confluence-client/types.ts +329 -0
- package/src/lib/confluence-client/user-operations.ts +58 -0
- package/src/lib/dependency-sorter.ts +233 -0
- package/src/lib/errors.ts +237 -0
- package/src/lib/file-scanner.ts +195 -0
- package/src/lib/formatters.ts +314 -0
- package/src/lib/health-check.ts +204 -0
- package/src/lib/markdown/converter.ts +427 -0
- package/src/lib/markdown/frontmatter.ts +116 -0
- package/src/lib/markdown/html-converter.ts +398 -0
- package/src/lib/markdown/index.ts +21 -0
- package/src/lib/markdown/link-converter.ts +189 -0
- package/src/lib/markdown/reference-updater.ts +251 -0
- package/src/lib/markdown/slugify.ts +32 -0
- package/src/lib/page-state.ts +195 -0
- package/src/lib/resolve-page-target.ts +33 -0
- package/src/lib/space-config.ts +264 -0
- package/src/lib/sync/cleanup.ts +50 -0
- package/src/lib/sync/folder-path.ts +61 -0
- package/src/lib/sync/index.ts +2 -0
- package/src/lib/sync/link-resolution-pass.ts +139 -0
- package/src/lib/sync/sync-engine.ts +681 -0
- package/src/lib/sync/sync-specific.ts +221 -0
- package/src/lib/sync/types.ts +42 -0
- package/src/test/attachments.test.ts +68 -0
- package/src/test/clone.test.ts +373 -0
- package/src/test/comments.test.ts +53 -0
- package/src/test/config.test.ts +209 -0
- package/src/test/confluence-client.test.ts +535 -0
- package/src/test/delete.test.ts +39 -0
- package/src/test/dependency-sorter.test.ts +384 -0
- package/src/test/errors.test.ts +199 -0
- package/src/test/file-rename.test.ts +305 -0
- package/src/test/file-scanner.test.ts +331 -0
- package/src/test/folder-hierarchy.test.ts +337 -0
- package/src/test/formatters.test.ts +213 -0
- package/src/test/html-converter.test.ts +399 -0
- package/src/test/info.test.ts +56 -0
- package/src/test/labels.test.ts +70 -0
- package/src/test/link-conversion-integration.test.ts +189 -0
- package/src/test/link-converter.test.ts +413 -0
- package/src/test/link-resolution-pass.test.ts +368 -0
- package/src/test/markdown.test.ts +443 -0
- package/src/test/mocks/handlers.ts +228 -0
- package/src/test/move.test.ts +53 -0
- package/src/test/msw-schema-validation.ts +151 -0
- package/src/test/page-state.test.ts +542 -0
- package/src/test/push.test.ts +551 -0
- package/src/test/reference-updater.test.ts +293 -0
- package/src/test/resolve-page-target.test.ts +55 -0
- package/src/test/search.test.ts +64 -0
- package/src/test/setup-msw.ts +75 -0
- package/src/test/space-config.test.ts +516 -0
- package/src/test/spaces.test.ts +53 -0
- package/src/test/sync-engine.test.ts +486 -0
- package/src/types/turndown-plugin-gfm.d.ts +9 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { HttpResponse, http } from 'msw';
|
|
3
|
+
import { ConfluenceClient } from '../lib/confluence-client/client.js';
|
|
4
|
+
import { ApiError, FolderNotFoundError, PageNotFoundError, VersionConflictError } from '../lib/errors.js';
|
|
5
|
+
import { server } from './setup-msw.js';
|
|
6
|
+
import { createValidFolder, createValidPage } from './msw-schema-validation.js';
|
|
7
|
+
|
|
8
|
+
const testConfig = {
|
|
9
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
10
|
+
email: 'test@example.com',
|
|
11
|
+
apiToken: 'test-token',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('ConfluenceClient - Push Operations', () => {
|
|
15
|
+
describe('createPage', () => {
|
|
16
|
+
test('creates a new page successfully', async () => {
|
|
17
|
+
const newPage = createValidPage({
|
|
18
|
+
id: 'new-page-123',
|
|
19
|
+
title: 'New Test Page',
|
|
20
|
+
spaceId: 'space-1',
|
|
21
|
+
version: 1,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
server.use(
|
|
25
|
+
http.post('*/wiki/api/v2/pages', async ({ request }) => {
|
|
26
|
+
const body = await request.json();
|
|
27
|
+
expect(body).toHaveProperty('title');
|
|
28
|
+
expect(body).toHaveProperty('spaceId');
|
|
29
|
+
expect(body).toHaveProperty('body');
|
|
30
|
+
return HttpResponse.json(newPage);
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const client = new ConfluenceClient(testConfig);
|
|
35
|
+
const result = await client.createPage({
|
|
36
|
+
spaceId: 'space-1',
|
|
37
|
+
status: 'current',
|
|
38
|
+
title: 'New Test Page',
|
|
39
|
+
body: {
|
|
40
|
+
representation: 'storage',
|
|
41
|
+
value: '<p>Test content</p>',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.id).toBe('new-page-123');
|
|
46
|
+
expect(result.title).toBe('New Test Page');
|
|
47
|
+
expect(result.version?.number).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('creates a page with parent ID', async () => {
|
|
51
|
+
const newPage = createValidPage({
|
|
52
|
+
id: 'child-page-456',
|
|
53
|
+
title: 'Child Page',
|
|
54
|
+
spaceId: 'space-1',
|
|
55
|
+
parentId: 'parent-123',
|
|
56
|
+
version: 1,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
server.use(
|
|
60
|
+
http.post('*/wiki/api/v2/pages', async ({ request }) => {
|
|
61
|
+
const body = await request.json();
|
|
62
|
+
expect(body).toHaveProperty('parentId', 'parent-123');
|
|
63
|
+
return HttpResponse.json(newPage);
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const client = new ConfluenceClient(testConfig);
|
|
68
|
+
const result = await client.createPage({
|
|
69
|
+
spaceId: 'space-1',
|
|
70
|
+
status: 'current',
|
|
71
|
+
title: 'Child Page',
|
|
72
|
+
parentId: 'parent-123',
|
|
73
|
+
body: {
|
|
74
|
+
representation: 'storage',
|
|
75
|
+
value: '<p>Child content</p>',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result.parentId).toBe('parent-123');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('handles 401 authentication error', async () => {
|
|
83
|
+
server.use(
|
|
84
|
+
http.post('*/wiki/api/v2/pages', () => {
|
|
85
|
+
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const client = new ConfluenceClient(testConfig);
|
|
90
|
+
|
|
91
|
+
expect(async () => {
|
|
92
|
+
await client.createPage({
|
|
93
|
+
spaceId: 'space-1',
|
|
94
|
+
status: 'current',
|
|
95
|
+
title: 'Test',
|
|
96
|
+
body: { representation: 'storage', value: '<p>Test</p>' },
|
|
97
|
+
});
|
|
98
|
+
}).toThrow();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('handles 403 permission error', async () => {
|
|
102
|
+
server.use(
|
|
103
|
+
http.post('*/wiki/api/v2/pages', () => {
|
|
104
|
+
return HttpResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
105
|
+
}),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const client = new ConfluenceClient(testConfig);
|
|
109
|
+
|
|
110
|
+
expect(async () => {
|
|
111
|
+
await client.createPage({
|
|
112
|
+
spaceId: 'space-1',
|
|
113
|
+
status: 'current',
|
|
114
|
+
title: 'Test',
|
|
115
|
+
body: { representation: 'storage', value: '<p>Test</p>' },
|
|
116
|
+
});
|
|
117
|
+
}).toThrow();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('updatePage', () => {
|
|
122
|
+
test('updates an existing page successfully', async () => {
|
|
123
|
+
const updatedPage = createValidPage({
|
|
124
|
+
id: 'page-123',
|
|
125
|
+
title: 'Updated Title',
|
|
126
|
+
version: 3,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
server.use(
|
|
130
|
+
http.put('*/wiki/api/v2/pages/page-123', async ({ request }) => {
|
|
131
|
+
const body = (await request.json()) as any;
|
|
132
|
+
expect(body).toHaveProperty('title', 'Updated Title');
|
|
133
|
+
expect(body).toHaveProperty('version');
|
|
134
|
+
expect(body.version.number).toBe(3);
|
|
135
|
+
return HttpResponse.json(updatedPage);
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const client = new ConfluenceClient(testConfig);
|
|
140
|
+
const result = await client.updatePage({
|
|
141
|
+
id: 'page-123',
|
|
142
|
+
status: 'current',
|
|
143
|
+
title: 'Updated Title',
|
|
144
|
+
body: {
|
|
145
|
+
representation: 'storage',
|
|
146
|
+
value: '<p>Updated content</p>',
|
|
147
|
+
},
|
|
148
|
+
version: { number: 3 },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(result.title).toBe('Updated Title');
|
|
152
|
+
expect(result.version?.number).toBe(3);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('throws PageNotFoundError on 404', async () => {
|
|
156
|
+
server.use(
|
|
157
|
+
http.put('*/wiki/api/v2/pages/missing-page', () => {
|
|
158
|
+
return HttpResponse.json({ error: 'Not Found' }, { status: 404 });
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const client = new ConfluenceClient(testConfig);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await client.updatePage({
|
|
166
|
+
id: 'missing-page',
|
|
167
|
+
status: 'current',
|
|
168
|
+
title: 'Test',
|
|
169
|
+
body: { representation: 'storage', value: '<p>Test</p>' },
|
|
170
|
+
version: { number: 2 },
|
|
171
|
+
});
|
|
172
|
+
expect(false).toBe(true); // Should not reach here
|
|
173
|
+
} catch (error: any) {
|
|
174
|
+
// Effect wraps errors, so we need to check the cause
|
|
175
|
+
expect(error.message).toContain('Page not found');
|
|
176
|
+
expect(error.message).toContain('missing-page');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('throws VersionConflictError on 409', async () => {
|
|
181
|
+
server.use(
|
|
182
|
+
http.put('*/wiki/api/v2/pages/page-123', () => {
|
|
183
|
+
return HttpResponse.json({ version: { number: 5 } }, { status: 409 });
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const client = new ConfluenceClient(testConfig);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await client.updatePage({
|
|
191
|
+
id: 'page-123',
|
|
192
|
+
status: 'current',
|
|
193
|
+
title: 'Test',
|
|
194
|
+
body: { representation: 'storage', value: '<p>Test</p>' },
|
|
195
|
+
version: { number: 3 },
|
|
196
|
+
});
|
|
197
|
+
expect(false).toBe(true); // Should not reach here
|
|
198
|
+
} catch (error: any) {
|
|
199
|
+
// Effect wraps errors, check the message
|
|
200
|
+
expect(error.message).toContain('Version conflict');
|
|
201
|
+
expect(error.message).toContain('3');
|
|
202
|
+
expect(error.message).toContain('5');
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('handles version conflict with missing remote version', async () => {
|
|
207
|
+
server.use(
|
|
208
|
+
http.put('*/wiki/api/v2/pages/page-123', () => {
|
|
209
|
+
return HttpResponse.json({}, { status: 409 });
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const client = new ConfluenceClient(testConfig);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await client.updatePage({
|
|
217
|
+
id: 'page-123',
|
|
218
|
+
status: 'current',
|
|
219
|
+
title: 'Test',
|
|
220
|
+
body: { representation: 'storage', value: '<p>Test</p>' },
|
|
221
|
+
version: { number: 3 },
|
|
222
|
+
});
|
|
223
|
+
expect(false).toBe(true); // Should not reach here
|
|
224
|
+
} catch (error: any) {
|
|
225
|
+
// Effect wraps errors, check the message
|
|
226
|
+
expect(error.message).toContain('Version conflict');
|
|
227
|
+
expect(error.message).toContain('3');
|
|
228
|
+
expect(error.message).toContain('0');
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('handles 401 authentication error', async () => {
|
|
233
|
+
server.use(
|
|
234
|
+
http.put('*/wiki/api/v2/pages/page-123', () => {
|
|
235
|
+
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const client = new ConfluenceClient(testConfig);
|
|
240
|
+
|
|
241
|
+
expect(async () => {
|
|
242
|
+
await client.updatePage({
|
|
243
|
+
id: 'page-123',
|
|
244
|
+
status: 'current',
|
|
245
|
+
title: 'Test',
|
|
246
|
+
body: { representation: 'storage', value: '<p>Test</p>' },
|
|
247
|
+
version: { number: 2 },
|
|
248
|
+
});
|
|
249
|
+
}).toThrow();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('handles 403 permission error', async () => {
|
|
253
|
+
server.use(
|
|
254
|
+
http.put('*/wiki/api/v2/pages/page-123', () => {
|
|
255
|
+
return HttpResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const client = new ConfluenceClient(testConfig);
|
|
260
|
+
|
|
261
|
+
expect(async () => {
|
|
262
|
+
await client.updatePage({
|
|
263
|
+
id: 'page-123',
|
|
264
|
+
status: 'current',
|
|
265
|
+
title: 'Test',
|
|
266
|
+
body: { representation: 'storage', value: '<p>Test</p>' },
|
|
267
|
+
version: { number: 2 },
|
|
268
|
+
});
|
|
269
|
+
}).toThrow();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('Parent validation', () => {
|
|
274
|
+
test('validates parent exists before creating page', async () => {
|
|
275
|
+
// First call: check parent exists
|
|
276
|
+
server.use(
|
|
277
|
+
http.get('*/wiki/api/v2/pages/parent-123', () => {
|
|
278
|
+
return HttpResponse.json(createValidPage({ id: 'parent-123', title: 'Parent Page' }));
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const client = new ConfluenceClient(testConfig);
|
|
283
|
+
const parent = await client.getPage('parent-123', false);
|
|
284
|
+
expect(parent.id).toBe('parent-123');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('throws ApiError when parent does not exist', async () => {
|
|
288
|
+
server.use(
|
|
289
|
+
http.get('*/wiki/api/v2/pages/missing-parent', () => {
|
|
290
|
+
return HttpResponse.json({ error: 'Not Found' }, { status: 404 });
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const client = new ConfluenceClient(testConfig);
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
await client.getPage('missing-parent', false);
|
|
298
|
+
expect(false).toBe(true); // Should not reach here
|
|
299
|
+
} catch (error: any) {
|
|
300
|
+
// getPage throws ApiError for 404, not PageNotFoundError
|
|
301
|
+
expect(error.message).toContain('404');
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Content Properties', () => {
|
|
307
|
+
test('sets content property on a page', async () => {
|
|
308
|
+
server.use(
|
|
309
|
+
http.post('*/wiki/api/v2/pages/page-123/properties', async ({ request }) => {
|
|
310
|
+
const body = (await request.json()) as any;
|
|
311
|
+
expect(body).toHaveProperty('key', 'editor');
|
|
312
|
+
expect(body).toHaveProperty('value', 'v2');
|
|
313
|
+
return HttpResponse.json({ key: 'editor', value: 'v2' }, { status: 200 });
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const client = new ConfluenceClient(testConfig);
|
|
318
|
+
await client.setContentProperty('page-123', 'editor', 'v2');
|
|
319
|
+
// No error means success
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('setEditorV2 sets editor property to v2', async () => {
|
|
323
|
+
server.use(
|
|
324
|
+
http.post('*/wiki/api/v2/pages/page-456/properties', async ({ request }) => {
|
|
325
|
+
const body = (await request.json()) as any;
|
|
326
|
+
expect(body).toHaveProperty('key', 'editor');
|
|
327
|
+
expect(body).toHaveProperty('value', 'v2');
|
|
328
|
+
return HttpResponse.json({ key: 'editor', value: 'v2' }, { status: 200 });
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const client = new ConfluenceClient(testConfig);
|
|
333
|
+
await client.setEditorV2('page-456');
|
|
334
|
+
// No error means success
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('handles 401 authentication error for content property', async () => {
|
|
338
|
+
server.use(
|
|
339
|
+
http.post('*/wiki/api/v2/pages/page-123/properties', () => {
|
|
340
|
+
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const client = new ConfluenceClient(testConfig);
|
|
345
|
+
|
|
346
|
+
expect(async () => {
|
|
347
|
+
await client.setContentProperty('page-123', 'editor', 'v2');
|
|
348
|
+
}).toThrow();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('handles 403 permission error for content property', async () => {
|
|
352
|
+
server.use(
|
|
353
|
+
http.post('*/wiki/api/v2/pages/page-123/properties', () => {
|
|
354
|
+
return HttpResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const client = new ConfluenceClient(testConfig);
|
|
359
|
+
|
|
360
|
+
expect(async () => {
|
|
361
|
+
await client.setContentProperty('page-123', 'editor', 'v2');
|
|
362
|
+
}).toThrow();
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('movePage', () => {
|
|
367
|
+
test('moves a page to a folder successfully', async () => {
|
|
368
|
+
server.use(
|
|
369
|
+
http.put('*/wiki/rest/api/content/page-123/move/append/folder-456', () => {
|
|
370
|
+
// Response body varies and is not validated - just need 200 OK
|
|
371
|
+
return HttpResponse.json({});
|
|
372
|
+
}),
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const client = new ConfluenceClient(testConfig);
|
|
376
|
+
// movePage returns void - just verify it doesn't throw
|
|
377
|
+
await client.movePage('page-123', 'folder-456', 'append');
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test('throws on 404 when page not found', async () => {
|
|
381
|
+
server.use(
|
|
382
|
+
http.put('*/wiki/rest/api/content/missing-page/move/append/folder-456', () => {
|
|
383
|
+
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const client = new ConfluenceClient(testConfig);
|
|
388
|
+
|
|
389
|
+
await expect(client.movePage('missing-page', 'folder-456')).rejects.toThrow();
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('throws on 404 when target folder not found', async () => {
|
|
393
|
+
server.use(
|
|
394
|
+
http.put('*/wiki/rest/api/content/page-123/move/append/missing-folder', () => {
|
|
395
|
+
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
|
|
396
|
+
}),
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const client = new ConfluenceClient(testConfig);
|
|
400
|
+
|
|
401
|
+
await expect(client.movePage('page-123', 'missing-folder')).rejects.toThrow();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('throws on 403 permission denied', async () => {
|
|
405
|
+
server.use(
|
|
406
|
+
http.put('*/wiki/rest/api/content/page-123/move/append/folder-456', () => {
|
|
407
|
+
return HttpResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
408
|
+
}),
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const client = new ConfluenceClient(testConfig);
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
await client.movePage('page-123', 'folder-456');
|
|
415
|
+
expect.unreachable('Should have thrown');
|
|
416
|
+
} catch (error) {
|
|
417
|
+
expect(String(error)).toContain('Access denied');
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test('handles 401 authentication error', async () => {
|
|
422
|
+
server.use(
|
|
423
|
+
http.put('*/wiki/rest/api/content/page-123/move/append/folder-456', () => {
|
|
424
|
+
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const client = new ConfluenceClient(testConfig);
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
await client.movePage('page-123', 'folder-456');
|
|
432
|
+
expect.unreachable('Should have thrown');
|
|
433
|
+
} catch (error) {
|
|
434
|
+
expect(String(error)).toContain('Invalid credentials');
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe('createFolder', () => {
|
|
440
|
+
test('creates a folder successfully', async () => {
|
|
441
|
+
server.use(
|
|
442
|
+
http.post('*/wiki/api/v2/folders', async ({ request }) => {
|
|
443
|
+
const body = (await request.json()) as { spaceId: string; title: string; parentId?: string };
|
|
444
|
+
const folder = createValidFolder({
|
|
445
|
+
id: 'new-folder-123',
|
|
446
|
+
title: body.title,
|
|
447
|
+
parentId: body.parentId || null,
|
|
448
|
+
});
|
|
449
|
+
return HttpResponse.json(folder);
|
|
450
|
+
}),
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const client = new ConfluenceClient(testConfig);
|
|
454
|
+
const folder = await client.createFolder({
|
|
455
|
+
spaceId: 'space-123',
|
|
456
|
+
title: 'New Folder',
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
expect(folder.id).toBe('new-folder-123');
|
|
460
|
+
expect(folder.title).toBe('New Folder');
|
|
461
|
+
expect(folder.type).toBe('folder');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('creates a nested folder with parent', async () => {
|
|
465
|
+
server.use(
|
|
466
|
+
http.post('*/wiki/api/v2/folders', async ({ request }) => {
|
|
467
|
+
const body = (await request.json()) as { spaceId: string; title: string; parentId?: string };
|
|
468
|
+
const folder = createValidFolder({
|
|
469
|
+
id: 'child-folder-456',
|
|
470
|
+
title: body.title,
|
|
471
|
+
parentId: body.parentId || null,
|
|
472
|
+
});
|
|
473
|
+
return HttpResponse.json(folder);
|
|
474
|
+
}),
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const client = new ConfluenceClient(testConfig);
|
|
478
|
+
const folder = await client.createFolder({
|
|
479
|
+
spaceId: 'space-123',
|
|
480
|
+
title: 'Child Folder',
|
|
481
|
+
parentId: 'parent-folder-123',
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
expect(folder.id).toBe('child-folder-456');
|
|
485
|
+
expect(folder.parentId).toBe('parent-folder-123');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test('handles 400 duplicate folder error', async () => {
|
|
489
|
+
server.use(
|
|
490
|
+
http.post('*/wiki/api/v2/folders', () => {
|
|
491
|
+
return HttpResponse.json({ message: 'A folder with this name already exists' }, { status: 400 });
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
const client = new ConfluenceClient(testConfig);
|
|
496
|
+
|
|
497
|
+
await expect(client.createFolder({ spaceId: 'space-123', title: 'Existing Folder' })).rejects.toThrow();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test('handles 409 conflict error', async () => {
|
|
501
|
+
server.use(
|
|
502
|
+
http.post('*/wiki/api/v2/folders', () => {
|
|
503
|
+
return HttpResponse.json({ message: 'Folder already exists' }, { status: 409 });
|
|
504
|
+
}),
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
const client = new ConfluenceClient(testConfig);
|
|
508
|
+
|
|
509
|
+
await expect(client.createFolder({ spaceId: 'space-123', title: 'Existing Folder' })).rejects.toThrow();
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe('getFolder with retry', () => {
|
|
514
|
+
test('retries on 429 rate limit', async () => {
|
|
515
|
+
let requestCount = 0;
|
|
516
|
+
|
|
517
|
+
server.use(
|
|
518
|
+
http.get('*/wiki/api/v2/folders/folder-123', () => {
|
|
519
|
+
requestCount++;
|
|
520
|
+
if (requestCount < 2) {
|
|
521
|
+
return HttpResponse.json({ error: 'Rate limited' }, { status: 429, headers: { 'Retry-After': '1' } });
|
|
522
|
+
}
|
|
523
|
+
return HttpResponse.json(createValidFolder({ id: 'folder-123', title: 'Test Folder' }));
|
|
524
|
+
}),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const client = new ConfluenceClient(testConfig);
|
|
528
|
+
const folder = await client.getFolder('folder-123');
|
|
529
|
+
|
|
530
|
+
expect(folder.id).toBe('folder-123');
|
|
531
|
+
expect(requestCount).toBe(2);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test('throws FolderNotFoundError on 404', async () => {
|
|
535
|
+
server.use(
|
|
536
|
+
http.get('*/wiki/api/v2/folders/nonexistent', () => {
|
|
537
|
+
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
|
|
538
|
+
}),
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
const client = new ConfluenceClient(testConfig);
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
await client.getFolder('nonexistent');
|
|
545
|
+
expect.unreachable('Should have thrown');
|
|
546
|
+
} catch (error) {
|
|
547
|
+
expect(String(error)).toContain('Folder not found: nonexistent');
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
});
|