@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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/package.json +73 -0
  4. package/src/cli/commands/attachments.ts +113 -0
  5. package/src/cli/commands/clone.ts +188 -0
  6. package/src/cli/commands/comments.ts +56 -0
  7. package/src/cli/commands/create.ts +58 -0
  8. package/src/cli/commands/delete.ts +46 -0
  9. package/src/cli/commands/doctor.ts +161 -0
  10. package/src/cli/commands/duplicate-check.ts +89 -0
  11. package/src/cli/commands/file-rename.ts +113 -0
  12. package/src/cli/commands/folder-hierarchy.ts +241 -0
  13. package/src/cli/commands/info.ts +56 -0
  14. package/src/cli/commands/labels.ts +53 -0
  15. package/src/cli/commands/move.ts +23 -0
  16. package/src/cli/commands/open.ts +145 -0
  17. package/src/cli/commands/pull.ts +241 -0
  18. package/src/cli/commands/push-errors.ts +40 -0
  19. package/src/cli/commands/push.ts +699 -0
  20. package/src/cli/commands/search.ts +62 -0
  21. package/src/cli/commands/setup.ts +124 -0
  22. package/src/cli/commands/spaces.ts +42 -0
  23. package/src/cli/commands/status.ts +88 -0
  24. package/src/cli/commands/tree.ts +190 -0
  25. package/src/cli/help.ts +425 -0
  26. package/src/cli/index.ts +413 -0
  27. package/src/cli/utils/browser.ts +34 -0
  28. package/src/cli/utils/progress-reporter.ts +49 -0
  29. package/src/cli.ts +6 -0
  30. package/src/lib/config.ts +156 -0
  31. package/src/lib/confluence-client/attachment-operations.ts +221 -0
  32. package/src/lib/confluence-client/client.ts +653 -0
  33. package/src/lib/confluence-client/comment-operations.ts +60 -0
  34. package/src/lib/confluence-client/folder-operations.ts +203 -0
  35. package/src/lib/confluence-client/index.ts +47 -0
  36. package/src/lib/confluence-client/label-operations.ts +102 -0
  37. package/src/lib/confluence-client/page-operations.ts +270 -0
  38. package/src/lib/confluence-client/search-operations.ts +60 -0
  39. package/src/lib/confluence-client/types.ts +329 -0
  40. package/src/lib/confluence-client/user-operations.ts +58 -0
  41. package/src/lib/dependency-sorter.ts +233 -0
  42. package/src/lib/errors.ts +237 -0
  43. package/src/lib/file-scanner.ts +195 -0
  44. package/src/lib/formatters.ts +314 -0
  45. package/src/lib/health-check.ts +204 -0
  46. package/src/lib/markdown/converter.ts +427 -0
  47. package/src/lib/markdown/frontmatter.ts +116 -0
  48. package/src/lib/markdown/html-converter.ts +398 -0
  49. package/src/lib/markdown/index.ts +21 -0
  50. package/src/lib/markdown/link-converter.ts +189 -0
  51. package/src/lib/markdown/reference-updater.ts +251 -0
  52. package/src/lib/markdown/slugify.ts +32 -0
  53. package/src/lib/page-state.ts +195 -0
  54. package/src/lib/resolve-page-target.ts +33 -0
  55. package/src/lib/space-config.ts +264 -0
  56. package/src/lib/sync/cleanup.ts +50 -0
  57. package/src/lib/sync/folder-path.ts +61 -0
  58. package/src/lib/sync/index.ts +2 -0
  59. package/src/lib/sync/link-resolution-pass.ts +139 -0
  60. package/src/lib/sync/sync-engine.ts +681 -0
  61. package/src/lib/sync/sync-specific.ts +221 -0
  62. package/src/lib/sync/types.ts +42 -0
  63. package/src/test/attachments.test.ts +68 -0
  64. package/src/test/clone.test.ts +373 -0
  65. package/src/test/comments.test.ts +53 -0
  66. package/src/test/config.test.ts +209 -0
  67. package/src/test/confluence-client.test.ts +535 -0
  68. package/src/test/delete.test.ts +39 -0
  69. package/src/test/dependency-sorter.test.ts +384 -0
  70. package/src/test/errors.test.ts +199 -0
  71. package/src/test/file-rename.test.ts +305 -0
  72. package/src/test/file-scanner.test.ts +331 -0
  73. package/src/test/folder-hierarchy.test.ts +337 -0
  74. package/src/test/formatters.test.ts +213 -0
  75. package/src/test/html-converter.test.ts +399 -0
  76. package/src/test/info.test.ts +56 -0
  77. package/src/test/labels.test.ts +70 -0
  78. package/src/test/link-conversion-integration.test.ts +189 -0
  79. package/src/test/link-converter.test.ts +413 -0
  80. package/src/test/link-resolution-pass.test.ts +368 -0
  81. package/src/test/markdown.test.ts +443 -0
  82. package/src/test/mocks/handlers.ts +228 -0
  83. package/src/test/move.test.ts +53 -0
  84. package/src/test/msw-schema-validation.ts +151 -0
  85. package/src/test/page-state.test.ts +542 -0
  86. package/src/test/push.test.ts +551 -0
  87. package/src/test/reference-updater.test.ts +293 -0
  88. package/src/test/resolve-page-target.test.ts +55 -0
  89. package/src/test/search.test.ts +64 -0
  90. package/src/test/setup-msw.ts +75 -0
  91. package/src/test/space-config.test.ts +516 -0
  92. package/src/test/spaces.test.ts +53 -0
  93. package/src/test/sync-engine.test.ts +486 -0
  94. 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
+ });