@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,293 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { updateReferencesAfterRename } from '../lib/markdown/reference-updater.js';
|
|
6
|
+
|
|
7
|
+
describe('updateReferencesAfterRename', () => {
|
|
8
|
+
let testDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Create a temporary directory for each test
|
|
12
|
+
testDir = mkdtempSync(join(tmpdir(), 'cn-test-'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
// Clean up test directory
|
|
17
|
+
if (testDir) {
|
|
18
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('updates single reference in one file', () => {
|
|
23
|
+
// Create files
|
|
24
|
+
writeFileSync(
|
|
25
|
+
join(testDir, 'page1.md'),
|
|
26
|
+
`---
|
|
27
|
+
title: Page 1
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
See [Page 2](./page2.md) for more info.
|
|
31
|
+
`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
writeFileSync(
|
|
35
|
+
join(testDir, 'page2.md'),
|
|
36
|
+
`---
|
|
37
|
+
title: Page 2
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
Content here.
|
|
41
|
+
`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Rename page2.md to page2-renamed.md
|
|
45
|
+
const results = updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
|
|
46
|
+
|
|
47
|
+
// Verify results
|
|
48
|
+
expect(results.length).toBe(1);
|
|
49
|
+
expect(results[0].filePath).toBe('page1.md');
|
|
50
|
+
expect(results[0].updatedCount).toBe(1);
|
|
51
|
+
|
|
52
|
+
// Verify file content was updated
|
|
53
|
+
const content = readFileSync(join(testDir, 'page1.md'), 'utf-8');
|
|
54
|
+
expect(content).toContain('[Page 2](./page2-renamed.md)');
|
|
55
|
+
expect(content).not.toContain('[Page 2](./page2.md)');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('updates multiple references in one file', () => {
|
|
59
|
+
writeFileSync(
|
|
60
|
+
join(testDir, 'page1.md'),
|
|
61
|
+
`---
|
|
62
|
+
title: Page 1
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
See [Page 2](./page2.md) for more info.
|
|
66
|
+
Also check out [this link](./page2.md) again.
|
|
67
|
+
And [another reference](./page2.md).
|
|
68
|
+
`,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
writeFileSync(join(testDir, 'page2.md'), '# Page 2');
|
|
72
|
+
|
|
73
|
+
const results = updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
|
|
74
|
+
|
|
75
|
+
expect(results.length).toBe(1);
|
|
76
|
+
expect(results[0].updatedCount).toBe(3);
|
|
77
|
+
|
|
78
|
+
const content = readFileSync(join(testDir, 'page1.md'), 'utf-8');
|
|
79
|
+
const matches = content.match(/\(\.\/page2-renamed\.md\)/g);
|
|
80
|
+
expect(matches?.length).toBe(3);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('updates references in multiple files', () => {
|
|
84
|
+
writeFileSync(join(testDir, 'page1.md'), 'Check [Page 3](./page3.md).');
|
|
85
|
+
writeFileSync(join(testDir, 'page2.md'), 'See [Page 3](./page3.md).');
|
|
86
|
+
writeFileSync(join(testDir, 'page3.md'), '# Page 3');
|
|
87
|
+
|
|
88
|
+
const results = updateReferencesAfterRename(testDir, 'page3.md', 'page3-renamed.md');
|
|
89
|
+
|
|
90
|
+
expect(results.length).toBe(2);
|
|
91
|
+
expect(results.map((r) => r.filePath).sort()).toEqual(['page1.md', 'page2.md']);
|
|
92
|
+
|
|
93
|
+
const content1 = readFileSync(join(testDir, 'page1.md'), 'utf-8');
|
|
94
|
+
const content2 = readFileSync(join(testDir, 'page2.md'), 'utf-8');
|
|
95
|
+
expect(content1).toContain('./page3-renamed.md');
|
|
96
|
+
expect(content2).toContain('./page3-renamed.md');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('handles nested directories', () => {
|
|
100
|
+
mkdirSync(join(testDir, 'architecture'));
|
|
101
|
+
|
|
102
|
+
writeFileSync(join(testDir, 'home.md'), 'See [Overview](./architecture/overview.md).');
|
|
103
|
+
writeFileSync(join(testDir, 'architecture', 'overview.md'), '# Overview');
|
|
104
|
+
|
|
105
|
+
const results = updateReferencesAfterRename(
|
|
106
|
+
testDir,
|
|
107
|
+
'architecture/overview.md',
|
|
108
|
+
'architecture/overview-renamed.md',
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(results.length).toBe(1);
|
|
112
|
+
expect(results[0].filePath).toBe('home.md');
|
|
113
|
+
|
|
114
|
+
const content = readFileSync(join(testDir, 'home.md'), 'utf-8');
|
|
115
|
+
expect(content).toContain('./architecture/overview-renamed.md');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('handles relative paths from subdirectories', () => {
|
|
119
|
+
mkdirSync(join(testDir, 'docs'));
|
|
120
|
+
mkdirSync(join(testDir, 'architecture'));
|
|
121
|
+
|
|
122
|
+
writeFileSync(join(testDir, 'docs', 'guide.md'), 'See [Overview](../architecture/overview.md).');
|
|
123
|
+
writeFileSync(join(testDir, 'architecture', 'overview.md'), '# Overview');
|
|
124
|
+
|
|
125
|
+
const results = updateReferencesAfterRename(
|
|
126
|
+
testDir,
|
|
127
|
+
'architecture/overview.md',
|
|
128
|
+
'architecture/overview-renamed.md',
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(results.length).toBe(1);
|
|
132
|
+
expect(results[0].filePath).toBe('docs/guide.md');
|
|
133
|
+
|
|
134
|
+
const content = readFileSync(join(testDir, 'docs', 'guide.md'), 'utf-8');
|
|
135
|
+
expect(content).toContain('../architecture/overview-renamed.md');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('returns empty array when no references found', () => {
|
|
139
|
+
writeFileSync(join(testDir, 'page1.md'), 'No references here.');
|
|
140
|
+
writeFileSync(join(testDir, 'page2.md'), '# Page 2');
|
|
141
|
+
|
|
142
|
+
const results = updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
|
|
143
|
+
|
|
144
|
+
expect(results.length).toBe(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('does not update the renamed file itself', () => {
|
|
148
|
+
writeFileSync(join(testDir, 'page1.md'), 'See [myself](./page1.md).');
|
|
149
|
+
|
|
150
|
+
const results = updateReferencesAfterRename(testDir, 'page1.md', 'page1-renamed.md');
|
|
151
|
+
|
|
152
|
+
expect(results.length).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('preserves frontmatter when updating references', () => {
|
|
156
|
+
writeFileSync(
|
|
157
|
+
join(testDir, 'page1.md'),
|
|
158
|
+
`---
|
|
159
|
+
page_id: "123"
|
|
160
|
+
title: "Page 1"
|
|
161
|
+
version: 5
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
See [Page 2](./page2.md).
|
|
165
|
+
`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
writeFileSync(join(testDir, 'page2.md'), '# Page 2');
|
|
169
|
+
|
|
170
|
+
updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
|
|
171
|
+
|
|
172
|
+
const content = readFileSync(join(testDir, 'page1.md'), 'utf-8');
|
|
173
|
+
// gray-matter converts double quotes to single quotes, which is valid YAML
|
|
174
|
+
expect(content).toMatch(/page_id:\s+['"]123['"]/);
|
|
175
|
+
expect(content).toMatch(/title:\s+['"]?Page 1['"]?/);
|
|
176
|
+
expect(content).toContain('version: 5');
|
|
177
|
+
expect(content).toContain('./page2-renamed.md');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('handles files with special characters in link text', () => {
|
|
181
|
+
writeFileSync(join(testDir, 'page1.md'), 'See [Special (Page) [2]](./page2.md).');
|
|
182
|
+
writeFileSync(join(testDir, 'page2.md'), '# Page 2');
|
|
183
|
+
|
|
184
|
+
const results = updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
|
|
185
|
+
|
|
186
|
+
expect(results.length).toBe(1);
|
|
187
|
+
|
|
188
|
+
const content = readFileSync(join(testDir, 'page1.md'), 'utf-8');
|
|
189
|
+
expect(content).toContain('[Special (Page) [2]](./page2-renamed.md)');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('ignores external links', () => {
|
|
193
|
+
writeFileSync(
|
|
194
|
+
join(testDir, 'page1.md'),
|
|
195
|
+
`See [External](https://example.com/page2.md).
|
|
196
|
+
Also [Local](./page2.md).`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
writeFileSync(join(testDir, 'page2.md'), '# Page 2');
|
|
200
|
+
|
|
201
|
+
const _results = updateReferencesAfterRename(testDir, 'page2.md', 'page2-renamed.md');
|
|
202
|
+
|
|
203
|
+
const content = readFileSync(join(testDir, 'page1.md'), 'utf-8');
|
|
204
|
+
expect(content).toContain('https://example.com/page2.md'); // Should not change
|
|
205
|
+
expect(content).toContain('./page2-renamed.md'); // Should change
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('handles move to subdirectory', () => {
|
|
209
|
+
mkdirSync(join(testDir, 'architecture'));
|
|
210
|
+
|
|
211
|
+
writeFileSync(join(testDir, 'home.md'), 'See [Overview](./overview.md).');
|
|
212
|
+
writeFileSync(join(testDir, 'overview.md'), '# Overview');
|
|
213
|
+
|
|
214
|
+
const results = updateReferencesAfterRename(testDir, 'overview.md', 'architecture/overview.md');
|
|
215
|
+
|
|
216
|
+
expect(results.length).toBe(1);
|
|
217
|
+
|
|
218
|
+
const content = readFileSync(join(testDir, 'home.md'), 'utf-8');
|
|
219
|
+
expect(content).toContain('./architecture/overview.md');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('handles move from subdirectory to root', () => {
|
|
223
|
+
mkdirSync(join(testDir, 'architecture'));
|
|
224
|
+
|
|
225
|
+
writeFileSync(join(testDir, 'home.md'), 'See [Overview](./architecture/overview.md).');
|
|
226
|
+
writeFileSync(join(testDir, 'architecture', 'overview.md'), '# Overview');
|
|
227
|
+
|
|
228
|
+
const results = updateReferencesAfterRename(testDir, 'architecture/overview.md', 'overview.md');
|
|
229
|
+
|
|
230
|
+
expect(results.length).toBe(1);
|
|
231
|
+
|
|
232
|
+
const content = readFileSync(join(testDir, 'home.md'), 'utf-8');
|
|
233
|
+
expect(content).toContain('./overview.md');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('updates links without ./ prefix', () => {
|
|
237
|
+
mkdirSync(join(testDir, 'development'));
|
|
238
|
+
|
|
239
|
+
// Link without ./ prefix (common in markdown)
|
|
240
|
+
writeFileSync(
|
|
241
|
+
join(testDir, 'README.md'),
|
|
242
|
+
`---
|
|
243
|
+
title: README
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
See [I18n Guide](development/i18n-guidelines.md) for details.
|
|
247
|
+
`,
|
|
248
|
+
);
|
|
249
|
+
writeFileSync(join(testDir, 'development', 'i18n-guidelines.md'), '# I18n');
|
|
250
|
+
|
|
251
|
+
const results = updateReferencesAfterRename(
|
|
252
|
+
testDir,
|
|
253
|
+
'development/i18n-guidelines.md',
|
|
254
|
+
'development/internationalization-guidelines.md',
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(results.length).toBe(1);
|
|
258
|
+
expect(results[0].filePath).toBe('README.md');
|
|
259
|
+
expect(results[0].updatedCount).toBe(1);
|
|
260
|
+
|
|
261
|
+
const content = readFileSync(join(testDir, 'README.md'), 'utf-8');
|
|
262
|
+
expect(content).toContain('development/internationalization-guidelines.md');
|
|
263
|
+
expect(content).not.toContain('development/i18n-guidelines.md');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('updates both prefixed and non-prefixed links in same file', () => {
|
|
267
|
+
mkdirSync(join(testDir, 'docs'));
|
|
268
|
+
|
|
269
|
+
writeFileSync(
|
|
270
|
+
join(testDir, 'index.md'),
|
|
271
|
+
`---
|
|
272
|
+
title: Index
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
See [Guide](docs/guide.md) and also [Guide Again](./docs/guide.md).
|
|
276
|
+
`,
|
|
277
|
+
);
|
|
278
|
+
writeFileSync(join(testDir, 'docs', 'guide.md'), '# Guide');
|
|
279
|
+
|
|
280
|
+
const results = updateReferencesAfterRename(testDir, 'docs/guide.md', 'docs/user-guide.md');
|
|
281
|
+
|
|
282
|
+
expect(results.length).toBe(1);
|
|
283
|
+
expect(results[0].updatedCount).toBe(2);
|
|
284
|
+
|
|
285
|
+
const content = readFileSync(join(testDir, 'index.md'), 'utf-8');
|
|
286
|
+
// Verify both links were updated
|
|
287
|
+
expect(content).not.toContain('docs/guide.md');
|
|
288
|
+
// Verify prefix style is preserved: non-prefixed stays non-prefixed
|
|
289
|
+
expect(content).toContain('[Guide](docs/user-guide.md)');
|
|
290
|
+
// Verify prefix style is preserved: prefixed stays prefixed
|
|
291
|
+
expect(content).toContain('[Guide Again](./docs/user-guide.md)');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { resolvePageTarget } from '../lib/resolve-page-target.js';
|
|
5
|
+
|
|
6
|
+
const TMP = '/tmp/cn-test-resolve';
|
|
7
|
+
|
|
8
|
+
describe('resolvePageTarget', () => {
|
|
9
|
+
test('returns numeric string directly', () => {
|
|
10
|
+
expect(resolvePageTarget('123456')).toBe('123456');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('returns large numeric string directly', () => {
|
|
14
|
+
expect(resolvePageTarget('987654321012')).toBe('987654321012');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('extracts page_id from .md file frontmatter', () => {
|
|
18
|
+
mkdirSync(TMP, { recursive: true });
|
|
19
|
+
const file = join(TMP, 'page.md');
|
|
20
|
+
writeFileSync(
|
|
21
|
+
file,
|
|
22
|
+
`---\npage_id: "99999"\ntitle: Test\nsynced_at: "2024-01-01T00:00:00Z"\n---\n\nContent here.\n`,
|
|
23
|
+
);
|
|
24
|
+
expect(resolvePageTarget(file)).toBe('99999');
|
|
25
|
+
rmSync(file);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('extracts page_id from path containing /', () => {
|
|
29
|
+
mkdirSync(TMP, { recursive: true });
|
|
30
|
+
const file = join(TMP, 'sub.md');
|
|
31
|
+
writeFileSync(file, `---\npage_id: "77777"\ntitle: Sub\nsynced_at: "2024-01-01T00:00:00Z"\n---\n`);
|
|
32
|
+
expect(resolvePageTarget(file)).toBe('77777');
|
|
33
|
+
rmSync(file);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('throws when .md file does not exist', () => {
|
|
37
|
+
expect(() => resolvePageTarget('/nonexistent/page.md')).toThrow('File not found');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('throws when .md file has no page_id in frontmatter', () => {
|
|
41
|
+
mkdirSync(TMP, { recursive: true });
|
|
42
|
+
const file = join(TMP, 'no-id.md');
|
|
43
|
+
writeFileSync(file, `---\ntitle: No ID\n---\n\nContent.\n`);
|
|
44
|
+
expect(() => resolvePageTarget(file)).toThrow('No page_id found');
|
|
45
|
+
rmSync(file);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('throws for non-numeric non-path string', () => {
|
|
49
|
+
expect(() => resolvePageTarget('my-page-slug')).toThrow('Invalid page target');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('throws for empty string', () => {
|
|
53
|
+
expect(() => resolvePageTarget('')).toThrow();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
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 - search', () => {
|
|
13
|
+
test('returns search results', async () => {
|
|
14
|
+
const client = new ConfluenceClient(testConfig);
|
|
15
|
+
const response = await client.search('type=page AND text~"test"');
|
|
16
|
+
expect(response.results).toBeArray();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('returns empty results for folder-type CQL', async () => {
|
|
20
|
+
const client = new ConfluenceClient(testConfig);
|
|
21
|
+
const response = await client.search('type=folder AND space="TEST"');
|
|
22
|
+
expect(response.results).toBeArray();
|
|
23
|
+
expect(response.results.length).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('narrows results with --space flag via CQL', async () => {
|
|
27
|
+
let capturedCql = '';
|
|
28
|
+
server.use(
|
|
29
|
+
http.get('*/wiki/rest/api/search', ({ request }) => {
|
|
30
|
+
const url = new URL(request.url);
|
|
31
|
+
capturedCql = url.searchParams.get('cql') || '';
|
|
32
|
+
return HttpResponse.json({ results: [], totalSize: 0 });
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const client = new ConfluenceClient(testConfig);
|
|
37
|
+
await client.search('type=page AND text~"api" AND space="DOCS"');
|
|
38
|
+
expect(capturedCql).toContain('space="DOCS"');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('handles empty results gracefully', async () => {
|
|
42
|
+
server.use(
|
|
43
|
+
http.get('*/wiki/rest/api/search', () => {
|
|
44
|
+
return HttpResponse.json({ results: [], totalSize: 0 });
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const client = new ConfluenceClient(testConfig);
|
|
49
|
+
const response = await client.search('type=page AND text~"nonexistent"');
|
|
50
|
+
expect(response.results).toBeArray();
|
|
51
|
+
expect(response.results.length).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('throws on API error', async () => {
|
|
55
|
+
server.use(
|
|
56
|
+
http.get('*/wiki/rest/api/search', () => {
|
|
57
|
+
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const client = new ConfluenceClient(testConfig);
|
|
62
|
+
await expect(client.search('type=page AND text~"test"')).rejects.toThrow();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll } from 'bun:test';
|
|
2
|
+
import { setupServer } from 'msw/node';
|
|
3
|
+
import { handlers } from './mocks/handlers.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MSW (Mock Service Worker) Setup for Bun Test Environment
|
|
7
|
+
*
|
|
8
|
+
* This file configures a shared MSW server for all test files. It is preloaded
|
|
9
|
+
* via bunfig.toml to ensure global hooks run before any test code executes.
|
|
10
|
+
*
|
|
11
|
+
* ## Why preload is required:
|
|
12
|
+
*
|
|
13
|
+
* MSW works by intercepting global.fetch before any code runs. If test files
|
|
14
|
+
* directly manipulate global.fetch, they can break MSW's internal state,
|
|
15
|
+
* causing all subsequent tests to fail.
|
|
16
|
+
*
|
|
17
|
+
* Preloading this file ensures:
|
|
18
|
+
* 1. MSW's server.listen() intercepts fetch first
|
|
19
|
+
* 2. All test files share the same MSW instance
|
|
20
|
+
* 3. Test isolation via server.resetHandlers() in afterEach
|
|
21
|
+
*
|
|
22
|
+
* ## Configuration:
|
|
23
|
+
*
|
|
24
|
+
* See bunfig.toml [test] section for preload configuration.
|
|
25
|
+
*
|
|
26
|
+
* ## Usage in tests:
|
|
27
|
+
*
|
|
28
|
+
* Import server from this file and use server.use() to add test-specific handlers.
|
|
29
|
+
* Handlers added with server.use() are automatically reset after each test.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* import { server } from './setup-msw';
|
|
33
|
+
* import { http, HttpResponse } from 'msw';
|
|
34
|
+
*
|
|
35
|
+
* test('my test', async () => {
|
|
36
|
+
* server.use(
|
|
37
|
+
* http.get(endpoint, () => HttpResponse.json(data))
|
|
38
|
+
* );
|
|
39
|
+
* });
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// Shared MSW server for all tests
|
|
43
|
+
// Tests add handlers using server.use() which are automatically reset after each test
|
|
44
|
+
export const server = setupServer(...handlers);
|
|
45
|
+
|
|
46
|
+
// Track if server has been started to prevent multiple calls to server.listen()
|
|
47
|
+
let serverStarted = false;
|
|
48
|
+
|
|
49
|
+
// Global hooks - MSW server is active for all tests
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
// Ensure we're in a test environment with fetch available
|
|
52
|
+
if (typeof fetch === 'undefined') {
|
|
53
|
+
console.warn('fetch is not defined in MSW setup - skipping MSW server start');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Only start the server once, even if multiple test files import this module
|
|
58
|
+
if (!serverStarted) {
|
|
59
|
+
server.listen({
|
|
60
|
+
// Use 'warn' mode to log unhandled requests but don't fail tests
|
|
61
|
+
onUnhandledRequest: 'warn',
|
|
62
|
+
});
|
|
63
|
+
serverStarted = true;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
// Reset handlers added with server.use() back to the original handlers
|
|
69
|
+
// This prevents handler accumulation which causes performance issues and conflicts
|
|
70
|
+
server.resetHandlers();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterAll(() => {
|
|
74
|
+
server.close();
|
|
75
|
+
});
|