@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,535 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { http, HttpResponse } from 'msw';
3
+ import { Effect } from 'effect';
4
+ import { ConfluenceClient } from '../lib/confluence-client/client.js';
5
+ import { AuthError, FolderNotFoundError, RateLimitError, SpaceNotFoundError } from '../lib/errors.js';
6
+ import { createValidFolder } from './msw-schema-validation.js';
7
+ import { server } from './setup-msw.js';
8
+ import { createValidPage, createValidSpace } from './msw-schema-validation.js';
9
+
10
+ const testConfig = {
11
+ confluenceUrl: 'https://test.atlassian.net',
12
+ email: 'test@example.com',
13
+ apiToken: 'test-token',
14
+ };
15
+
16
+ describe('ConfluenceClient', () => {
17
+ describe('verifyConnection', () => {
18
+ test('succeeds when API returns spaces', async () => {
19
+ const client = new ConfluenceClient(testConfig);
20
+ const result = await client.verifyConnection();
21
+ expect(result).toBe(true);
22
+ });
23
+
24
+ test('throws AuthError on 401', async () => {
25
+ server.use(
26
+ http.get('*/wiki/api/v2/spaces', () => {
27
+ return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
28
+ }),
29
+ );
30
+
31
+ const client = new ConfluenceClient(testConfig);
32
+
33
+ expect(async () => {
34
+ await client.verifyConnection();
35
+ }).toThrow();
36
+ });
37
+
38
+ test('throws AuthError on 403', async () => {
39
+ server.use(
40
+ http.get('*/wiki/api/v2/spaces', () => {
41
+ return HttpResponse.json({ error: 'Forbidden' }, { status: 403 });
42
+ }),
43
+ );
44
+
45
+ const client = new ConfluenceClient(testConfig);
46
+
47
+ expect(async () => {
48
+ await client.verifyConnection();
49
+ }).toThrow();
50
+ });
51
+ });
52
+
53
+ describe('getSpaces', () => {
54
+ test('returns list of spaces', async () => {
55
+ const client = new ConfluenceClient(testConfig);
56
+ const response = await client.getSpaces();
57
+
58
+ expect(response.results).toBeArray();
59
+ expect(response.results.length).toBeGreaterThan(0);
60
+ expect(response.results[0]).toHaveProperty('key');
61
+ expect(response.results[0]).toHaveProperty('name');
62
+ });
63
+
64
+ test('handles spaces with null description', async () => {
65
+ server.use(
66
+ http.get('*/wiki/api/v2/spaces', () => {
67
+ return HttpResponse.json({
68
+ results: [
69
+ {
70
+ id: 'space-null-desc',
71
+ key: 'NULL',
72
+ name: 'Space with null description',
73
+ description: null,
74
+ },
75
+ ],
76
+ });
77
+ }),
78
+ );
79
+
80
+ const client = new ConfluenceClient(testConfig);
81
+ const response = await client.getSpaces();
82
+
83
+ expect(response.results).toBeArray();
84
+ expect(response.results[0].key).toBe('NULL');
85
+ expect(response.results[0].description).toBeNull();
86
+ });
87
+ });
88
+
89
+ describe('getSpacesEffect', () => {
90
+ test('returns spaces with Effect', async () => {
91
+ const client = new ConfluenceClient(testConfig);
92
+ const result = await Effect.runPromise(Effect.either(client.getSpacesEffect()));
93
+
94
+ expect(result._tag).toBe('Right');
95
+ if (result._tag === 'Right') {
96
+ expect(result.right.results).toBeArray();
97
+ }
98
+ });
99
+ });
100
+
101
+ describe('getSpaceByKey', () => {
102
+ test('returns space for valid key', async () => {
103
+ server.use(
104
+ http.get('*/wiki/api/v2/spaces', ({ request }) => {
105
+ const url = new URL(request.url);
106
+ const keys = url.searchParams.get('keys');
107
+ if (keys === 'DOCS') {
108
+ return HttpResponse.json({
109
+ results: [createValidSpace({ key: 'DOCS', name: 'Documentation' })],
110
+ });
111
+ }
112
+ return HttpResponse.json({ results: [] });
113
+ }),
114
+ );
115
+
116
+ const client = new ConfluenceClient(testConfig);
117
+ const space = await client.getSpaceByKey('DOCS');
118
+
119
+ expect(space.key).toBe('DOCS');
120
+ expect(space.name).toBe('Documentation');
121
+ });
122
+
123
+ test('throws SpaceNotFoundError for invalid key', async () => {
124
+ server.use(
125
+ http.get('*/wiki/api/v2/spaces', () => {
126
+ return HttpResponse.json({ results: [] });
127
+ }),
128
+ );
129
+
130
+ const client = new ConfluenceClient(testConfig);
131
+
132
+ expect(async () => {
133
+ await client.getSpaceByKey('INVALID');
134
+ }).toThrow();
135
+ });
136
+ });
137
+
138
+ describe('getSpaceByKeyEffect', () => {
139
+ test('fails with SpaceNotFoundError for missing space', async () => {
140
+ server.use(
141
+ http.get('*/wiki/api/v2/spaces', () => {
142
+ return HttpResponse.json({ results: [] });
143
+ }),
144
+ );
145
+
146
+ const client = new ConfluenceClient(testConfig);
147
+ const result = await Effect.runPromise(Effect.either(client.getSpaceByKeyEffect('MISSING')));
148
+
149
+ expect(result._tag).toBe('Left');
150
+ if (result._tag === 'Left') {
151
+ expect(result.left).toBeInstanceOf(SpaceNotFoundError);
152
+ }
153
+ });
154
+ });
155
+
156
+ describe('getPagesInSpace', () => {
157
+ test('returns pages in space', async () => {
158
+ const client = new ConfluenceClient(testConfig);
159
+ const response = await client.getPagesInSpace('space-123');
160
+
161
+ expect(response.results).toBeArray();
162
+ });
163
+ });
164
+
165
+ describe('getPage', () => {
166
+ test('returns page by ID', async () => {
167
+ const client = new ConfluenceClient(testConfig);
168
+ const page = await client.getPage('page-123');
169
+
170
+ expect(page.id).toBe('page-123');
171
+ expect(page.title).toBeDefined();
172
+ });
173
+ });
174
+
175
+ describe('getAllPagesInSpace', () => {
176
+ test('fetches all pages with pagination', async () => {
177
+ const client = new ConfluenceClient(testConfig);
178
+ const pages = await client.getAllPagesInSpace('space-123');
179
+
180
+ expect(pages).toBeArray();
181
+ });
182
+
183
+ test('filters out archived pages', async () => {
184
+ server.use(
185
+ http.get('*/wiki/api/v2/spaces/:spaceId/pages', () => {
186
+ return HttpResponse.json({
187
+ results: [
188
+ createValidPage({ id: 'page-1', title: 'Current Page 1', spaceId: 'space-123' }),
189
+ {
190
+ ...createValidPage({ id: 'page-2', title: 'Archived Page', spaceId: 'space-123' }),
191
+ status: 'archived',
192
+ },
193
+ createValidPage({ id: 'page-3', title: 'Current Page 2', spaceId: 'space-123' }),
194
+ { ...createValidPage({ id: 'page-4', title: 'Trashed Page', spaceId: 'space-123' }), status: 'trashed' },
195
+ { ...createValidPage({ id: 'page-5', title: 'Draft Page', spaceId: 'space-123' }), status: 'draft' },
196
+ ],
197
+ });
198
+ }),
199
+ );
200
+
201
+ const client = new ConfluenceClient(testConfig);
202
+ const pages = await client.getAllPagesInSpace('space-123');
203
+
204
+ // Only current pages should be returned (filters out archived, trashed, and draft)
205
+ expect(pages).toHaveLength(2);
206
+ expect(pages[0].id).toBe('page-1');
207
+ expect(pages[1].id).toBe('page-3');
208
+ expect(pages.every((p) => p.status === 'current')).toBe(true);
209
+ });
210
+ });
211
+
212
+ describe('getLabels', () => {
213
+ test('returns labels for page', async () => {
214
+ const client = new ConfluenceClient(testConfig);
215
+ const response = await client.getLabels('page-123');
216
+
217
+ expect(response.results).toBeArray();
218
+ });
219
+ });
220
+
221
+ describe('getFolder', () => {
222
+ test('returns folder by ID', async () => {
223
+ const client = new ConfluenceClient(testConfig);
224
+ const folder = await client.getFolder('folder-123');
225
+
226
+ expect(folder.id).toBe('folder-123');
227
+ expect(folder.type).toBe('folder');
228
+ expect(folder.title).toBeDefined();
229
+ });
230
+ });
231
+
232
+ describe('discoverFolders', () => {
233
+ test('discovers folders referenced by pages', async () => {
234
+ server.use(
235
+ http.get('*/wiki/api/v2/folders/:folderId', ({ params }) => {
236
+ return HttpResponse.json({
237
+ id: params.folderId,
238
+ type: 'folder',
239
+ title: `Folder ${params.folderId}`,
240
+ parentId: 'page-1', // Parent is a known page, not another folder
241
+ parentType: 'page',
242
+ });
243
+ }),
244
+ );
245
+
246
+ const client = new ConfluenceClient(testConfig);
247
+ const pages = [
248
+ { id: 'page-1', title: 'Page 1', spaceId: 'space-1', parentId: null },
249
+ { id: 'page-2', title: 'Page 2', spaceId: 'space-1', parentId: 'folder-1' },
250
+ { id: 'page-3', title: 'Page 3', spaceId: 'space-1', parentId: 'page-1' },
251
+ ];
252
+
253
+ const folders = await client.discoverFolders(pages);
254
+
255
+ expect(folders).toHaveLength(1);
256
+ expect(folders[0].id).toBe('folder-1');
257
+ });
258
+
259
+ test('returns empty array when no folders referenced', async () => {
260
+ const client = new ConfluenceClient(testConfig);
261
+ const pages = [
262
+ { id: 'page-1', title: 'Page 1', spaceId: 'space-1', parentId: null },
263
+ { id: 'page-2', title: 'Page 2', spaceId: 'space-1', parentId: 'page-1' },
264
+ ];
265
+
266
+ const folders = await client.discoverFolders(pages);
267
+
268
+ expect(folders).toHaveLength(0);
269
+ });
270
+ });
271
+
272
+ describe('getFolder (404 handling)', () => {
273
+ test('throws FolderNotFoundError on 404', async () => {
274
+ server.use(
275
+ http.get('*/wiki/api/v2/folders/:folderId', () => {
276
+ return HttpResponse.json({ error: 'Not found' }, { status: 404 });
277
+ }),
278
+ );
279
+
280
+ const client = new ConfluenceClient(testConfig);
281
+
282
+ try {
283
+ await client.getFolder('nonexistent-folder');
284
+ expect.unreachable('Should have thrown');
285
+ } catch (error) {
286
+ // Effect wraps errors - check the message
287
+ expect(String(error)).toContain('Folder not found: nonexistent-folder');
288
+ }
289
+ });
290
+ });
291
+
292
+ describe('createFolder', () => {
293
+ test('creates a folder successfully', async () => {
294
+ server.use(
295
+ http.post('*/wiki/api/v2/folders', async ({ request }) => {
296
+ const body = (await request.json()) as { spaceId: string; title: string; parentId?: string };
297
+ const folder = createValidFolder({
298
+ id: 'new-folder-123',
299
+ title: body.title,
300
+ parentId: body.parentId || null,
301
+ });
302
+ return HttpResponse.json(folder);
303
+ }),
304
+ );
305
+
306
+ const client = new ConfluenceClient(testConfig);
307
+ const folder = await client.createFolder({
308
+ spaceId: 'space-123',
309
+ title: 'New Folder',
310
+ });
311
+
312
+ expect(folder.id).toBe('new-folder-123');
313
+ expect(folder.title).toBe('New Folder');
314
+ expect(folder.type).toBe('folder');
315
+ });
316
+
317
+ test('creates a folder with parent', async () => {
318
+ server.use(
319
+ http.post('*/wiki/api/v2/folders', async ({ request }) => {
320
+ const body = (await request.json()) as { spaceId: string; title: string; parentId?: string };
321
+ const folder = createValidFolder({
322
+ id: 'child-folder-123',
323
+ title: body.title,
324
+ parentId: body.parentId || null,
325
+ });
326
+ return HttpResponse.json(folder);
327
+ }),
328
+ );
329
+
330
+ const client = new ConfluenceClient(testConfig);
331
+ const folder = await client.createFolder({
332
+ spaceId: 'space-123',
333
+ title: 'Child Folder',
334
+ parentId: 'parent-folder-123',
335
+ });
336
+
337
+ expect(folder.id).toBe('child-folder-123');
338
+ expect(folder.parentId).toBe('parent-folder-123');
339
+ });
340
+
341
+ test('handles 401 authentication error', async () => {
342
+ server.use(
343
+ http.post('*/wiki/api/v2/folders', () => {
344
+ return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
345
+ }),
346
+ );
347
+
348
+ const client = new ConfluenceClient(testConfig);
349
+
350
+ try {
351
+ await client.createFolder({ spaceId: 'space-123', title: 'Test' });
352
+ expect.unreachable('Should have thrown');
353
+ } catch (error) {
354
+ // Effect wraps errors - check the message
355
+ expect(String(error)).toContain('Invalid credentials');
356
+ }
357
+ });
358
+ });
359
+
360
+ describe('movePage', () => {
361
+ test('moves a page successfully', async () => {
362
+ server.use(
363
+ http.put('*/wiki/rest/api/content/:pageId/move/:position/:targetId', () => {
364
+ // Response body varies and is not validated - just need 200 OK
365
+ return HttpResponse.json({});
366
+ }),
367
+ );
368
+
369
+ const client = new ConfluenceClient(testConfig);
370
+ // movePage returns void - just verify it doesn't throw
371
+ await client.movePage('page-123', 'folder-456', 'append');
372
+ });
373
+
374
+ test('handles 404 when page not found', async () => {
375
+ server.use(
376
+ http.put('*/wiki/rest/api/content/:pageId/move/:position/:targetId', () => {
377
+ return HttpResponse.json({ error: 'Not found' }, { status: 404 });
378
+ }),
379
+ );
380
+
381
+ const client = new ConfluenceClient(testConfig);
382
+
383
+ await expect(client.movePage('nonexistent', 'folder-456')).rejects.toThrow();
384
+ });
385
+
386
+ test('handles 401 authentication error', async () => {
387
+ server.use(
388
+ http.put('*/wiki/rest/api/content/:pageId/move/:position/:targetId', () => {
389
+ return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
390
+ }),
391
+ );
392
+
393
+ const client = new ConfluenceClient(testConfig);
394
+
395
+ try {
396
+ await client.movePage('page-123', 'folder-456');
397
+ expect.unreachable('Should have thrown');
398
+ } catch (error) {
399
+ // Effect wraps errors - check the message
400
+ expect(String(error)).toContain('Invalid credentials');
401
+ }
402
+ });
403
+ });
404
+
405
+ describe('rate limiting', () => {
406
+ test('handles 429 responses', async () => {
407
+ let requestCount = 0;
408
+
409
+ server.use(
410
+ http.get('*/wiki/api/v2/spaces', () => {
411
+ requestCount++;
412
+ if (requestCount < 2) {
413
+ return HttpResponse.json(
414
+ { error: 'Rate limited' },
415
+ {
416
+ status: 429,
417
+ headers: { 'Retry-After': '1' },
418
+ },
419
+ );
420
+ }
421
+ return HttpResponse.json({
422
+ results: [createValidSpace()],
423
+ });
424
+ }),
425
+ );
426
+
427
+ const client = new ConfluenceClient(testConfig);
428
+ const response = await client.getSpaces();
429
+
430
+ expect(response.results).toBeArray();
431
+ expect(requestCount).toBe(2);
432
+ });
433
+ });
434
+
435
+ describe('search', () => {
436
+ test('returns search results', async () => {
437
+ const client = new ConfluenceClient(testConfig);
438
+ const response = await client.search('type=page AND text~"test"');
439
+ expect(response.results).toBeArray();
440
+ });
441
+
442
+ test('returns empty results for folder-type CQL', async () => {
443
+ const client = new ConfluenceClient(testConfig);
444
+ const response = await client.search('type=folder AND space="TEST"');
445
+ expect(response.results).toBeArray();
446
+ expect(response.results.length).toBe(0);
447
+ });
448
+ });
449
+
450
+ describe('getFooterComments', () => {
451
+ test('returns empty comments by default', async () => {
452
+ const client = new ConfluenceClient(testConfig);
453
+ const response = await client.getFooterComments('page-123');
454
+ expect(response.results).toBeArray();
455
+ expect(response.results.length).toBe(0);
456
+ });
457
+
458
+ test('returns comments when present', async () => {
459
+ server.use(
460
+ http.get('*/wiki/api/v2/pages/:pageId/footer-comments', () => {
461
+ return HttpResponse.json({
462
+ results: [
463
+ {
464
+ id: 'comment-1',
465
+ body: { storage: { value: '<p>Test comment</p>', representation: 'storage' } },
466
+ authorId: 'user-123',
467
+ createdAt: new Date().toISOString(),
468
+ },
469
+ ],
470
+ });
471
+ }),
472
+ );
473
+
474
+ const client = new ConfluenceClient(testConfig);
475
+ const response = await client.getFooterComments('page-123');
476
+ expect(response.results.length).toBe(1);
477
+ expect(response.results[0].id).toBe('comment-1');
478
+ });
479
+ });
480
+
481
+ describe('getAttachments', () => {
482
+ test('returns empty attachments by default', async () => {
483
+ const client = new ConfluenceClient(testConfig);
484
+ const response = await client.getAttachments('page-123');
485
+ expect(response.results).toBeArray();
486
+ expect(response.results.length).toBe(0);
487
+ });
488
+ });
489
+
490
+ describe('addLabel', () => {
491
+ test('adds label without throwing', async () => {
492
+ const client = new ConfluenceClient(testConfig);
493
+ await client.addLabel('page-123', 'documentation');
494
+ });
495
+ });
496
+
497
+ describe('removeLabel', () => {
498
+ test('removes label without throwing', async () => {
499
+ const client = new ConfluenceClient(testConfig);
500
+ await client.removeLabel('page-123', 'documentation');
501
+ });
502
+ });
503
+
504
+ describe('deletePage', () => {
505
+ test('deletes page without throwing', async () => {
506
+ const client = new ConfluenceClient(testConfig);
507
+ await client.deletePage('page-123');
508
+ });
509
+
510
+ test('throws on 404', async () => {
511
+ server.use(
512
+ http.delete('*/wiki/api/v2/pages/:pageId', () => {
513
+ return HttpResponse.json({ error: 'Not found' }, { status: 404 });
514
+ }),
515
+ );
516
+
517
+ const client = new ConfluenceClient(testConfig);
518
+ await expect(client.deletePage('nonexistent')).rejects.toThrow();
519
+ });
520
+ });
521
+
522
+ describe('uploadAttachment', () => {
523
+ test('uploads attachment without throwing', async () => {
524
+ const client = new ConfluenceClient(testConfig);
525
+ await client.uploadAttachment('page-123', 'test.png', Buffer.from('data'), 'image/png');
526
+ });
527
+ });
528
+
529
+ describe('deleteAttachment', () => {
530
+ test('deletes attachment without throwing', async () => {
531
+ const client = new ConfluenceClient(testConfig);
532
+ await client.deleteAttachment('att-123');
533
+ });
534
+ });
535
+ });
@@ -0,0 +1,39 @@
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 { server } from './setup-msw.js';
5
+
6
+ const testConfig = {
7
+ confluenceUrl: 'https://test.atlassian.net',
8
+ email: 'test@example.com',
9
+ apiToken: 'test-token',
10
+ };
11
+
12
+ describe('ConfluenceClient - deletePage', () => {
13
+ test('deletes page successfully (204)', async () => {
14
+ const client = new ConfluenceClient(testConfig);
15
+ await client.deletePage('page-123');
16
+ });
17
+
18
+ test('throws on 404', async () => {
19
+ server.use(
20
+ http.delete('*/wiki/api/v2/pages/:pageId', () => {
21
+ return HttpResponse.json({ error: 'Not found' }, { status: 404 });
22
+ }),
23
+ );
24
+
25
+ const client = new ConfluenceClient(testConfig);
26
+ await expect(client.deletePage('nonexistent')).rejects.toThrow();
27
+ });
28
+
29
+ test('throws on 401', async () => {
30
+ server.use(
31
+ http.delete('*/wiki/api/v2/pages/:pageId', () => {
32
+ return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
33
+ }),
34
+ );
35
+
36
+ const client = new ConfluenceClient(testConfig);
37
+ await expect(client.deletePage('page-123')).rejects.toThrow();
38
+ });
39
+ });