confluence-cli 1.18.0 → 1.20.0
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/.github/workflows/ci.yml +3 -2
- package/CHANGELOG.md +14 -0
- package/README.md +8 -5
- package/bin/confluence.js +53 -15
- package/lib/config.js +17 -17
- package/lib/confluence-client.js +4 -3
- package/package.json +1 -1
- package/tests/config.test.js +79 -0
- package/tests/confluence-client.test.js +123 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -31,8 +31,9 @@ jobs:
|
|
|
31
31
|
runs-on: ubuntu-latest
|
|
32
32
|
steps:
|
|
33
33
|
- uses: actions/checkout@v3
|
|
34
|
-
-
|
|
35
|
-
|
|
34
|
+
- run: npm ci
|
|
35
|
+
- name: Run npm audit (production only)
|
|
36
|
+
run: npm audit --audit-level moderate --omit=dev
|
|
36
37
|
|
|
37
38
|
publish:
|
|
38
39
|
needs: [test, security]
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.20.0](https://github.com/pchuri/confluence-cli/compare/v1.19.0...v1.20.0) (2026-02-26)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add JSON output format to attachments command ([#45](https://github.com/pchuri/confluence-cli/issues/45)) ([b512ffb](https://github.com/pchuri/confluence-cli/commit/b512ffbea6cd083879f5030c10db72cef32302c2)), closes [#44](https://github.com/pchuri/confluence-cli/issues/44)
|
|
7
|
+
|
|
8
|
+
# [1.19.0](https://github.com/pchuri/confluence-cli/compare/v1.18.0...v1.19.0) (2026-02-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add --cql flag to search command for raw CQL queries ([#40](https://github.com/pchuri/confluence-cli/issues/40)) ([311f5a9](https://github.com/pchuri/confluence-cli/commit/311f5a98bfd175c6b7902c55b4dd0687b2a0d8c0)), closes [#39](https://github.com/pchuri/confluence-cli/issues/39)
|
|
14
|
+
|
|
1
15
|
# [1.18.0](https://github.com/pchuri/confluence-cli/compare/v1.17.0...v1.18.0) (2026-02-15)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ npx confluence-cli
|
|
|
68
68
|
confluence init
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
The wizard helps you choose the right API endpoint and authentication method. It recommends `/wiki/rest/api` for Atlassian Cloud domains (e.g., `*.atlassian.net`) and `/rest/api` for self-hosted/Data Center instances, then prompts for Basic (email + token) or Bearer authentication.
|
|
71
|
+
The wizard helps you choose the right API endpoint and authentication method. It recommends `/wiki/rest/api` for Atlassian Cloud domains (e.g., `*.atlassian.net`) and `/rest/api` for self-hosted/Data Center instances, then prompts for Basic (email/username + token/password) or Bearer authentication.
|
|
72
72
|
|
|
73
73
|
### Option 2: Non-interactive Setup (CLI Flags)
|
|
74
74
|
|
|
@@ -97,16 +97,16 @@ confluence init --email "user@example.com" --token "your-api-token"
|
|
|
97
97
|
- `-d, --domain <domain>` - Confluence domain (e.g., `company.atlassian.net`)
|
|
98
98
|
- `-p, --api-path <path>` - REST API path (e.g., `/wiki/rest/api`)
|
|
99
99
|
- `-a, --auth-type <type>` - Authentication type: `basic` or `bearer`
|
|
100
|
-
- `-e, --email <email>` - Email for basic authentication
|
|
101
|
-
- `-t, --token <token>` - API token
|
|
100
|
+
- `-e, --email <email>` - Email or username for basic authentication
|
|
101
|
+
- `-t, --token <token>` - API token or password
|
|
102
102
|
|
|
103
103
|
⚠️ **Security note:** While flags work, storing tokens in shell history is risky. Prefer environment variables (Option 3) for production environments.
|
|
104
104
|
|
|
105
105
|
### Option 3: Environment Variables
|
|
106
106
|
```bash
|
|
107
107
|
export CONFLUENCE_DOMAIN="your-domain.atlassian.net"
|
|
108
|
-
export CONFLUENCE_API_TOKEN="your-api-token"
|
|
109
|
-
export CONFLUENCE_EMAIL="your.email@example.com" # required
|
|
108
|
+
export CONFLUENCE_API_TOKEN="your-api-token" # or password for on-premise (alias: CONFLUENCE_PASSWORD)
|
|
109
|
+
export CONFLUENCE_EMAIL="your.email@example.com" # required for basic auth (alias: CONFLUENCE_USERNAME for on-premise)
|
|
110
110
|
export CONFLUENCE_API_PATH="/wiki/rest/api" # Cloud default; use /rest/api for Server/DC
|
|
111
111
|
# Optional: set to 'bearer' for self-hosted/Data Center instances
|
|
112
112
|
export CONFLUENCE_AUTH_TYPE="basic"
|
|
@@ -116,11 +116,14 @@ export CONFLUENCE_AUTH_TYPE="basic"
|
|
|
116
116
|
|
|
117
117
|
### Getting Your API Token
|
|
118
118
|
|
|
119
|
+
**Atlassian Cloud:**
|
|
119
120
|
1. Go to [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens)
|
|
120
121
|
2. Click "Create API token"
|
|
121
122
|
3. Give it a label (e.g., "confluence-cli")
|
|
122
123
|
4. Copy the generated token
|
|
123
124
|
|
|
125
|
+
**On-premise / Data Center:** Use your Confluence username and password for basic authentication.
|
|
126
|
+
|
|
124
127
|
## Usage
|
|
125
128
|
|
|
126
129
|
### Read a Page
|
package/bin/confluence.js
CHANGED
|
@@ -20,7 +20,7 @@ program
|
|
|
20
20
|
.option('-d, --domain <domain>', 'Confluence domain')
|
|
21
21
|
.option('-p, --api-path <path>', 'REST API path')
|
|
22
22
|
.option('-a, --auth-type <type>', 'Authentication type (basic or bearer)')
|
|
23
|
-
.option('-e, --email <email>', 'Email for basic auth')
|
|
23
|
+
.option('-e, --email <email>', 'Email or username for basic auth')
|
|
24
24
|
.option('-t, --token <token>', 'API token')
|
|
25
25
|
.action(async (options) => {
|
|
26
26
|
await initConfig(options);
|
|
@@ -75,11 +75,12 @@ program
|
|
|
75
75
|
.command('search <query>')
|
|
76
76
|
.description('Search for Confluence pages')
|
|
77
77
|
.option('-l, --limit <limit>', 'Limit number of results', '10')
|
|
78
|
+
.option('--cql', 'Pass query as raw CQL instead of text search')
|
|
78
79
|
.action(async (query, options) => {
|
|
79
80
|
const analytics = new Analytics();
|
|
80
81
|
try {
|
|
81
82
|
const client = new ConfluenceClient(getConfig());
|
|
82
|
-
const results = await client.search(query, parseInt(options.limit));
|
|
83
|
+
const results = await client.search(query, parseInt(options.limit), options.cql);
|
|
83
84
|
|
|
84
85
|
if (results.length === 0) {
|
|
85
86
|
console.log(chalk.yellow('No results found.'));
|
|
@@ -418,6 +419,7 @@ program
|
|
|
418
419
|
.option('-p, --pattern <glob>', 'Filter attachments by filename (e.g., "*.png")')
|
|
419
420
|
.option('-d, --download', 'Download matching attachments')
|
|
420
421
|
.option('--dest <directory>', 'Directory to save downloads (default: current directory)', '.')
|
|
422
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
421
423
|
.action(async (pageId, options) => {
|
|
422
424
|
const analytics = new Analytics();
|
|
423
425
|
try {
|
|
@@ -430,22 +432,47 @@ program
|
|
|
430
432
|
throw new Error('Limit must be a positive number.');
|
|
431
433
|
}
|
|
432
434
|
|
|
435
|
+
const format = (options.format || 'text').toLowerCase();
|
|
436
|
+
if (!['text', 'json'].includes(format)) {
|
|
437
|
+
throw new Error('Format must be one of: text, json');
|
|
438
|
+
}
|
|
439
|
+
|
|
433
440
|
const attachments = await client.getAllAttachments(pageId, { maxResults });
|
|
434
441
|
const filtered = pattern ? attachments.filter(att => client.matchesPattern(att.title, pattern)) : attachments;
|
|
435
442
|
|
|
436
443
|
if (filtered.length === 0) {
|
|
437
|
-
|
|
444
|
+
if (format === 'json') {
|
|
445
|
+
console.log(JSON.stringify({ attachmentCount: 0, attachments: [] }, null, 2));
|
|
446
|
+
} else {
|
|
447
|
+
console.log(chalk.yellow('No attachments found.'));
|
|
448
|
+
}
|
|
438
449
|
analytics.track('attachments', true);
|
|
439
450
|
return;
|
|
440
451
|
}
|
|
441
452
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
453
|
+
if (format === 'json' && !options.download) {
|
|
454
|
+
const output = {
|
|
455
|
+
attachmentCount: filtered.length,
|
|
456
|
+
attachments: filtered.map(att => ({
|
|
457
|
+
id: att.id,
|
|
458
|
+
title: att.title,
|
|
459
|
+
mediaType: att.mediaType || '',
|
|
460
|
+
fileSize: att.fileSize,
|
|
461
|
+
fileSizeFormatted: att.fileSize ? `${Math.max(1, Math.round(att.fileSize / 1024))} KB` : 'unknown size',
|
|
462
|
+
version: att.version,
|
|
463
|
+
downloadLink: att.downloadLink
|
|
464
|
+
}))
|
|
465
|
+
};
|
|
466
|
+
console.log(JSON.stringify(output, null, 2));
|
|
467
|
+
} else if (!options.download) {
|
|
468
|
+
console.log(chalk.blue(`Found ${filtered.length} attachment${filtered.length === 1 ? '' : 's'}:`));
|
|
469
|
+
filtered.forEach((att, index) => {
|
|
470
|
+
const sizeKb = att.fileSize ? `${Math.max(1, Math.round(att.fileSize / 1024))} KB` : 'unknown size';
|
|
471
|
+
const typeLabel = att.mediaType || 'unknown';
|
|
472
|
+
console.log(`${index + 1}. ${chalk.green(att.title)} (ID: ${att.id})`);
|
|
473
|
+
console.log(` Type: ${chalk.gray(typeLabel)} • Size: ${chalk.gray(sizeKb)} • Version: ${chalk.gray(att.version)}`);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
449
476
|
|
|
450
477
|
if (options.download) {
|
|
451
478
|
const fs = require('fs');
|
|
@@ -474,17 +501,28 @@ program
|
|
|
474
501
|
writer.on('finish', resolve);
|
|
475
502
|
});
|
|
476
503
|
|
|
477
|
-
|
|
504
|
+
const downloadResults = [];
|
|
478
505
|
for (const attachment of filtered) {
|
|
479
506
|
const targetPath = uniquePathFor(destDir, attachment.title);
|
|
480
|
-
// Pass the full attachment object so downloadAttachment can use downloadLink directly
|
|
481
507
|
const dataStream = await client.downloadAttachment(pageId, attachment);
|
|
482
508
|
await writeStream(dataStream, targetPath);
|
|
483
|
-
|
|
484
|
-
|
|
509
|
+
downloadResults.push({ title: attachment.title, id: attachment.id, savedTo: targetPath });
|
|
510
|
+
if (format !== 'json') {
|
|
511
|
+
console.log(`⬇️ ${chalk.green(attachment.title)} -> ${chalk.gray(targetPath)}`);
|
|
512
|
+
}
|
|
485
513
|
}
|
|
486
514
|
|
|
487
|
-
|
|
515
|
+
if (format === 'json') {
|
|
516
|
+
const output = {
|
|
517
|
+
attachmentCount: filtered.length,
|
|
518
|
+
downloaded: downloadResults.length,
|
|
519
|
+
destination: destDir,
|
|
520
|
+
attachments: downloadResults
|
|
521
|
+
};
|
|
522
|
+
console.log(JSON.stringify(output, null, 2));
|
|
523
|
+
} else {
|
|
524
|
+
console.log(chalk.green(`Downloaded ${downloadResults.length} attachment${downloadResults.length === 1 ? '' : 's'} to ${destDir}`));
|
|
525
|
+
}
|
|
488
526
|
}
|
|
489
527
|
|
|
490
528
|
analytics.track('attachments', true);
|
package/lib/config.js
CHANGED
|
@@ -8,7 +8,7 @@ const CONFIG_DIR = path.join(os.homedir(), '.confluence-cli');
|
|
|
8
8
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
9
9
|
|
|
10
10
|
const AUTH_CHOICES = [
|
|
11
|
-
{ name: 'Basic (
|
|
11
|
+
{ name: 'Basic (credentials)', value: 'basic' },
|
|
12
12
|
{ name: 'Bearer token', value: 'bearer' }
|
|
13
13
|
];
|
|
14
14
|
|
|
@@ -91,7 +91,7 @@ const validateCliOptions = (options) => {
|
|
|
91
91
|
// Check if basic auth is provided with email
|
|
92
92
|
const normAuthType = options.authType ? normalizeAuthType(options.authType, Boolean(options.email)) : null;
|
|
93
93
|
if (normAuthType === 'basic' && !options.email) {
|
|
94
|
-
errors.push('--email is required when using basic authentication');
|
|
94
|
+
errors.push('--email is required when using basic authentication (use your username for on-premise)');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
return errors;
|
|
@@ -175,12 +175,12 @@ const promptForMissingValues = async (providedValues) => {
|
|
|
175
175
|
questions.push({
|
|
176
176
|
type: 'input',
|
|
177
177
|
name: 'email',
|
|
178
|
-
message: '
|
|
178
|
+
message: 'Email / username:',
|
|
179
179
|
when: (responses) => {
|
|
180
180
|
const authType = providedValues.authType || responses.authType;
|
|
181
181
|
return authType === 'basic';
|
|
182
182
|
},
|
|
183
|
-
validate: requiredInput('Email')
|
|
183
|
+
validate: requiredInput('Email / username')
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
186
|
|
|
@@ -189,8 +189,8 @@ const promptForMissingValues = async (providedValues) => {
|
|
|
189
189
|
questions.push({
|
|
190
190
|
type: 'password',
|
|
191
191
|
name: 'token',
|
|
192
|
-
message: 'API
|
|
193
|
-
validate: requiredInput('API
|
|
192
|
+
message: 'API token / password:',
|
|
193
|
+
validate: requiredInput('API token / password')
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -258,15 +258,15 @@ async function initConfig(cliOptions = {}) {
|
|
|
258
258
|
{
|
|
259
259
|
type: 'input',
|
|
260
260
|
name: 'email',
|
|
261
|
-
message: '
|
|
261
|
+
message: 'Email / username:',
|
|
262
262
|
when: (responses) => responses.authType === 'basic',
|
|
263
|
-
validate: requiredInput('Email')
|
|
263
|
+
validate: requiredInput('Email / username')
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
266
|
type: 'password',
|
|
267
267
|
name: 'token',
|
|
268
|
-
message: 'API
|
|
269
|
-
validate: requiredInput('API
|
|
268
|
+
message: 'API token / password:',
|
|
269
|
+
validate: requiredInput('API token / password')
|
|
270
270
|
}
|
|
271
271
|
]);
|
|
272
272
|
|
|
@@ -351,8 +351,8 @@ async function initConfig(cliOptions = {}) {
|
|
|
351
351
|
|
|
352
352
|
function getConfig() {
|
|
353
353
|
const envDomain = process.env.CONFLUENCE_DOMAIN || process.env.CONFLUENCE_HOST;
|
|
354
|
-
const envToken = process.env.CONFLUENCE_API_TOKEN;
|
|
355
|
-
const envEmail = process.env.CONFLUENCE_EMAIL;
|
|
354
|
+
const envToken = process.env.CONFLUENCE_API_TOKEN || process.env.CONFLUENCE_PASSWORD;
|
|
355
|
+
const envEmail = process.env.CONFLUENCE_EMAIL || process.env.CONFLUENCE_USERNAME;
|
|
356
356
|
const envAuthType = process.env.CONFLUENCE_AUTH_TYPE;
|
|
357
357
|
const envApiPath = process.env.CONFLUENCE_API_PATH;
|
|
358
358
|
|
|
@@ -368,8 +368,8 @@ function getConfig() {
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
if (authType === 'basic' && !envEmail) {
|
|
371
|
-
console.error(chalk.red('❌ Basic authentication requires CONFLUENCE_EMAIL.'));
|
|
372
|
-
console.log(chalk.yellow('Set CONFLUENCE_EMAIL or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
|
|
371
|
+
console.error(chalk.red('❌ Basic authentication requires CONFLUENCE_EMAIL or CONFLUENCE_USERNAME.'));
|
|
372
|
+
console.log(chalk.yellow('Set CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME for on-premise) or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
|
|
373
373
|
process.exit(1);
|
|
374
374
|
}
|
|
375
375
|
|
|
@@ -385,7 +385,7 @@ function getConfig() {
|
|
|
385
385
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
386
386
|
console.error(chalk.red('❌ No configuration found!'));
|
|
387
387
|
console.log(chalk.yellow('Please run "confluence init" to set up your configuration.'));
|
|
388
|
-
console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN, CONFLUENCE_EMAIL, and optionally CONFLUENCE_API_PATH.'));
|
|
388
|
+
console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN (or CONFLUENCE_PASSWORD), CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME), and optionally CONFLUENCE_API_PATH.'));
|
|
389
389
|
process.exit(1);
|
|
390
390
|
}
|
|
391
391
|
|
|
@@ -404,8 +404,8 @@ function getConfig() {
|
|
|
404
404
|
}
|
|
405
405
|
|
|
406
406
|
if (authType === 'basic' && !trimmedEmail) {
|
|
407
|
-
console.error(chalk.red('❌ Basic authentication requires an email address.'));
|
|
408
|
-
console.log(chalk.yellow('Please rerun "confluence init" to add your Confluence email.'));
|
|
407
|
+
console.error(chalk.red('❌ Basic authentication requires an email address or username.'));
|
|
408
|
+
console.log(chalk.yellow('Please rerun "confluence init" to add your Confluence email or username.'));
|
|
409
409
|
process.exit(1);
|
|
410
410
|
}
|
|
411
411
|
|
package/lib/confluence-client.js
CHANGED
|
@@ -42,7 +42,7 @@ class ConfluenceClient {
|
|
|
42
42
|
|
|
43
43
|
buildBasicAuthHeader() {
|
|
44
44
|
if (!this.email) {
|
|
45
|
-
throw new Error('Basic authentication requires an email address.');
|
|
45
|
+
throw new Error('Basic authentication requires an email address or username.');
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const encodedCredentials = Buffer.from(`${this.email}:${this.token}`).toString('base64');
|
|
@@ -221,10 +221,11 @@ class ConfluenceClient {
|
|
|
221
221
|
/**
|
|
222
222
|
* Search for pages
|
|
223
223
|
*/
|
|
224
|
-
async search(query, limit = 10) {
|
|
224
|
+
async search(query, limit = 10, rawCql = false) {
|
|
225
|
+
const cql = rawCql ? query : `text ~ "${String(query).replace(/"/g, '\\"')}"`;
|
|
225
226
|
const response = await this.client.get('/search', {
|
|
226
227
|
params: {
|
|
227
|
-
cql
|
|
228
|
+
cql,
|
|
228
229
|
limit: limit
|
|
229
230
|
}
|
|
230
231
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { getConfig } = require('../lib/config');
|
|
2
|
+
|
|
3
|
+
// Save and restore all relevant env vars around each test
|
|
4
|
+
const ENV_KEYS = [
|
|
5
|
+
'CONFLUENCE_DOMAIN', 'CONFLUENCE_HOST',
|
|
6
|
+
'CONFLUENCE_API_TOKEN', 'CONFLUENCE_PASSWORD',
|
|
7
|
+
'CONFLUENCE_EMAIL', 'CONFLUENCE_USERNAME',
|
|
8
|
+
'CONFLUENCE_AUTH_TYPE', 'CONFLUENCE_API_PATH'
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
describe('getConfig env var aliases', () => {
|
|
12
|
+
const saved = {};
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
for (const key of ENV_KEYS) {
|
|
16
|
+
saved[key] = process.env[key];
|
|
17
|
+
delete process.env[key];
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const key of ENV_KEYS) {
|
|
23
|
+
if (saved[key] !== undefined) {
|
|
24
|
+
process.env[key] = saved[key];
|
|
25
|
+
} else {
|
|
26
|
+
delete process.env[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('CONFLUENCE_USERNAME is used when CONFLUENCE_EMAIL is not set', () => {
|
|
32
|
+
process.env.CONFLUENCE_DOMAIN = 'on-prem.example.com';
|
|
33
|
+
process.env.CONFLUENCE_PASSWORD = 'secret';
|
|
34
|
+
process.env.CONFLUENCE_USERNAME = 'admin';
|
|
35
|
+
process.env.CONFLUENCE_AUTH_TYPE = 'basic';
|
|
36
|
+
|
|
37
|
+
const config = getConfig();
|
|
38
|
+
expect(config.email).toBe('admin');
|
|
39
|
+
expect(config.token).toBe('secret');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('CONFLUENCE_EMAIL takes priority over CONFLUENCE_USERNAME', () => {
|
|
43
|
+
process.env.CONFLUENCE_DOMAIN = 'cloud.atlassian.net';
|
|
44
|
+
process.env.CONFLUENCE_API_TOKEN = 'api-token';
|
|
45
|
+
process.env.CONFLUENCE_EMAIL = 'user@example.com';
|
|
46
|
+
process.env.CONFLUENCE_USERNAME = 'admin';
|
|
47
|
+
process.env.CONFLUENCE_AUTH_TYPE = 'basic';
|
|
48
|
+
|
|
49
|
+
const config = getConfig();
|
|
50
|
+
expect(config.email).toBe('user@example.com');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('CONFLUENCE_PASSWORD is used when CONFLUENCE_API_TOKEN is not set', () => {
|
|
54
|
+
process.env.CONFLUENCE_DOMAIN = 'on-prem.example.com';
|
|
55
|
+
process.env.CONFLUENCE_PASSWORD = 'my-password';
|
|
56
|
+
process.env.CONFLUENCE_USERNAME = 'admin';
|
|
57
|
+
process.env.CONFLUENCE_AUTH_TYPE = 'basic';
|
|
58
|
+
|
|
59
|
+
const config = getConfig();
|
|
60
|
+
expect(config.token).toBe('my-password');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('CONFLUENCE_API_TOKEN takes priority over CONFLUENCE_PASSWORD', () => {
|
|
64
|
+
process.env.CONFLUENCE_DOMAIN = 'cloud.atlassian.net';
|
|
65
|
+
process.env.CONFLUENCE_API_TOKEN = 'api-token';
|
|
66
|
+
process.env.CONFLUENCE_PASSWORD = 'password';
|
|
67
|
+
|
|
68
|
+
const config = getConfig();
|
|
69
|
+
expect(config.token).toBe('api-token');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('CONFLUENCE_HOST alias still works for domain', () => {
|
|
73
|
+
process.env.CONFLUENCE_HOST = 'host.example.com';
|
|
74
|
+
process.env.CONFLUENCE_API_TOKEN = 'token';
|
|
75
|
+
|
|
76
|
+
const config = getConfig();
|
|
77
|
+
expect(config.domain).toBe('host.example.com');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -97,7 +97,7 @@ describe('ConfluenceClient', () => {
|
|
|
97
97
|
domain: 'test.atlassian.net',
|
|
98
98
|
token: 'missing-email',
|
|
99
99
|
authType: 'basic'
|
|
100
|
-
})).toThrow('Basic authentication requires an email address.');
|
|
100
|
+
})).toThrow('Basic authentication requires an email address or username.');
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
|
|
@@ -303,6 +303,68 @@ describe('ConfluenceClient', () => {
|
|
|
303
303
|
});
|
|
304
304
|
});
|
|
305
305
|
|
|
306
|
+
describe('search', () => {
|
|
307
|
+
test('should wrap query in text search by default', async () => {
|
|
308
|
+
const mock = new MockAdapter(client.client);
|
|
309
|
+
mock.onGet('/search').reply((config) => {
|
|
310
|
+
expect(config.params.cql).toBe('text ~ "architecture decisions"');
|
|
311
|
+
expect(config.params.limit).toBe(10);
|
|
312
|
+
return [200, { results: [] }];
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const results = await client.search('architecture decisions');
|
|
316
|
+
expect(results).toEqual([]);
|
|
317
|
+
|
|
318
|
+
mock.restore();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('should pass raw CQL when rawCql is true', async () => {
|
|
322
|
+
const mock = new MockAdapter(client.client);
|
|
323
|
+
const rawQuery = 'contributor = currentUser() order by lastmodified desc';
|
|
324
|
+
mock.onGet('/search').reply((config) => {
|
|
325
|
+
expect(config.params.cql).toBe(rawQuery);
|
|
326
|
+
return [200, {
|
|
327
|
+
results: [{
|
|
328
|
+
content: { id: '123', title: 'Test Page', type: 'page' },
|
|
329
|
+
excerpt: 'test excerpt'
|
|
330
|
+
}]
|
|
331
|
+
}];
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const results = await client.search(rawQuery, 10, true);
|
|
335
|
+
expect(results).toHaveLength(1);
|
|
336
|
+
expect(results[0].id).toBe('123');
|
|
337
|
+
expect(results[0].title).toBe('Test Page');
|
|
338
|
+
|
|
339
|
+
mock.restore();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('should escape double quotes in text search query', async () => {
|
|
343
|
+
const mock = new MockAdapter(client.client);
|
|
344
|
+
mock.onGet('/search').reply((config) => {
|
|
345
|
+
expect(config.params.cql).toBe('text ~ "test \\"quoted\\" term"');
|
|
346
|
+
return [200, { results: [] }];
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const results = await client.search('test "quoted" term');
|
|
350
|
+
expect(results).toEqual([]);
|
|
351
|
+
|
|
352
|
+
mock.restore();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('should respect limit parameter', async () => {
|
|
356
|
+
const mock = new MockAdapter(client.client);
|
|
357
|
+
mock.onGet('/search').reply((config) => {
|
|
358
|
+
expect(config.params.limit).toBe(5);
|
|
359
|
+
return [200, { results: [] }];
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await client.search('test', 5);
|
|
363
|
+
|
|
364
|
+
mock.restore();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
306
368
|
describe('page creation and updates', () => {
|
|
307
369
|
test('should have required methods for page management', () => {
|
|
308
370
|
expect(typeof client.createPage).toBe('function');
|
|
@@ -709,6 +771,66 @@ describe('ConfluenceClient', () => {
|
|
|
709
771
|
}
|
|
710
772
|
});
|
|
711
773
|
|
|
774
|
+
test('normalizeAttachment should return all fields needed for JSON output', () => {
|
|
775
|
+
const raw = {
|
|
776
|
+
id: '101',
|
|
777
|
+
title: 'diagram.png',
|
|
778
|
+
metadata: { mediaType: 'image/png' },
|
|
779
|
+
extensions: { fileSize: 204800 },
|
|
780
|
+
version: { number: 3 },
|
|
781
|
+
_links: { download: '/download/attachments/123/diagram.png' }
|
|
782
|
+
};
|
|
783
|
+
const result = client.normalizeAttachment(raw);
|
|
784
|
+
expect(result).toEqual({
|
|
785
|
+
id: '101',
|
|
786
|
+
title: 'diagram.png',
|
|
787
|
+
mediaType: 'image/png',
|
|
788
|
+
fileSize: 204800,
|
|
789
|
+
version: 3,
|
|
790
|
+
downloadLink: expect.stringContaining('/download/attachments/123/diagram.png')
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test('normalizeAttachment should handle missing metadata gracefully', () => {
|
|
795
|
+
const raw = {
|
|
796
|
+
id: '102',
|
|
797
|
+
title: 'readme.txt',
|
|
798
|
+
version: { number: 1 },
|
|
799
|
+
_links: {}
|
|
800
|
+
};
|
|
801
|
+
const result = client.normalizeAttachment(raw);
|
|
802
|
+
expect(result.mediaType).toBe('');
|
|
803
|
+
expect(result.fileSize).toBe(0);
|
|
804
|
+
expect(result.version).toBe(1);
|
|
805
|
+
expect(result.downloadLink).toBeNull();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
test('getAllAttachments should return normalized attachment objects', async () => {
|
|
809
|
+
const mock = new MockAdapter(client.client);
|
|
810
|
+
mock.onGet('/content/123/child/attachment').reply(200, {
|
|
811
|
+
results: [{
|
|
812
|
+
id: '201',
|
|
813
|
+
title: 'report.pdf',
|
|
814
|
+
metadata: { mediaType: 'application/pdf' },
|
|
815
|
+
extensions: { fileSize: 512000 },
|
|
816
|
+
version: { number: 2 },
|
|
817
|
+
_links: { download: '/download/attachments/123/report.pdf' }
|
|
818
|
+
}],
|
|
819
|
+
_links: {}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
const attachments = await client.getAllAttachments('123');
|
|
823
|
+
expect(attachments).toHaveLength(1);
|
|
824
|
+
expect(attachments[0]).toHaveProperty('id', '201');
|
|
825
|
+
expect(attachments[0]).toHaveProperty('title', 'report.pdf');
|
|
826
|
+
expect(attachments[0]).toHaveProperty('mediaType', 'application/pdf');
|
|
827
|
+
expect(attachments[0]).toHaveProperty('fileSize', 512000);
|
|
828
|
+
expect(attachments[0]).toHaveProperty('version', 2);
|
|
829
|
+
expect(attachments[0]).toHaveProperty('downloadLink');
|
|
830
|
+
|
|
831
|
+
mock.restore();
|
|
832
|
+
});
|
|
833
|
+
|
|
712
834
|
test('deleteAttachment should call delete endpoint', async () => {
|
|
713
835
|
const mock = new MockAdapter(client.client);
|
|
714
836
|
mock.onDelete('/content/123/child/attachment/999').reply(204);
|