@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,516 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import {
|
|
6
|
+
createSpaceConfig,
|
|
7
|
+
readSpaceConfig,
|
|
8
|
+
writeSpaceConfig,
|
|
9
|
+
hasSpaceConfig,
|
|
10
|
+
updateLastSync,
|
|
11
|
+
updatePageSyncInfo,
|
|
12
|
+
removePageSyncInfo,
|
|
13
|
+
getTrackedPageIds,
|
|
14
|
+
updateFolderSyncInfo,
|
|
15
|
+
getFolderByPath,
|
|
16
|
+
getFolderById,
|
|
17
|
+
removeFolderSyncInfo,
|
|
18
|
+
type SpaceConfigWithState,
|
|
19
|
+
type FolderSyncInfo,
|
|
20
|
+
} from '../lib/space-config.js';
|
|
21
|
+
|
|
22
|
+
describe('space-config', () => {
|
|
23
|
+
let testDir: string;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
testDir = join(tmpdir(), `cn-test-${Date.now()}`);
|
|
27
|
+
mkdirSync(testDir, { recursive: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
if (existsSync(testDir)) {
|
|
32
|
+
rmSync(testDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('createSpaceConfig', () => {
|
|
37
|
+
test('creates config with required fields', () => {
|
|
38
|
+
const config = createSpaceConfig('TEST', 'space-123', 'Test Space');
|
|
39
|
+
|
|
40
|
+
expect(config.spaceKey).toBe('TEST');
|
|
41
|
+
expect(config.spaceId).toBe('space-123');
|
|
42
|
+
expect(config.spaceName).toBe('Test Space');
|
|
43
|
+
expect(config.pages).toEqual({});
|
|
44
|
+
expect(config.lastSync).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('hasSpaceConfig', () => {
|
|
49
|
+
test('returns false when no config exists', () => {
|
|
50
|
+
expect(hasSpaceConfig(testDir)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('returns true when config exists', () => {
|
|
54
|
+
const configPath = join(testDir, '.confluence.json');
|
|
55
|
+
writeFileSync(configPath, JSON.stringify({ spaceKey: 'TEST' }));
|
|
56
|
+
|
|
57
|
+
expect(hasSpaceConfig(testDir)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('readSpaceConfig', () => {
|
|
62
|
+
test('returns null when no config exists', () => {
|
|
63
|
+
const config = readSpaceConfig(testDir);
|
|
64
|
+
expect(config).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('reads valid config', () => {
|
|
68
|
+
// Per ADR-0024: pages is now Record<string, string> (pageId -> localPath)
|
|
69
|
+
const testConfig: SpaceConfigWithState = {
|
|
70
|
+
spaceKey: 'TEST',
|
|
71
|
+
spaceId: 'space-123',
|
|
72
|
+
spaceName: 'Test Space',
|
|
73
|
+
lastSync: '2024-01-01T00:00:00Z',
|
|
74
|
+
pages: {
|
|
75
|
+
'page-1': 'home.md',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const configPath = join(testDir, '.confluence.json');
|
|
80
|
+
writeFileSync(configPath, JSON.stringify(testConfig));
|
|
81
|
+
|
|
82
|
+
const config = readSpaceConfig(testDir);
|
|
83
|
+
|
|
84
|
+
expect(config).not.toBeNull();
|
|
85
|
+
expect(config?.spaceKey).toBe('TEST');
|
|
86
|
+
expect(config?.spaceName).toBe('Test Space');
|
|
87
|
+
expect(config?.pages['page-1']).toBe('home.md');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('migrates legacy format to new format', () => {
|
|
91
|
+
// Legacy format: pages contain full PageSyncInfo objects
|
|
92
|
+
const legacyConfig = {
|
|
93
|
+
spaceKey: 'TEST',
|
|
94
|
+
spaceId: 'space-123',
|
|
95
|
+
spaceName: 'Test Space',
|
|
96
|
+
pages: {
|
|
97
|
+
'page-1': {
|
|
98
|
+
pageId: 'page-1',
|
|
99
|
+
version: 5,
|
|
100
|
+
lastModified: '2024-01-14T08:00:00Z',
|
|
101
|
+
localPath: 'docs/intro.md',
|
|
102
|
+
title: 'Introduction',
|
|
103
|
+
},
|
|
104
|
+
'page-2': {
|
|
105
|
+
pageId: 'page-2',
|
|
106
|
+
version: 3,
|
|
107
|
+
localPath: 'docs/setup.md',
|
|
108
|
+
title: 'Setup Guide',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const configPath = join(testDir, '.confluence.json');
|
|
114
|
+
writeFileSync(configPath, JSON.stringify(legacyConfig));
|
|
115
|
+
|
|
116
|
+
const config = readSpaceConfig(testDir);
|
|
117
|
+
|
|
118
|
+
// Should be migrated to new format
|
|
119
|
+
expect(config).not.toBeNull();
|
|
120
|
+
expect(config?.pages['page-1']).toBe('docs/intro.md');
|
|
121
|
+
expect(config?.pages['page-2']).toBe('docs/setup.md');
|
|
122
|
+
|
|
123
|
+
// Verify the file was rewritten with new format
|
|
124
|
+
const savedContent = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
125
|
+
expect(savedContent.pages['page-1']).toBe('docs/intro.md');
|
|
126
|
+
expect(savedContent.pages['page-2']).toBe('docs/setup.md');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('returns null for invalid JSON', () => {
|
|
130
|
+
const configPath = join(testDir, '.confluence.json');
|
|
131
|
+
writeFileSync(configPath, 'invalid json');
|
|
132
|
+
|
|
133
|
+
const config = readSpaceConfig(testDir);
|
|
134
|
+
expect(config).toBeNull();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('writeSpaceConfig', () => {
|
|
139
|
+
test('writes config to file', () => {
|
|
140
|
+
const config: SpaceConfigWithState = {
|
|
141
|
+
spaceKey: 'TEST',
|
|
142
|
+
spaceId: 'space-123',
|
|
143
|
+
spaceName: 'Test Space',
|
|
144
|
+
pages: {},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
writeSpaceConfig(testDir, config);
|
|
148
|
+
|
|
149
|
+
const configPath = join(testDir, '.confluence.json');
|
|
150
|
+
expect(existsSync(configPath)).toBe(true);
|
|
151
|
+
|
|
152
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
153
|
+
expect(saved.spaceKey).toBe('TEST');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('updateLastSync', () => {
|
|
158
|
+
test('adds lastSync timestamp', () => {
|
|
159
|
+
const config: SpaceConfigWithState = {
|
|
160
|
+
spaceKey: 'TEST',
|
|
161
|
+
spaceId: 'space-123',
|
|
162
|
+
spaceName: 'Test Space',
|
|
163
|
+
pages: {},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const updated = updateLastSync(config);
|
|
167
|
+
|
|
168
|
+
expect(updated.lastSync).toBeDefined();
|
|
169
|
+
if (updated.lastSync) {
|
|
170
|
+
expect(new Date(updated.lastSync).getTime()).toBeGreaterThan(0);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('does not mutate original config', () => {
|
|
175
|
+
const config: SpaceConfigWithState = {
|
|
176
|
+
spaceKey: 'TEST',
|
|
177
|
+
spaceId: 'space-123',
|
|
178
|
+
spaceName: 'Test Space',
|
|
179
|
+
pages: {},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const updated = updateLastSync(config);
|
|
183
|
+
|
|
184
|
+
expect(config.lastSync).toBeUndefined();
|
|
185
|
+
expect(updated).not.toBe(config);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('updatePageSyncInfo', () => {
|
|
190
|
+
test('adds new page', () => {
|
|
191
|
+
const config: SpaceConfigWithState = {
|
|
192
|
+
spaceKey: 'TEST',
|
|
193
|
+
spaceId: 'space-123',
|
|
194
|
+
spaceName: 'Test Space',
|
|
195
|
+
pages: {},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Per ADR-0024: Only pageId and localPath are stored
|
|
199
|
+
const updated = updatePageSyncInfo(config, {
|
|
200
|
+
pageId: 'page-1',
|
|
201
|
+
localPath: 'home.md',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(updated.pages['page-1']).toBeDefined();
|
|
205
|
+
expect(updated.pages['page-1']).toBe('home.md');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('updates existing page', () => {
|
|
209
|
+
// Per ADR-0024: pages is now Record<string, string>
|
|
210
|
+
const config: SpaceConfigWithState = {
|
|
211
|
+
spaceKey: 'TEST',
|
|
212
|
+
spaceId: 'space-123',
|
|
213
|
+
spaceName: 'Test Space',
|
|
214
|
+
pages: {
|
|
215
|
+
'page-1': 'home.md',
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const updated = updatePageSyncInfo(config, {
|
|
220
|
+
pageId: 'page-1',
|
|
221
|
+
localPath: 'new-home.md',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(updated.pages['page-1']).toBe('new-home.md');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('does not mutate original config', () => {
|
|
228
|
+
const config: SpaceConfigWithState = {
|
|
229
|
+
spaceKey: 'TEST',
|
|
230
|
+
spaceId: 'space-123',
|
|
231
|
+
spaceName: 'Test Space',
|
|
232
|
+
pages: {},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const updated = updatePageSyncInfo(config, {
|
|
236
|
+
pageId: 'page-1',
|
|
237
|
+
localPath: 'home.md',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(Object.keys(config.pages)).toHaveLength(0);
|
|
241
|
+
expect(updated).not.toBe(config);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('removePageSyncInfo', () => {
|
|
246
|
+
test('removes existing page', () => {
|
|
247
|
+
// Per ADR-0024: pages is now Record<string, string>
|
|
248
|
+
const config: SpaceConfigWithState = {
|
|
249
|
+
spaceKey: 'TEST',
|
|
250
|
+
spaceId: 'space-123',
|
|
251
|
+
spaceName: 'Test Space',
|
|
252
|
+
pages: {
|
|
253
|
+
'page-1': 'home.md',
|
|
254
|
+
'page-2': 'getting-started.md',
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const updated = removePageSyncInfo(config, 'page-1');
|
|
259
|
+
|
|
260
|
+
expect(updated.pages['page-1']).toBeUndefined();
|
|
261
|
+
expect(updated.pages['page-2']).toBeDefined();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('does nothing for non-existent page', () => {
|
|
265
|
+
const config: SpaceConfigWithState = {
|
|
266
|
+
spaceKey: 'TEST',
|
|
267
|
+
spaceId: 'space-123',
|
|
268
|
+
spaceName: 'Test Space',
|
|
269
|
+
pages: {},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const updated = removePageSyncInfo(config, 'page-1');
|
|
273
|
+
|
|
274
|
+
expect(Object.keys(updated.pages)).toHaveLength(0);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('getTrackedPageIds', () => {
|
|
279
|
+
test('returns all tracked page IDs', () => {
|
|
280
|
+
// Per ADR-0024: pages is now Record<string, string>
|
|
281
|
+
const config: SpaceConfigWithState = {
|
|
282
|
+
spaceKey: 'TEST',
|
|
283
|
+
spaceId: 'space-123',
|
|
284
|
+
spaceName: 'Test Space',
|
|
285
|
+
pages: {
|
|
286
|
+
'page-1': 'home.md',
|
|
287
|
+
'page-2': 'getting-started.md',
|
|
288
|
+
'page-3': 'api.md',
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const ids = getTrackedPageIds(config);
|
|
293
|
+
|
|
294
|
+
expect(ids).toHaveLength(3);
|
|
295
|
+
expect(ids).toContain('page-1');
|
|
296
|
+
expect(ids).toContain('page-2');
|
|
297
|
+
expect(ids).toContain('page-3');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('returns empty array for no pages', () => {
|
|
301
|
+
const config: SpaceConfigWithState = {
|
|
302
|
+
spaceKey: 'TEST',
|
|
303
|
+
spaceId: 'space-123',
|
|
304
|
+
spaceName: 'Test Space',
|
|
305
|
+
pages: {},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const ids = getTrackedPageIds(config);
|
|
309
|
+
|
|
310
|
+
expect(ids).toHaveLength(0);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('updateFolderSyncInfo', () => {
|
|
315
|
+
test('adds new folder', () => {
|
|
316
|
+
const config: SpaceConfigWithState = {
|
|
317
|
+
spaceKey: 'TEST',
|
|
318
|
+
spaceId: 'space-123',
|
|
319
|
+
spaceName: 'Test Space',
|
|
320
|
+
pages: {},
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const folderInfo: FolderSyncInfo = {
|
|
324
|
+
folderId: 'folder-1',
|
|
325
|
+
title: 'docs',
|
|
326
|
+
localPath: 'docs',
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const updated = updateFolderSyncInfo(config, folderInfo);
|
|
330
|
+
|
|
331
|
+
expect(updated.folders).toBeDefined();
|
|
332
|
+
expect(updated.folders?.['folder-1']).toBeDefined();
|
|
333
|
+
expect(updated.folders?.['folder-1'].title).toBe('docs');
|
|
334
|
+
expect(updated.folders?.['folder-1'].localPath).toBe('docs');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('updates existing folder', () => {
|
|
338
|
+
const config: SpaceConfigWithState = {
|
|
339
|
+
spaceKey: 'TEST',
|
|
340
|
+
spaceId: 'space-123',
|
|
341
|
+
spaceName: 'Test Space',
|
|
342
|
+
pages: {},
|
|
343
|
+
folders: {
|
|
344
|
+
'folder-1': {
|
|
345
|
+
folderId: 'folder-1',
|
|
346
|
+
title: 'docs',
|
|
347
|
+
localPath: 'docs',
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const folderInfo: FolderSyncInfo = {
|
|
353
|
+
folderId: 'folder-1',
|
|
354
|
+
title: 'documentation',
|
|
355
|
+
localPath: 'docs',
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const updated = updateFolderSyncInfo(config, folderInfo);
|
|
359
|
+
|
|
360
|
+
expect(updated.folders?.['folder-1'].title).toBe('documentation');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('does not mutate original config', () => {
|
|
364
|
+
const config: SpaceConfigWithState = {
|
|
365
|
+
spaceKey: 'TEST',
|
|
366
|
+
spaceId: 'space-123',
|
|
367
|
+
spaceName: 'Test Space',
|
|
368
|
+
pages: {},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const folderInfo: FolderSyncInfo = {
|
|
372
|
+
folderId: 'folder-1',
|
|
373
|
+
title: 'docs',
|
|
374
|
+
localPath: 'docs',
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const updated = updateFolderSyncInfo(config, folderInfo);
|
|
378
|
+
|
|
379
|
+
expect(config.folders).toBeUndefined();
|
|
380
|
+
expect(updated).not.toBe(config);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('getFolderByPath', () => {
|
|
385
|
+
test('returns folder matching path', () => {
|
|
386
|
+
const config: SpaceConfigWithState = {
|
|
387
|
+
spaceKey: 'TEST',
|
|
388
|
+
spaceId: 'space-123',
|
|
389
|
+
spaceName: 'Test Space',
|
|
390
|
+
pages: {},
|
|
391
|
+
folders: {
|
|
392
|
+
'folder-1': { folderId: 'folder-1', title: 'docs', localPath: 'docs' },
|
|
393
|
+
'folder-2': { folderId: 'folder-2', title: 'api', parentId: 'folder-1', localPath: 'docs/api' },
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const folder = getFolderByPath(config, 'docs/api');
|
|
398
|
+
|
|
399
|
+
expect(folder).toBeDefined();
|
|
400
|
+
expect(folder?.folderId).toBe('folder-2');
|
|
401
|
+
expect(folder?.title).toBe('api');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('returns undefined for non-existent path', () => {
|
|
405
|
+
const config: SpaceConfigWithState = {
|
|
406
|
+
spaceKey: 'TEST',
|
|
407
|
+
spaceId: 'space-123',
|
|
408
|
+
spaceName: 'Test Space',
|
|
409
|
+
pages: {},
|
|
410
|
+
folders: {
|
|
411
|
+
'folder-1': { folderId: 'folder-1', title: 'docs', localPath: 'docs' },
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const folder = getFolderByPath(config, 'nonexistent');
|
|
416
|
+
|
|
417
|
+
expect(folder).toBeUndefined();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('returns undefined when no folders exist', () => {
|
|
421
|
+
const config: SpaceConfigWithState = {
|
|
422
|
+
spaceKey: 'TEST',
|
|
423
|
+
spaceId: 'space-123',
|
|
424
|
+
spaceName: 'Test Space',
|
|
425
|
+
pages: {},
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const folder = getFolderByPath(config, 'docs');
|
|
429
|
+
|
|
430
|
+
expect(folder).toBeUndefined();
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('getFolderById', () => {
|
|
435
|
+
test('returns folder by ID', () => {
|
|
436
|
+
const config: SpaceConfigWithState = {
|
|
437
|
+
spaceKey: 'TEST',
|
|
438
|
+
spaceId: 'space-123',
|
|
439
|
+
spaceName: 'Test Space',
|
|
440
|
+
pages: {},
|
|
441
|
+
folders: {
|
|
442
|
+
'folder-1': { folderId: 'folder-1', title: 'docs', localPath: 'docs' },
|
|
443
|
+
'folder-2': { folderId: 'folder-2', title: 'api', localPath: 'docs/api' },
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const folder = getFolderById(config, 'folder-1');
|
|
448
|
+
|
|
449
|
+
expect(folder).toBeDefined();
|
|
450
|
+
expect(folder?.title).toBe('docs');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test('returns undefined for non-existent ID', () => {
|
|
454
|
+
const config: SpaceConfigWithState = {
|
|
455
|
+
spaceKey: 'TEST',
|
|
456
|
+
spaceId: 'space-123',
|
|
457
|
+
spaceName: 'Test Space',
|
|
458
|
+
pages: {},
|
|
459
|
+
folders: {
|
|
460
|
+
'folder-1': { folderId: 'folder-1', title: 'docs', localPath: 'docs' },
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const folder = getFolderById(config, 'nonexistent');
|
|
465
|
+
|
|
466
|
+
expect(folder).toBeUndefined();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe('removeFolderSyncInfo', () => {
|
|
471
|
+
test('removes existing folder', () => {
|
|
472
|
+
const config: SpaceConfigWithState = {
|
|
473
|
+
spaceKey: 'TEST',
|
|
474
|
+
spaceId: 'space-123',
|
|
475
|
+
spaceName: 'Test Space',
|
|
476
|
+
pages: {},
|
|
477
|
+
folders: {
|
|
478
|
+
'folder-1': { folderId: 'folder-1', title: 'docs', localPath: 'docs' },
|
|
479
|
+
'folder-2': { folderId: 'folder-2', title: 'api', localPath: 'docs/api' },
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const updated = removeFolderSyncInfo(config, 'folder-1');
|
|
484
|
+
|
|
485
|
+
expect(updated.folders?.['folder-1']).toBeUndefined();
|
|
486
|
+
expect(updated.folders?.['folder-2']).toBeDefined();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test('does nothing for non-existent folder', () => {
|
|
490
|
+
const config: SpaceConfigWithState = {
|
|
491
|
+
spaceKey: 'TEST',
|
|
492
|
+
spaceId: 'space-123',
|
|
493
|
+
spaceName: 'Test Space',
|
|
494
|
+
pages: {},
|
|
495
|
+
folders: {},
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const updated = removeFolderSyncInfo(config, 'folder-1');
|
|
499
|
+
|
|
500
|
+
expect(Object.keys(updated.folders || {})).toHaveLength(0);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
test('handles config with no folders field', () => {
|
|
504
|
+
const config: SpaceConfigWithState = {
|
|
505
|
+
spaceKey: 'TEST',
|
|
506
|
+
spaceId: 'space-123',
|
|
507
|
+
spaceName: 'Test Space',
|
|
508
|
+
pages: {},
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const updated = removeFolderSyncInfo(config, 'folder-1');
|
|
512
|
+
|
|
513
|
+
expect(updated.folders).toBeUndefined();
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { createValidSpace } from './msw-schema-validation.js';
|
|
5
|
+
import { server } from './setup-msw.js';
|
|
6
|
+
|
|
7
|
+
const testConfig = {
|
|
8
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
9
|
+
email: 'test@example.com',
|
|
10
|
+
apiToken: 'test-token',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('ConfluenceClient - spaces', () => {
|
|
14
|
+
test('lists spaces', async () => {
|
|
15
|
+
const client = new ConfluenceClient(testConfig);
|
|
16
|
+
const response = await client.getSpaces();
|
|
17
|
+
expect(response.results).toBeArray();
|
|
18
|
+
expect(response.results.length).toBeGreaterThan(0);
|
|
19
|
+
expect(response.results[0]).toHaveProperty('key');
|
|
20
|
+
expect(response.results[0]).toHaveProperty('name');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('returns specific space by key', async () => {
|
|
24
|
+
server.use(
|
|
25
|
+
http.get('*/wiki/api/v2/spaces', ({ request }) => {
|
|
26
|
+
const url = new URL(request.url);
|
|
27
|
+
const keys = url.searchParams.get('keys');
|
|
28
|
+
if (keys === 'DOCS') {
|
|
29
|
+
return HttpResponse.json({
|
|
30
|
+
results: [createValidSpace({ id: 'space-docs', key: 'DOCS', name: 'Documentation' })],
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return HttpResponse.json({ results: [] });
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const client = new ConfluenceClient(testConfig);
|
|
38
|
+
const space = await client.getSpaceByKey('DOCS');
|
|
39
|
+
expect(space.key).toBe('DOCS');
|
|
40
|
+
expect(space.name).toBe('Documentation');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('throws on 401', async () => {
|
|
44
|
+
server.use(
|
|
45
|
+
http.get('*/wiki/api/v2/spaces', () => {
|
|
46
|
+
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const client = new ConfluenceClient(testConfig);
|
|
51
|
+
await expect(client.getSpaces()).rejects.toThrow();
|
|
52
|
+
});
|
|
53
|
+
});
|