@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,399 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { HtmlConverter } from '../lib/markdown/html-converter.js';
|
|
3
|
+
|
|
4
|
+
describe('HtmlConverter', () => {
|
|
5
|
+
test('converts simple markdown to HTML', () => {
|
|
6
|
+
const converter = new HtmlConverter();
|
|
7
|
+
const { html } = converter.convert('Hello **world**!');
|
|
8
|
+
|
|
9
|
+
expect(html).toContain('<strong>world</strong>');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('converts headings', () => {
|
|
13
|
+
const converter = new HtmlConverter();
|
|
14
|
+
const { html } = converter.convert('# Heading 1\n## Heading 2');
|
|
15
|
+
|
|
16
|
+
expect(html).toContain('<h1>Heading 1</h1>');
|
|
17
|
+
expect(html).toContain('<h2>Heading 2</h2>');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('converts lists', () => {
|
|
21
|
+
const converter = new HtmlConverter();
|
|
22
|
+
const { html } = converter.convert('- Item 1\n- Item 2');
|
|
23
|
+
|
|
24
|
+
expect(html).toContain('<ul>');
|
|
25
|
+
expect(html).toContain('<li>');
|
|
26
|
+
expect(html).toContain('Item 1');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('converts inline formatting inside list items', () => {
|
|
30
|
+
const converter = new HtmlConverter();
|
|
31
|
+
const markdown =
|
|
32
|
+
'- **[ger](https://github.com/aaronshaf/ger)** - Gerrit CLI for code review\n- **[jk](https://github.com/aaronshaf/jk)** - Jenkins CLI';
|
|
33
|
+
const { html } = converter.convert(markdown);
|
|
34
|
+
|
|
35
|
+
expect(html).toContain('<ul>');
|
|
36
|
+
expect(html).toContain('<strong><a href="https://github.com/aaronshaf/ger">ger</a></strong>');
|
|
37
|
+
expect(html).toContain('<strong><a href="https://github.com/aaronshaf/jk">jk</a></strong>');
|
|
38
|
+
expect(html).toContain('Gerrit CLI for code review');
|
|
39
|
+
expect(html).not.toContain('**[ger]'); // Should not contain raw markdown
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('converts italic and strikethrough', () => {
|
|
43
|
+
const converter = new HtmlConverter();
|
|
44
|
+
const { html } = converter.convert('Text with *italic* and ~~strikethrough~~ formatting.');
|
|
45
|
+
|
|
46
|
+
expect(html).toContain('<em>italic</em>');
|
|
47
|
+
expect(html).toContain('<del>strikethrough</del>');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('converts line breaks and horizontal rules', () => {
|
|
51
|
+
const converter = new HtmlConverter();
|
|
52
|
+
const { html } = converter.convert('Line 1 \nLine 2\n\n---\n\nLine 3');
|
|
53
|
+
|
|
54
|
+
expect(html).toContain('<br />');
|
|
55
|
+
expect(html).toContain('<hr />');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('converts links', () => {
|
|
59
|
+
const converter = new HtmlConverter();
|
|
60
|
+
const { html } = converter.convert('[Example](https://example.com)');
|
|
61
|
+
|
|
62
|
+
expect(html).toContain('<a href="https://example.com">Example</a>');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('converts code blocks to Confluence macro', () => {
|
|
66
|
+
const converter = new HtmlConverter();
|
|
67
|
+
const markdown = '```javascript\nconst x = 1;\n```';
|
|
68
|
+
const { html } = converter.convert(markdown);
|
|
69
|
+
|
|
70
|
+
expect(html).toContain('ac:structured-macro');
|
|
71
|
+
expect(html).toContain('ac:name="code"');
|
|
72
|
+
expect(html).toContain('javascript');
|
|
73
|
+
expect(html).toContain('const x = 1;');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('converts tables', () => {
|
|
77
|
+
const converter = new HtmlConverter();
|
|
78
|
+
const markdown = '| Col 1 | Col 2 |\n|-------|-------|\n| A | B |';
|
|
79
|
+
const { html } = converter.convert(markdown);
|
|
80
|
+
|
|
81
|
+
expect(html).toContain('<table>');
|
|
82
|
+
expect(html).toContain('<th>Col 1</th>');
|
|
83
|
+
expect(html).toContain('<td>A</td>');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('converts blockquotes', () => {
|
|
87
|
+
const converter = new HtmlConverter();
|
|
88
|
+
const { html } = converter.convert('> This is a quote');
|
|
89
|
+
|
|
90
|
+
expect(html).toContain('<blockquote>');
|
|
91
|
+
expect(html).toContain('This is a quote');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('converts info panel syntax to Confluence macro', () => {
|
|
95
|
+
const converter = new HtmlConverter();
|
|
96
|
+
const { html } = converter.convert('> **Info:** This is an info message');
|
|
97
|
+
|
|
98
|
+
expect(html).toContain('ac:structured-macro');
|
|
99
|
+
expect(html).toContain('ac:name="info"');
|
|
100
|
+
expect(html).toContain('This is an info message');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('ensures XHTML compliance with self-closing tags', () => {
|
|
104
|
+
const converter = new HtmlConverter();
|
|
105
|
+
const { html } = converter.convert('Line 1\n\nLine 2\n\n---');
|
|
106
|
+
|
|
107
|
+
expect(html).toContain('<hr />');
|
|
108
|
+
expect(html).not.toContain('<hr>');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('warns about local image references', () => {
|
|
112
|
+
const converter = new HtmlConverter();
|
|
113
|
+
const { warnings } = converter.convert('');
|
|
114
|
+
|
|
115
|
+
expect(warnings.length).toBeGreaterThan(0);
|
|
116
|
+
expect(warnings.some((w) => w.includes('local-image.png'))).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('strips @ prefix from mentions', () => {
|
|
120
|
+
const converter = new HtmlConverter();
|
|
121
|
+
const { html, warnings } = converter.convert('Contact @user123 for help.');
|
|
122
|
+
|
|
123
|
+
// @ should be stripped, leaving just the username
|
|
124
|
+
expect(html).toContain('user123');
|
|
125
|
+
expect(html).not.toContain('@user123');
|
|
126
|
+
// No warning since we handle it by stripping
|
|
127
|
+
expect(warnings.some((w) => w.includes('mentions'))).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('does not strip @ from email addresses', () => {
|
|
131
|
+
const converter = new HtmlConverter();
|
|
132
|
+
const { html } = converter.convert('Email us at support@example.com for help.');
|
|
133
|
+
|
|
134
|
+
// Email addresses should be preserved
|
|
135
|
+
expect(html).toContain('support@example.com');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('warns about task lists', () => {
|
|
139
|
+
const converter = new HtmlConverter();
|
|
140
|
+
const { warnings } = converter.convert('- [x] Done\n- [ ] Todo');
|
|
141
|
+
|
|
142
|
+
expect(warnings.length).toBeGreaterThan(0);
|
|
143
|
+
expect(warnings.some((w) => w.includes('Task list'))).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('escapes CDATA sections in code blocks', () => {
|
|
147
|
+
const converter = new HtmlConverter();
|
|
148
|
+
const markdown = '```javascript\nconst x = "]]>";\n```';
|
|
149
|
+
const { html } = converter.convert(markdown);
|
|
150
|
+
|
|
151
|
+
// Should escape ]]> to prevent breaking CDATA
|
|
152
|
+
expect(html).toContain(']]]]><![CDATA[>');
|
|
153
|
+
expect(html).not.toContain('const x = "]]>";');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('sanitizes language identifiers in code blocks', () => {
|
|
157
|
+
const converter = new HtmlConverter();
|
|
158
|
+
const markdown = '```javascript"><script>alert("xss")</script><x lang="evil\nconsole.log("test");\n```';
|
|
159
|
+
const { html } = converter.convert(markdown);
|
|
160
|
+
|
|
161
|
+
// Should strip dangerous characters from language (quotes, angle brackets, etc.)
|
|
162
|
+
expect(html).not.toContain('"><script>');
|
|
163
|
+
expect(html).not.toContain('<x lang="evil');
|
|
164
|
+
// Should keep alphanumeric parts
|
|
165
|
+
expect(html).toContain('javascript');
|
|
166
|
+
// The sanitized version should only have safe characters
|
|
167
|
+
expect(html).toMatch(/<ac:parameter ac:name="language">[a-zA-Z0-9\-_+]*<\/ac:parameter>/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('sanitizes script tags in HTML passthrough', () => {
|
|
171
|
+
const converter = new HtmlConverter();
|
|
172
|
+
// Use block-level HTML which marked processes through the html renderer
|
|
173
|
+
const markdown = '<div>\nHello\n</div>\n<script>alert("xss")</script>\n<div>\nworld\n</div>';
|
|
174
|
+
const { html, warnings } = converter.convert(markdown);
|
|
175
|
+
|
|
176
|
+
// Should remove script tags
|
|
177
|
+
expect(html).not.toContain('<script>');
|
|
178
|
+
expect(html).not.toContain('alert("xss")');
|
|
179
|
+
expect(html).toContain('Hello');
|
|
180
|
+
expect(html).toContain('world');
|
|
181
|
+
// Should warn about sanitization
|
|
182
|
+
expect(warnings.some((w) => w.includes('unsafe HTML'))).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('sanitizes event handlers in HTML passthrough', () => {
|
|
186
|
+
const converter = new HtmlConverter();
|
|
187
|
+
const markdown = '<div onclick="alert(\'xss\')">Click me</div>';
|
|
188
|
+
const { html, warnings } = converter.convert(markdown);
|
|
189
|
+
|
|
190
|
+
// Should remove event handlers
|
|
191
|
+
expect(html).not.toContain('onclick');
|
|
192
|
+
expect(html).not.toContain('alert');
|
|
193
|
+
expect(html).toContain('Click me');
|
|
194
|
+
// Should warn about sanitization
|
|
195
|
+
expect(warnings.some((w) => w.includes('unsafe HTML'))).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('sanitizes javascript: protocol in links', () => {
|
|
199
|
+
const converter = new HtmlConverter();
|
|
200
|
+
const markdown = '<a href="javascript:alert(\'xss\')">Click</a>';
|
|
201
|
+
const { html, warnings } = converter.convert(markdown);
|
|
202
|
+
|
|
203
|
+
// Should replace javascript: with safe URL
|
|
204
|
+
expect(html).not.toContain('javascript:');
|
|
205
|
+
expect(html).toContain('href="#"');
|
|
206
|
+
expect(html).toContain('Click');
|
|
207
|
+
// Should warn about sanitization
|
|
208
|
+
expect(warnings.some((w) => w.includes('unsafe HTML'))).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('allows safe HTML passthrough', () => {
|
|
212
|
+
const converter = new HtmlConverter();
|
|
213
|
+
const markdown = '<div class="highlight">Safe HTML</div>';
|
|
214
|
+
const { html, warnings } = converter.convert(markdown);
|
|
215
|
+
|
|
216
|
+
// Should pass through safe HTML
|
|
217
|
+
expect(html).toContain('<div class="highlight">');
|
|
218
|
+
expect(html).toContain('Safe HTML');
|
|
219
|
+
// Should not warn
|
|
220
|
+
expect(warnings.some((w) => w.includes('unsafe HTML'))).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('converts relative .md links to Confluence page links', () => {
|
|
224
|
+
const converter = new HtmlConverter();
|
|
225
|
+
const pageLookupMap = {
|
|
226
|
+
titleToPage: new Map(),
|
|
227
|
+
idToPage: new Map([
|
|
228
|
+
[
|
|
229
|
+
'page-123',
|
|
230
|
+
{
|
|
231
|
+
pageId: 'page-123',
|
|
232
|
+
version: 1,
|
|
233
|
+
localPath: 'architecture/overview.md',
|
|
234
|
+
title: 'Architecture Overview',
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
]),
|
|
238
|
+
pathToPage: new Map([
|
|
239
|
+
[
|
|
240
|
+
'architecture/overview.md',
|
|
241
|
+
{
|
|
242
|
+
pageId: 'page-123',
|
|
243
|
+
version: 1,
|
|
244
|
+
localPath: 'architecture/overview.md',
|
|
245
|
+
title: 'Architecture Overview',
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
]),
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const markdown = 'See [Architecture](./architecture/overview.md) for details.';
|
|
252
|
+
const { html } = converter.convert(markdown, '/test/space', 'home.md', 'TEST', pageLookupMap);
|
|
253
|
+
|
|
254
|
+
expect(html).toContain('<ac:link>');
|
|
255
|
+
expect(html).toContain('ri:page ri:content-title="Architecture Overview"');
|
|
256
|
+
expect(html).toContain('ri:space-key="TEST"');
|
|
257
|
+
expect(html).toContain('<ac:plain-text-link-body><![CDATA[Architecture]]></ac:plain-text-link-body>');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('warns about broken local links', () => {
|
|
261
|
+
const converter = new HtmlConverter();
|
|
262
|
+
const pageLookupMap = {
|
|
263
|
+
titleToPage: new Map(),
|
|
264
|
+
idToPage: new Map(),
|
|
265
|
+
pathToPage: new Map(),
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const markdown = 'See [Missing](./non-existent.md).';
|
|
269
|
+
const { warnings } = converter.convert(markdown, '/test/space', 'home.md', 'TEST', pageLookupMap);
|
|
270
|
+
|
|
271
|
+
expect(warnings.length).toBeGreaterThan(0);
|
|
272
|
+
expect(warnings.some((w) => w.includes('non-existent.md'))).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('preserves external links unchanged', () => {
|
|
276
|
+
const converter = new HtmlConverter();
|
|
277
|
+
const pageLookupMap = {
|
|
278
|
+
titleToPage: new Map(),
|
|
279
|
+
idToPage: new Map(),
|
|
280
|
+
pathToPage: new Map(),
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const markdown = 'See [Google](https://google.com) and [Example](https://example.com/page.md).';
|
|
284
|
+
const { html } = converter.convert(markdown, '/test/space', 'home.md', 'TEST', pageLookupMap);
|
|
285
|
+
|
|
286
|
+
expect(html).toContain('<a href="https://google.com">Google</a>');
|
|
287
|
+
expect(html).toContain('<a href="https://example.com/page.md">Example</a>');
|
|
288
|
+
expect(html).not.toContain('<ac:link>');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('properly escapes XML special characters in link titles', () => {
|
|
292
|
+
const converter = new HtmlConverter();
|
|
293
|
+
const pageLookupMap = {
|
|
294
|
+
titleToPage: new Map(),
|
|
295
|
+
idToPage: new Map([
|
|
296
|
+
[
|
|
297
|
+
'page-123',
|
|
298
|
+
{
|
|
299
|
+
pageId: 'page-123',
|
|
300
|
+
version: 1,
|
|
301
|
+
localPath: 'special.md',
|
|
302
|
+
title: 'Page with <Special> & "Chars"',
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
]),
|
|
306
|
+
pathToPage: new Map([
|
|
307
|
+
[
|
|
308
|
+
'special.md',
|
|
309
|
+
{
|
|
310
|
+
pageId: 'page-123',
|
|
311
|
+
version: 1,
|
|
312
|
+
localPath: 'special.md',
|
|
313
|
+
title: 'Page with <Special> & "Chars"',
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
]),
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const markdown = 'See [Special Page](./special.md).';
|
|
320
|
+
const { html } = converter.convert(markdown, '/test/space', 'home.md', 'TEST', pageLookupMap);
|
|
321
|
+
|
|
322
|
+
// Verify XML special characters are properly escaped
|
|
323
|
+
expect(html).toContain('ri:content-title="Page with <Special> & "Chars""');
|
|
324
|
+
expect(html).not.toContain('ri:content-title="Page with <Special> & "Chars""');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('handles links in code blocks without converting them', () => {
|
|
328
|
+
const converter = new HtmlConverter();
|
|
329
|
+
const pageLookupMap = {
|
|
330
|
+
titleToPage: new Map(),
|
|
331
|
+
idToPage: new Map([
|
|
332
|
+
[
|
|
333
|
+
'page-123',
|
|
334
|
+
{
|
|
335
|
+
pageId: 'page-123',
|
|
336
|
+
version: 1,
|
|
337
|
+
localPath: 'page.md',
|
|
338
|
+
title: 'Test Page',
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
]),
|
|
342
|
+
pathToPage: new Map([
|
|
343
|
+
[
|
|
344
|
+
'page.md',
|
|
345
|
+
{
|
|
346
|
+
pageId: 'page-123',
|
|
347
|
+
version: 1,
|
|
348
|
+
localPath: 'page.md',
|
|
349
|
+
title: 'Test Page',
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
]),
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const markdown = '```\n[Link](./page.md)\n```';
|
|
356
|
+
const { html } = converter.convert(markdown, '/test/space', 'home.md', 'TEST', pageLookupMap);
|
|
357
|
+
|
|
358
|
+
// Links inside code blocks should NOT be converted to Confluence links
|
|
359
|
+
expect(html).not.toContain('<ac:link>');
|
|
360
|
+
expect(html).toContain('[Link](./page.md)');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('handles inline code with link-like syntax without converting', () => {
|
|
364
|
+
const converter = new HtmlConverter();
|
|
365
|
+
const pageLookupMap = {
|
|
366
|
+
titleToPage: new Map(),
|
|
367
|
+
idToPage: new Map([
|
|
368
|
+
[
|
|
369
|
+
'page-123',
|
|
370
|
+
{
|
|
371
|
+
pageId: 'page-123',
|
|
372
|
+
version: 1,
|
|
373
|
+
localPath: 'page.md',
|
|
374
|
+
title: 'Test Page',
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
]),
|
|
378
|
+
pathToPage: new Map([
|
|
379
|
+
[
|
|
380
|
+
'page.md',
|
|
381
|
+
{
|
|
382
|
+
pageId: 'page-123',
|
|
383
|
+
version: 1,
|
|
384
|
+
localPath: 'page.md',
|
|
385
|
+
title: 'Test Page',
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
]),
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const markdown = 'Use the syntax `[Link](./page.md)` in markdown.';
|
|
392
|
+
const { html } = converter.convert(markdown, '/test/space', 'home.md', 'TEST', pageLookupMap);
|
|
393
|
+
|
|
394
|
+
// Inline code should NOT be converted
|
|
395
|
+
expect(html).toContain('<code>[Link](./page.md)</code>');
|
|
396
|
+
// But if we have a real link, it should be converted
|
|
397
|
+
expect(html.match(/<ac:link>/g)).toBeNull();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
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 { createValidPage } 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 - getPage (info)', () => {
|
|
14
|
+
test('returns page without body when includeBody=false', async () => {
|
|
15
|
+
const page = createValidPage({ id: 'page-123', title: 'My Page' });
|
|
16
|
+
server.use(
|
|
17
|
+
http.get('*/wiki/api/v2/pages/:pageId', () => {
|
|
18
|
+
return HttpResponse.json(page);
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const client = new ConfluenceClient(testConfig);
|
|
23
|
+
const result = await client.getPage('page-123', false);
|
|
24
|
+
expect(result.id).toBe('page-123');
|
|
25
|
+
expect(result.title).toBe('My Page');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('returns labels for a page', async () => {
|
|
29
|
+
server.use(
|
|
30
|
+
http.get('*/wiki/api/v2/pages/:pageId/labels', () => {
|
|
31
|
+
return HttpResponse.json({
|
|
32
|
+
results: [
|
|
33
|
+
{ id: 'lbl-1', name: 'docs', prefix: 'global' },
|
|
34
|
+
{ id: 'lbl-2', name: 'public', prefix: 'global' },
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const client = new ConfluenceClient(testConfig);
|
|
41
|
+
const labels = await client.getAllLabels('page-123');
|
|
42
|
+
expect(labels.length).toBe(2);
|
|
43
|
+
expect(labels[0].name).toBe('docs');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('throws on 404', async () => {
|
|
47
|
+
server.use(
|
|
48
|
+
http.get('*/wiki/api/v2/pages/:pageId', () => {
|
|
49
|
+
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const client = new ConfluenceClient(testConfig);
|
|
54
|
+
await expect(client.getPage('nonexistent', false)).rejects.toThrow();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
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 - label operations', () => {
|
|
13
|
+
test('lists labels for a page', async () => {
|
|
14
|
+
server.use(
|
|
15
|
+
http.get('*/wiki/api/v2/pages/:pageId/labels', () => {
|
|
16
|
+
return HttpResponse.json({
|
|
17
|
+
results: [
|
|
18
|
+
{ id: 'label-1', name: 'documentation', prefix: 'global' },
|
|
19
|
+
{ id: 'label-2', name: 'draft', prefix: 'global' },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const client = new ConfluenceClient(testConfig);
|
|
26
|
+
const labels = await client.getAllLabels('page-123');
|
|
27
|
+
expect(labels.length).toBe(2);
|
|
28
|
+
expect(labels[0].name).toBe('documentation');
|
|
29
|
+
expect(labels[1].name).toBe('draft');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('adds a label', async () => {
|
|
33
|
+
let requestBody: unknown;
|
|
34
|
+
server.use(
|
|
35
|
+
http.post('*/wiki/rest/api/content/:pageId/label', async ({ request }) => {
|
|
36
|
+
requestBody = await request.json();
|
|
37
|
+
return HttpResponse.json([]);
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const client = new ConfluenceClient(testConfig);
|
|
42
|
+
await client.addLabel('page-123', 'documentation');
|
|
43
|
+
expect(requestBody).toEqual([{ prefix: 'global', name: 'documentation' }]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('removes a label', async () => {
|
|
47
|
+
let deletedLabel = '';
|
|
48
|
+
server.use(
|
|
49
|
+
http.delete('*/wiki/rest/api/content/:pageId/label/:labelName', ({ params }) => {
|
|
50
|
+
deletedLabel = params.labelName as string;
|
|
51
|
+
return new HttpResponse(null, { status: 204 });
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const client = new ConfluenceClient(testConfig);
|
|
56
|
+
await client.removeLabel('page-123', 'draft');
|
|
57
|
+
expect(deletedLabel).toBe('draft');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('throws on 401 when adding label', async () => {
|
|
61
|
+
server.use(
|
|
62
|
+
http.post('*/wiki/rest/api/content/:pageId/label', () => {
|
|
63
|
+
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const client = new ConfluenceClient(testConfig);
|
|
68
|
+
await expect(client.addLabel('page-123', 'documentation')).rejects.toThrow();
|
|
69
|
+
});
|
|
70
|
+
});
|