confluence-cli 1.19.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/CHANGELOG.md +7 -0
- package/README.md +8 -5
- package/bin/confluence.js +51 -14
- package/lib/config.js +17 -17
- package/lib/confluence-client.js +1 -1
- package/package.json +1 -1
- package/tests/config.test.js +79 -0
- package/tests/confluence-client.test.js +61 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
|
|
1
8
|
# [1.19.0](https://github.com/pchuri/confluence-cli/compare/v1.18.0...v1.19.0) (2026-02-20)
|
|
2
9
|
|
|
3
10
|
|
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);
|
|
@@ -419,6 +419,7 @@ program
|
|
|
419
419
|
.option('-p, --pattern <glob>', 'Filter attachments by filename (e.g., "*.png")')
|
|
420
420
|
.option('-d, --download', 'Download matching attachments')
|
|
421
421
|
.option('--dest <directory>', 'Directory to save downloads (default: current directory)', '.')
|
|
422
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
422
423
|
.action(async (pageId, options) => {
|
|
423
424
|
const analytics = new Analytics();
|
|
424
425
|
try {
|
|
@@ -431,22 +432,47 @@ program
|
|
|
431
432
|
throw new Error('Limit must be a positive number.');
|
|
432
433
|
}
|
|
433
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
|
+
|
|
434
440
|
const attachments = await client.getAllAttachments(pageId, { maxResults });
|
|
435
441
|
const filtered = pattern ? attachments.filter(att => client.matchesPattern(att.title, pattern)) : attachments;
|
|
436
442
|
|
|
437
443
|
if (filtered.length === 0) {
|
|
438
|
-
|
|
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
|
+
}
|
|
439
449
|
analytics.track('attachments', true);
|
|
440
450
|
return;
|
|
441
451
|
}
|
|
442
452
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
+
}
|
|
450
476
|
|
|
451
477
|
if (options.download) {
|
|
452
478
|
const fs = require('fs');
|
|
@@ -475,17 +501,28 @@ program
|
|
|
475
501
|
writer.on('finish', resolve);
|
|
476
502
|
});
|
|
477
503
|
|
|
478
|
-
|
|
504
|
+
const downloadResults = [];
|
|
479
505
|
for (const attachment of filtered) {
|
|
480
506
|
const targetPath = uniquePathFor(destDir, attachment.title);
|
|
481
|
-
// Pass the full attachment object so downloadAttachment can use downloadLink directly
|
|
482
507
|
const dataStream = await client.downloadAttachment(pageId, attachment);
|
|
483
508
|
await writeStream(dataStream, targetPath);
|
|
484
|
-
|
|
485
|
-
|
|
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
|
+
}
|
|
486
513
|
}
|
|
487
514
|
|
|
488
|
-
|
|
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
|
+
}
|
|
489
526
|
}
|
|
490
527
|
|
|
491
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');
|
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
|
|
|
@@ -771,6 +771,66 @@ describe('ConfluenceClient', () => {
|
|
|
771
771
|
}
|
|
772
772
|
});
|
|
773
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
|
+
|
|
774
834
|
test('deleteAttachment should call delete endpoint', async () => {
|
|
775
835
|
const mock = new MockAdapter(client.client);
|
|
776
836
|
mock.onDelete('/content/123/child/attachment/999').reply(204);
|