@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,337 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { HttpResponse, http } from 'msw';
|
|
6
|
+
import { ConfluenceClient } from '../lib/confluence-client/client.js';
|
|
7
|
+
import {
|
|
8
|
+
determineExpectedParent,
|
|
9
|
+
ensureFolderHierarchy,
|
|
10
|
+
FolderHierarchyError,
|
|
11
|
+
sanitizeFolderTitle,
|
|
12
|
+
} from '../cli/commands/folder-hierarchy.js';
|
|
13
|
+
import type { SpaceConfigWithState } from '../lib/space-config.js';
|
|
14
|
+
import { server } from './setup-msw.js';
|
|
15
|
+
import { createValidFolder } from './msw-schema-validation.js';
|
|
16
|
+
|
|
17
|
+
const testConfig = {
|
|
18
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
19
|
+
email: 'test@example.com',
|
|
20
|
+
apiToken: 'test-token',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('folder-hierarchy', () => {
|
|
24
|
+
describe('sanitizeFolderTitle', () => {
|
|
25
|
+
test('returns unchanged title for valid names', () => {
|
|
26
|
+
const result = sanitizeFolderTitle('My Folder');
|
|
27
|
+
expect(result.sanitized).toBe('My Folder');
|
|
28
|
+
expect(result.wasModified).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('replaces pipe character', () => {
|
|
32
|
+
const result = sanitizeFolderTitle('Folder | Name');
|
|
33
|
+
expect(result.sanitized).toBe('Folder - Name');
|
|
34
|
+
expect(result.wasModified).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('replaces backslash', () => {
|
|
38
|
+
const result = sanitizeFolderTitle('Folder\\Name');
|
|
39
|
+
expect(result.sanitized).toBe('Folder-Name');
|
|
40
|
+
expect(result.wasModified).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('replaces forward slash', () => {
|
|
44
|
+
const result = sanitizeFolderTitle('Folder/Name');
|
|
45
|
+
expect(result.sanitized).toBe('Folder-Name');
|
|
46
|
+
expect(result.wasModified).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('replaces colon', () => {
|
|
50
|
+
const result = sanitizeFolderTitle('Folder: Name');
|
|
51
|
+
expect(result.sanitized).toBe('Folder- Name');
|
|
52
|
+
expect(result.wasModified).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('replaces asterisk', () => {
|
|
56
|
+
const result = sanitizeFolderTitle('Folder*Name');
|
|
57
|
+
expect(result.sanitized).toBe('Folder-Name');
|
|
58
|
+
expect(result.wasModified).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('replaces question mark', () => {
|
|
62
|
+
const result = sanitizeFolderTitle('Folder?Name');
|
|
63
|
+
expect(result.sanitized).toBe('Folder-Name');
|
|
64
|
+
expect(result.wasModified).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('replaces double quotes', () => {
|
|
68
|
+
const result = sanitizeFolderTitle('Folder"Name');
|
|
69
|
+
expect(result.sanitized).toBe('Folder-Name');
|
|
70
|
+
expect(result.wasModified).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('replaces angle brackets', () => {
|
|
74
|
+
const result = sanitizeFolderTitle('Folder<Name>');
|
|
75
|
+
expect(result.sanitized).toBe('Folder-Name-');
|
|
76
|
+
expect(result.wasModified).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('replaces multiple invalid characters', () => {
|
|
80
|
+
const result = sanitizeFolderTitle('A|B/C:D*E?F"G<H>I');
|
|
81
|
+
expect(result.sanitized).toBe('A-B-C-D-E-F-G-H-I');
|
|
82
|
+
expect(result.wasModified).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('trims whitespace', () => {
|
|
86
|
+
const result = sanitizeFolderTitle(' Folder Name ');
|
|
87
|
+
expect(result.sanitized).toBe('Folder Name');
|
|
88
|
+
expect(result.wasModified).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('ensureFolderHierarchy', () => {
|
|
93
|
+
let testDir: string;
|
|
94
|
+
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
testDir = mkdtempSync(join(tmpdir(), 'cn-folder-hierarchy-test-'));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => {
|
|
100
|
+
if (existsSync(testDir)) {
|
|
101
|
+
rmSync(testDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const createSpaceConfig = (overrides?: Partial<SpaceConfigWithState>): SpaceConfigWithState => ({
|
|
106
|
+
spaceKey: 'TEST',
|
|
107
|
+
spaceId: 'space-123',
|
|
108
|
+
spaceName: 'Test Space',
|
|
109
|
+
pages: {},
|
|
110
|
+
folders: {},
|
|
111
|
+
...overrides,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('returns undefined parentId for root-level files', async () => {
|
|
115
|
+
const client = new ConfluenceClient(testConfig);
|
|
116
|
+
const spaceConfig = createSpaceConfig();
|
|
117
|
+
|
|
118
|
+
const result = await ensureFolderHierarchy(client, spaceConfig, testDir, 'readme.md', true);
|
|
119
|
+
|
|
120
|
+
expect(result.parentId).toBeUndefined();
|
|
121
|
+
expect(result.updatedConfig).toEqual(spaceConfig);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('returns undefined parentId for files with ./ prefix at root', async () => {
|
|
125
|
+
const client = new ConfluenceClient(testConfig);
|
|
126
|
+
const spaceConfig = createSpaceConfig();
|
|
127
|
+
|
|
128
|
+
const result = await ensureFolderHierarchy(client, spaceConfig, testDir, './readme.md', true);
|
|
129
|
+
|
|
130
|
+
expect(result.parentId).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('returns existing folder ID from local config', async () => {
|
|
134
|
+
const client = new ConfluenceClient(testConfig);
|
|
135
|
+
const spaceConfig = createSpaceConfig({
|
|
136
|
+
folders: {
|
|
137
|
+
'folder-abc': {
|
|
138
|
+
folderId: 'folder-abc',
|
|
139
|
+
title: 'docs',
|
|
140
|
+
localPath: 'docs',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const result = await ensureFolderHierarchy(client, spaceConfig, testDir, 'docs/guide.md', true);
|
|
146
|
+
|
|
147
|
+
expect(result.parentId).toBe('folder-abc');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('returns nested folder ID from local config', async () => {
|
|
151
|
+
const client = new ConfluenceClient(testConfig);
|
|
152
|
+
const spaceConfig = createSpaceConfig({
|
|
153
|
+
folders: {
|
|
154
|
+
'folder-docs': {
|
|
155
|
+
folderId: 'folder-docs',
|
|
156
|
+
title: 'docs',
|
|
157
|
+
localPath: 'docs',
|
|
158
|
+
},
|
|
159
|
+
'folder-api': {
|
|
160
|
+
folderId: 'folder-api',
|
|
161
|
+
title: 'api',
|
|
162
|
+
parentId: 'folder-docs',
|
|
163
|
+
localPath: 'docs/api',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const result = await ensureFolderHierarchy(client, spaceConfig, testDir, 'docs/api/endpoints.md', true);
|
|
169
|
+
|
|
170
|
+
expect(result.parentId).toBe('folder-api');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('throws error for path traversal attempts', async () => {
|
|
174
|
+
const client = new ConfluenceClient(testConfig);
|
|
175
|
+
const spaceConfig = createSpaceConfig();
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await ensureFolderHierarchy(client, spaceConfig, testDir, '../etc/passwd', true);
|
|
179
|
+
expect.unreachable('Should have thrown');
|
|
180
|
+
} catch (error) {
|
|
181
|
+
expect(error).toBeInstanceOf(FolderHierarchyError);
|
|
182
|
+
expect((error as FolderHierarchyError).message).toContain('path traversal');
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('throws error for deeply nested paths exceeding max depth', async () => {
|
|
187
|
+
const client = new ConfluenceClient(testConfig);
|
|
188
|
+
const spaceConfig = createSpaceConfig();
|
|
189
|
+
|
|
190
|
+
// Create a path with 11 levels (exceeds MAX_FOLDER_DEPTH of 10)
|
|
191
|
+
const deepPath = 'a/b/c/d/e/f/g/h/i/j/k/file.md';
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await ensureFolderHierarchy(client, spaceConfig, testDir, deepPath, true);
|
|
195
|
+
expect.unreachable('Should have thrown');
|
|
196
|
+
} catch (error) {
|
|
197
|
+
expect(error).toBeInstanceOf(FolderHierarchyError);
|
|
198
|
+
expect((error as FolderHierarchyError).message).toContain('too deep');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('dry run returns undefined when folder needs creation', async () => {
|
|
203
|
+
const client = new ConfluenceClient(testConfig);
|
|
204
|
+
const spaceConfig = createSpaceConfig();
|
|
205
|
+
|
|
206
|
+
// Mock: folder does not exist on Confluence
|
|
207
|
+
server.use(
|
|
208
|
+
http.get('*/wiki/api/v2/spaces/space-123/folders', () => {
|
|
209
|
+
return HttpResponse.json({ results: [] });
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const result = await ensureFolderHierarchy(client, spaceConfig, testDir, 'newdir/file.md', true);
|
|
214
|
+
|
|
215
|
+
// Dry run can't continue without a real folder ID
|
|
216
|
+
expect(result.parentId).toBeUndefined();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('determineExpectedParent', () => {
|
|
221
|
+
let testDir: string;
|
|
222
|
+
|
|
223
|
+
beforeEach(() => {
|
|
224
|
+
testDir = mkdtempSync(join(tmpdir(), 'cn-determine-parent-test-'));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
afterEach(() => {
|
|
228
|
+
if (existsSync(testDir)) {
|
|
229
|
+
rmSync(testDir, { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const createSpaceConfig = (overrides?: Partial<SpaceConfigWithState>): SpaceConfigWithState => ({
|
|
234
|
+
spaceKey: 'TEST',
|
|
235
|
+
spaceId: 'space-123',
|
|
236
|
+
spaceName: 'Test Space',
|
|
237
|
+
pages: {},
|
|
238
|
+
folders: {},
|
|
239
|
+
...overrides,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('returns undefined parentId for root-level files', async () => {
|
|
243
|
+
const client = new ConfluenceClient(testConfig);
|
|
244
|
+
const spaceConfig = createSpaceConfig();
|
|
245
|
+
|
|
246
|
+
const result = await determineExpectedParent(client, spaceConfig, testDir, 'readme.md', true);
|
|
247
|
+
|
|
248
|
+
expect(result.parentId).toBeUndefined();
|
|
249
|
+
expect(result.updatedConfig).toEqual(spaceConfig);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('returns undefined for files with . directory', async () => {
|
|
253
|
+
const client = new ConfluenceClient(testConfig);
|
|
254
|
+
const spaceConfig = createSpaceConfig();
|
|
255
|
+
|
|
256
|
+
// This tests the early return for dirPath === '.'
|
|
257
|
+
const result = await determineExpectedParent(client, spaceConfig, testDir, './file.md', true);
|
|
258
|
+
|
|
259
|
+
expect(result.parentId).toBeUndefined();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('returns existing folder ID for nested file', async () => {
|
|
263
|
+
const client = new ConfluenceClient(testConfig);
|
|
264
|
+
const spaceConfig = createSpaceConfig({
|
|
265
|
+
folders: {
|
|
266
|
+
'folder-xyz': {
|
|
267
|
+
folderId: 'folder-xyz',
|
|
268
|
+
title: 'docs',
|
|
269
|
+
localPath: 'docs',
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const result = await determineExpectedParent(client, spaceConfig, testDir, 'docs/page.md', true);
|
|
275
|
+
|
|
276
|
+
expect(result.parentId).toBe('folder-xyz');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('returns deeply nested folder ID', async () => {
|
|
280
|
+
const client = new ConfluenceClient(testConfig);
|
|
281
|
+
const spaceConfig = createSpaceConfig({
|
|
282
|
+
folders: {
|
|
283
|
+
'folder-docs': {
|
|
284
|
+
folderId: 'folder-docs',
|
|
285
|
+
title: 'docs',
|
|
286
|
+
localPath: 'docs',
|
|
287
|
+
},
|
|
288
|
+
'folder-api': {
|
|
289
|
+
folderId: 'folder-api',
|
|
290
|
+
title: 'api',
|
|
291
|
+
parentId: 'folder-docs',
|
|
292
|
+
localPath: 'docs/api',
|
|
293
|
+
},
|
|
294
|
+
'folder-v2': {
|
|
295
|
+
folderId: 'folder-v2',
|
|
296
|
+
title: 'v2',
|
|
297
|
+
parentId: 'folder-api',
|
|
298
|
+
localPath: 'docs/api/v2',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const result = await determineExpectedParent(client, spaceConfig, testDir, 'docs/api/v2/endpoints.md', true);
|
|
304
|
+
|
|
305
|
+
expect(result.parentId).toBe('folder-v2');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('handles path normalization with ./ prefix', async () => {
|
|
309
|
+
const client = new ConfluenceClient(testConfig);
|
|
310
|
+
const spaceConfig = createSpaceConfig({
|
|
311
|
+
folders: {
|
|
312
|
+
'folder-docs': {
|
|
313
|
+
folderId: 'folder-docs',
|
|
314
|
+
title: 'docs',
|
|
315
|
+
localPath: 'docs',
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const result = await determineExpectedParent(client, spaceConfig, testDir, './docs/guide.md', true);
|
|
321
|
+
|
|
322
|
+
expect(result.parentId).toBe('folder-docs');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('propagates FolderHierarchyError from ensureFolderHierarchy', async () => {
|
|
326
|
+
const client = new ConfluenceClient(testConfig);
|
|
327
|
+
const spaceConfig = createSpaceConfig();
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
await determineExpectedParent(client, spaceConfig, testDir, '../outside/file.md', true);
|
|
331
|
+
expect.unreachable('Should have thrown');
|
|
332
|
+
} catch (error) {
|
|
333
|
+
expect(error).toBeInstanceOf(FolderHierarchyError);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { HumanFormatter, XmlFormatter, getFormatter, type StatusInfo, type TreeNode } from '../lib/formatters.js';
|
|
3
|
+
import type { SyncDiff } from '../lib/sync/sync-engine.js';
|
|
4
|
+
|
|
5
|
+
describe('HumanFormatter', () => {
|
|
6
|
+
const formatter = new HumanFormatter();
|
|
7
|
+
|
|
8
|
+
describe('formatSpaces', () => {
|
|
9
|
+
test('formats list of spaces', () => {
|
|
10
|
+
const spaces = [
|
|
11
|
+
{ id: '1', key: 'TEST', name: 'Test Space' },
|
|
12
|
+
{ id: '2', key: 'DOCS', name: 'Documentation' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const output = formatter.formatSpaces(spaces);
|
|
16
|
+
|
|
17
|
+
expect(output).toContain('TEST');
|
|
18
|
+
expect(output).toContain('Test Space');
|
|
19
|
+
expect(output).toContain('DOCS');
|
|
20
|
+
expect(output).toContain('Documentation');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('handles empty spaces list', () => {
|
|
24
|
+
const output = formatter.formatSpaces([]);
|
|
25
|
+
expect(output).toContain('No spaces found');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('formatSyncDiff', () => {
|
|
30
|
+
test('formats diff with changes', () => {
|
|
31
|
+
const diff: SyncDiff = {
|
|
32
|
+
added: [{ type: 'added', pageId: '1', title: 'New Page' }],
|
|
33
|
+
modified: [{ type: 'modified', pageId: '2', title: 'Updated Page' }],
|
|
34
|
+
deleted: [{ type: 'deleted', pageId: '3', title: 'Removed Page' }],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const output = formatter.formatSyncDiff(diff);
|
|
38
|
+
|
|
39
|
+
expect(output).toContain('Added');
|
|
40
|
+
expect(output).toContain('New Page');
|
|
41
|
+
expect(output).toContain('Modified');
|
|
42
|
+
expect(output).toContain('Updated Page');
|
|
43
|
+
expect(output).toContain('Deleted');
|
|
44
|
+
expect(output).toContain('Removed Page');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('handles no changes', () => {
|
|
48
|
+
const diff: SyncDiff = {
|
|
49
|
+
added: [],
|
|
50
|
+
modified: [],
|
|
51
|
+
deleted: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const output = formatter.formatSyncDiff(diff);
|
|
55
|
+
expect(output).toContain('No changes');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('formatStatus', () => {
|
|
60
|
+
test('formats unconfigured status', () => {
|
|
61
|
+
const status: StatusInfo = {
|
|
62
|
+
configured: false,
|
|
63
|
+
connected: false,
|
|
64
|
+
initialized: false,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const output = formatter.formatStatus(status);
|
|
68
|
+
expect(output).toContain('Not configured');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('formats configured status', () => {
|
|
72
|
+
const status: StatusInfo = {
|
|
73
|
+
configured: true,
|
|
74
|
+
connected: true,
|
|
75
|
+
initialized: true,
|
|
76
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
77
|
+
email: 'test@example.com',
|
|
78
|
+
spaceKey: 'TEST',
|
|
79
|
+
spaceName: 'Test Space',
|
|
80
|
+
lastSync: '2024-01-01T00:00:00Z',
|
|
81
|
+
pageCount: 10,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const output = formatter.formatStatus(status);
|
|
85
|
+
|
|
86
|
+
expect(output).toContain('https://test.atlassian.net');
|
|
87
|
+
expect(output).toContain('test@example.com');
|
|
88
|
+
expect(output).toContain('TEST');
|
|
89
|
+
expect(output).toContain('Test Space');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('formatTree', () => {
|
|
94
|
+
test('formats page tree', () => {
|
|
95
|
+
const nodes: TreeNode[] = [
|
|
96
|
+
{
|
|
97
|
+
id: '1',
|
|
98
|
+
title: 'Home',
|
|
99
|
+
children: [
|
|
100
|
+
{ id: '2', title: 'Getting Started', children: [] },
|
|
101
|
+
{ id: '3', title: 'API Reference', children: [] },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const output = formatter.formatTree(nodes);
|
|
107
|
+
|
|
108
|
+
expect(output).toContain('Home');
|
|
109
|
+
expect(output).toContain('Getting Started');
|
|
110
|
+
expect(output).toContain('API Reference');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('formats empty tree', () => {
|
|
114
|
+
const output = formatter.formatTree([]);
|
|
115
|
+
expect(output).toBe('');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('XmlFormatter', () => {
|
|
121
|
+
const formatter = new XmlFormatter();
|
|
122
|
+
|
|
123
|
+
describe('formatSpaces', () => {
|
|
124
|
+
test('formats spaces as XML', () => {
|
|
125
|
+
const spaces = [{ id: '1', key: 'TEST', name: 'Test Space' }];
|
|
126
|
+
|
|
127
|
+
const output = formatter.formatSpaces(spaces);
|
|
128
|
+
|
|
129
|
+
expect(output).toContain('<spaces>');
|
|
130
|
+
expect(output).toContain('</spaces>');
|
|
131
|
+
expect(output).toContain('key="TEST"');
|
|
132
|
+
expect(output).toContain('<name>Test Space</name>');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('escapes special characters', () => {
|
|
136
|
+
const spaces = [{ id: '1', key: 'TEST', name: 'Test & <Space>' }];
|
|
137
|
+
|
|
138
|
+
const output = formatter.formatSpaces(spaces);
|
|
139
|
+
|
|
140
|
+
expect(output).toContain('&');
|
|
141
|
+
expect(output).toContain('<');
|
|
142
|
+
expect(output).toContain('>');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('formatSyncDiff', () => {
|
|
147
|
+
test('formats diff as XML', () => {
|
|
148
|
+
const diff: SyncDiff = {
|
|
149
|
+
added: [{ type: 'added', pageId: '1', title: 'New Page' }],
|
|
150
|
+
modified: [],
|
|
151
|
+
deleted: [],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const output = formatter.formatSyncDiff(diff);
|
|
155
|
+
|
|
156
|
+
expect(output).toContain('<sync-diff');
|
|
157
|
+
expect(output).toContain('added="1"');
|
|
158
|
+
expect(output).toContain('<added>');
|
|
159
|
+
expect(output).toContain('id="1"');
|
|
160
|
+
expect(output).toContain('title="New Page"');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('formatStatus', () => {
|
|
165
|
+
test('formats status as XML', () => {
|
|
166
|
+
const status: StatusInfo = {
|
|
167
|
+
configured: true,
|
|
168
|
+
connected: true,
|
|
169
|
+
initialized: true,
|
|
170
|
+
confluenceUrl: 'https://test.atlassian.net',
|
|
171
|
+
spaceKey: 'TEST',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const output = formatter.formatStatus(status);
|
|
175
|
+
|
|
176
|
+
expect(output).toContain('<status');
|
|
177
|
+
expect(output).toContain('configured="true"');
|
|
178
|
+
expect(output).toContain('connected="true"');
|
|
179
|
+
expect(output).toContain('<url>https://test.atlassian.net</url>');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('formatTree', () => {
|
|
184
|
+
test('formats tree as XML', () => {
|
|
185
|
+
const nodes: TreeNode[] = [
|
|
186
|
+
{
|
|
187
|
+
id: '1',
|
|
188
|
+
title: 'Home',
|
|
189
|
+
children: [{ id: '2', title: 'Child', children: [] }],
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const output = formatter.formatTree(nodes);
|
|
194
|
+
|
|
195
|
+
expect(output).toContain('<tree>');
|
|
196
|
+
expect(output).toContain('</tree>');
|
|
197
|
+
expect(output).toContain('id="1"');
|
|
198
|
+
expect(output).toContain('title="Home"');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('getFormatter', () => {
|
|
204
|
+
test('returns HumanFormatter for non-XML mode', () => {
|
|
205
|
+
const formatter = getFormatter(false);
|
|
206
|
+
expect(formatter).toBeInstanceOf(HumanFormatter);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('returns XmlFormatter for XML mode', () => {
|
|
210
|
+
const formatter = getFormatter(true);
|
|
211
|
+
expect(formatter).toBeInstanceOf(XmlFormatter);
|
|
212
|
+
});
|
|
213
|
+
});
|