@atlassian-dc-mcp/common 0.17.1 → 0.19.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 +22 -0
- package/build/__tests__/args.test.d.ts +2 -0
- package/build/__tests__/args.test.d.ts.map +1 -0
- package/build/__tests__/args.test.js +117 -0
- package/build/__tests__/args.test.js.map +1 -0
- package/build/__tests__/describe-error.test.d.ts +2 -0
- package/build/__tests__/describe-error.test.d.ts.map +1 -0
- package/build/__tests__/describe-error.test.js +54 -0
- package/build/__tests__/describe-error.test.js.map +1 -0
- package/build/__tests__/setup-cli.test.js +337 -44
- package/build/__tests__/setup-cli.test.js.map +1 -1
- package/build/config/source.d.ts +1 -0
- package/build/config/source.d.ts.map +1 -1
- package/build/config/source.js.map +1 -1
- package/build/index.d.ts +3 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -1
- package/build/index.js.map +1 -1
- package/build/setup/args.d.ts +14 -0
- package/build/setup/args.d.ts.map +1 -0
- package/build/setup/args.js +66 -0
- package/build/setup/args.js.map +1 -0
- package/build/setup/describe-error.d.ts +2 -0
- package/build/setup/describe-error.d.ts.map +1 -0
- package/build/setup/describe-error.js +50 -0
- package/build/setup/describe-error.js.map +1 -0
- package/build/setup/value-validator.d.ts +8 -0
- package/build/setup/value-validator.d.ts.map +1 -0
- package/build/setup/value-validator.js +57 -0
- package/build/setup/value-validator.js.map +1 -0
- package/build/setup-cli.d.ts +18 -0
- package/build/setup-cli.d.ts.map +1 -1
- package/build/setup-cli.js +241 -33
- package/build/setup-cli.js.map +1 -1
- package/jest.config.js +1 -0
- package/package.json +2 -2
- package/src/__tests__/args.test.ts +132 -0
- package/src/__tests__/describe-error.test.ts +70 -0
- package/src/__tests__/setup-cli.test.ts +393 -38
- package/src/config/source.ts +1 -0
- package/src/index.ts +3 -1
- package/src/setup/args.ts +78 -0
- package/src/setup/describe-error.ts +67 -0
- package/src/setup/value-validator.ts +59 -0
- package/src/setup-cli.ts +318 -36
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [0.19.0](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.18.0...v0.19.0) (2026-04-27)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **setup:** add CLI flags and non-interactive mode ([7de53f0](https://github.com/b1ff/atlassian-dc-mcp/commit/7de53f0b23704e84324b59bbcb8d830a1596f44a))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# [0.18.0](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.17.1...v0.18.0) (2026-04-24)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* **setup:** validate credentials via product services with retry and friendly errors ([6056320](https://github.com/b1ff/atlassian-dc-mcp/commit/60563200e62b513fe8f3b5b4a190edfa1be25a13))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
## [0.17.1](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.17.0...v0.17.1) (2026-04-23)
|
|
7
29
|
|
|
8
30
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"args.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/args.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { parseSetupArgs, printSetupHelp, SetupArgsError } from '../setup/args.js';
|
|
2
|
+
describe('parseSetupArgs', () => {
|
|
3
|
+
it('returns defaults for an empty argv', () => {
|
|
4
|
+
const args = parseSetupArgs([]);
|
|
5
|
+
expect(args).toEqual({
|
|
6
|
+
host: undefined,
|
|
7
|
+
apiBasePath: undefined,
|
|
8
|
+
token: undefined,
|
|
9
|
+
defaultPageSize: undefined,
|
|
10
|
+
nonInteractive: false,
|
|
11
|
+
help: false,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
it('parses every supported flag with = form and space form', () => {
|
|
15
|
+
const args = parseSetupArgs([
|
|
16
|
+
'--host=jira.example.com',
|
|
17
|
+
'--api-base-path',
|
|
18
|
+
'/rest/api/2',
|
|
19
|
+
'--token=secret',
|
|
20
|
+
'--default-page-size=50',
|
|
21
|
+
'--non-interactive',
|
|
22
|
+
]);
|
|
23
|
+
expect(args).toEqual({
|
|
24
|
+
host: 'jira.example.com',
|
|
25
|
+
apiBasePath: '/rest/api/2',
|
|
26
|
+
token: 'secret',
|
|
27
|
+
defaultPageSize: '50',
|
|
28
|
+
nonInteractive: true,
|
|
29
|
+
help: false,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it('treats whitespace-only and empty values as not provided', () => {
|
|
33
|
+
const args = parseSetupArgs(['--host=', '--token= ']);
|
|
34
|
+
expect(args.host).toBeUndefined();
|
|
35
|
+
expect(args.token).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
it('supports -h as short alias for --help', () => {
|
|
38
|
+
expect(parseSetupArgs(['-h']).help).toBe(true);
|
|
39
|
+
expect(parseSetupArgs(['--help']).help).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it('supports short aliases for every flag', () => {
|
|
42
|
+
const args = parseSetupArgs([
|
|
43
|
+
'-H', 'jira.example.com',
|
|
44
|
+
'-b', '/rest/api/2',
|
|
45
|
+
'-t', 'short-token',
|
|
46
|
+
'-s', '50',
|
|
47
|
+
'-n',
|
|
48
|
+
]);
|
|
49
|
+
expect(args).toEqual({
|
|
50
|
+
host: 'jira.example.com',
|
|
51
|
+
apiBasePath: '/rest/api/2',
|
|
52
|
+
token: 'short-token',
|
|
53
|
+
defaultPageSize: '50',
|
|
54
|
+
nonInteractive: true,
|
|
55
|
+
help: false,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('short alias mapping', () => {
|
|
59
|
+
it('-H sets host', () => {
|
|
60
|
+
expect(parseSetupArgs(['-H', 'jira.example.com']).host).toBe('jira.example.com');
|
|
61
|
+
});
|
|
62
|
+
it('-b sets api-base-path', () => {
|
|
63
|
+
expect(parseSetupArgs(['-b', '/rest/api/2']).apiBasePath).toBe('/rest/api/2');
|
|
64
|
+
});
|
|
65
|
+
it('-t sets token', () => {
|
|
66
|
+
expect(parseSetupArgs(['-t', 'secret']).token).toBe('secret');
|
|
67
|
+
});
|
|
68
|
+
it('-s sets default-page-size', () => {
|
|
69
|
+
expect(parseSetupArgs(['-s', '100']).defaultPageSize).toBe('100');
|
|
70
|
+
});
|
|
71
|
+
it('-n sets non-interactive', () => {
|
|
72
|
+
expect(parseSetupArgs(['-n']).nonInteractive).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
it('short and long forms produce identical output', () => {
|
|
75
|
+
const short = parseSetupArgs([
|
|
76
|
+
'-H', 'jira.example.com',
|
|
77
|
+
'-b', '/rest',
|
|
78
|
+
'-t', 'tok',
|
|
79
|
+
'-s', '25',
|
|
80
|
+
'-n',
|
|
81
|
+
]);
|
|
82
|
+
const long = parseSetupArgs([
|
|
83
|
+
'--host', 'jira.example.com',
|
|
84
|
+
'--api-base-path', '/rest',
|
|
85
|
+
'--token', 'tok',
|
|
86
|
+
'--default-page-size', '25',
|
|
87
|
+
'--non-interactive',
|
|
88
|
+
]);
|
|
89
|
+
expect(short).toEqual(long);
|
|
90
|
+
});
|
|
91
|
+
it('-H is not aliased to help (lowercase -h is help, uppercase -H is host)', () => {
|
|
92
|
+
expect(parseSetupArgs(['-H', 'jira.example.com']).help).toBe(false);
|
|
93
|
+
expect(parseSetupArgs(['-h']).help).toBe(true);
|
|
94
|
+
expect(parseSetupArgs(['-h']).host).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
it('rejects unknown single-letter flags', () => {
|
|
97
|
+
expect(() => parseSetupArgs(['-x'])).toThrow(SetupArgsError);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
it('throws SetupArgsError on an unknown flag', () => {
|
|
101
|
+
expect(() => parseSetupArgs(['--unknown'])).toThrow(SetupArgsError);
|
|
102
|
+
});
|
|
103
|
+
it('throws SetupArgsError on a stray positional', () => {
|
|
104
|
+
expect(() => parseSetupArgs(['extra'])).toThrow(SetupArgsError);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('printSetupHelp', () => {
|
|
108
|
+
it('emits product-aware lines including the home file path', () => {
|
|
109
|
+
const out = [];
|
|
110
|
+
printSetupHelp('jira', (m) => out.push(m));
|
|
111
|
+
const joined = out.join('\n');
|
|
112
|
+
expect(joined).toContain('@atlassian-dc-mcp/jira setup');
|
|
113
|
+
expect(joined).toContain('--non-interactive');
|
|
114
|
+
expect(joined).toContain('~/.atlassian-dc-mcp/jira.env');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=args.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"args.test.js","sourceRoot":"","sources":["../../src/__tests__/args.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAElF,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,IAAI,GAAG,cAAc,CAAC,EAAE,CAAC,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;YACnB,IAAI,EAAE,SAAS;YACf,WAAW,EAAE,SAAS;YACtB,KAAK,EAAE,SAAS;YAChB,eAAe,EAAE,SAAS;YAC1B,cAAc,EAAE,KAAK;YACrB,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,yBAAyB;YACzB,iBAAiB;YACjB,aAAa;YACb,gBAAgB;YAChB,wBAAwB;YACxB,mBAAmB;SACpB,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;YACnB,IAAI,EAAE,kBAAkB;YACxB,WAAW,EAAE,aAAa;YAC1B,KAAK,EAAE,QAAQ;YACf,eAAe,EAAE,IAAI;YACrB,cAAc,EAAE,IAAI;YACpB,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,IAAI,GAAG,cAAc,CAAC,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC;QACxD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;QAClC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,aAAa,EAAE,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,MAAM,CAAC,cAAc,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,IAAI,EAAE,kBAAkB;YACxB,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,IAAI;YACV,IAAI;SACL,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;YACnB,IAAI,EAAE,kBAAkB;YACxB,WAAW,EAAE,aAAa;YAC1B,KAAK,EAAE,aAAa;YACpB,eAAe,EAAE,IAAI;YACrB,cAAc,EAAE,IAAI;YACpB,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;QACnC,EAAE,CAAC,cAAc,EAAE,GAAG,EAAE;YACtB,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QACnF,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,uBAAuB,EAAE,GAAG,EAAE;YAC/B,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAChF,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,eAAe,EAAE,GAAG,EAAE;YACvB,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;YACnC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,yBAAyB,EAAE,GAAG,EAAE;YACjC,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;YACvD,MAAM,KAAK,GAAG,cAAc,CAAC;gBAC3B,IAAI,EAAE,kBAAkB;gBACxB,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,KAAK;gBACX,IAAI,EAAE,IAAI;gBACV,IAAI;aACL,CAAC,CAAC;YACH,MAAM,IAAI,GAAG,cAAc,CAAC;gBAC1B,QAAQ,EAAE,kBAAkB;gBAC5B,iBAAiB,EAAE,OAAO;gBAC1B,SAAS,EAAE,KAAK;gBAChB,qBAAqB,EAAE,IAAI;gBAC3B,mBAAmB;aACpB,CAAC,CAAC;YACH,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wEAAwE,EAAE,GAAG,EAAE;YAChF,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACpE,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/C,MAAM,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;QACtD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;YAC7C,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QAC/D,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,cAAc,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;QAC9C,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"describe-error.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/describe-error.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describeValidationError } from '../setup/describe-error.js';
|
|
2
|
+
describe('describeValidationError', () => {
|
|
3
|
+
it('formats HTTP errors with status, URL, and a 401 hint', () => {
|
|
4
|
+
const apiError = {
|
|
5
|
+
name: 'ApiError',
|
|
6
|
+
status: 401,
|
|
7
|
+
statusText: 'Unauthorized',
|
|
8
|
+
url: 'https://jira.example.com/rest/api/2/myself',
|
|
9
|
+
body: {},
|
|
10
|
+
};
|
|
11
|
+
expect(describeValidationError(apiError)).toBe('https://jira.example.com/rest/api/2/myself returned 401 Unauthorized. Check the host, API base path, and API token.');
|
|
12
|
+
});
|
|
13
|
+
it('formats non-auth HTTP errors without the auth hint', () => {
|
|
14
|
+
const apiError = {
|
|
15
|
+
name: 'ApiError',
|
|
16
|
+
status: 500,
|
|
17
|
+
statusText: 'Internal Server Error',
|
|
18
|
+
url: 'https://jira.example.com/rest/api/2/myself',
|
|
19
|
+
};
|
|
20
|
+
expect(describeValidationError(apiError)).toBe('https://jira.example.com/rest/api/2/myself returned 500 Internal Server Error.');
|
|
21
|
+
});
|
|
22
|
+
it('unwraps Node fetch TypeErrors with ECONNREFUSED via cause', () => {
|
|
23
|
+
const error = Object.assign(new TypeError('fetch failed'), {
|
|
24
|
+
cause: Object.assign(new Error('connect ECONNREFUSED 127.0.0.1:443'), {
|
|
25
|
+
code: 'ECONNREFUSED',
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
expect(describeValidationError(error)).toBe('connection refused (ECONNREFUSED: connect ECONNREFUSED 127.0.0.1:443)');
|
|
29
|
+
});
|
|
30
|
+
it('unwraps DNS failures as ENOTFOUND', () => {
|
|
31
|
+
const error = Object.assign(new TypeError('fetch failed'), {
|
|
32
|
+
cause: Object.assign(new Error('getaddrinfo ENOTFOUND jira.nope'), {
|
|
33
|
+
code: 'ENOTFOUND',
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
expect(describeValidationError(error)).toBe('could not resolve host (ENOTFOUND: getaddrinfo ENOTFOUND jira.nope)');
|
|
37
|
+
});
|
|
38
|
+
it('recognises self-signed certificate errors', () => {
|
|
39
|
+
const error = Object.assign(new TypeError('fetch failed'), {
|
|
40
|
+
cause: Object.assign(new Error('self-signed certificate'), {
|
|
41
|
+
code: 'DEPTH_ZERO_SELF_SIGNED_CERT',
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
expect(describeValidationError(error)).toBe('server uses a self-signed TLS certificate (DEPTH_ZERO_SELF_SIGNED_CERT: self-signed certificate)');
|
|
45
|
+
});
|
|
46
|
+
it('describes aborted requests', () => {
|
|
47
|
+
const error = Object.assign(new Error('aborted'), { name: 'AbortError' });
|
|
48
|
+
expect(describeValidationError(error)).toBe('request was aborted (possibly timed out)');
|
|
49
|
+
});
|
|
50
|
+
it('falls back to the raw message when nothing else matches', () => {
|
|
51
|
+
expect(describeValidationError(new Error('something weird'))).toBe('something weird');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
//# sourceMappingURL=describe-error.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"describe-error.test.js","sourceRoot":"","sources":["../../src/__tests__/describe-error.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAErE,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,QAAQ,GAAG;YACf,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,GAAG;YACX,UAAU,EAAE,cAAc;YAC1B,GAAG,EAAE,4CAA4C;YACjD,IAAI,EAAE,EAAE;SACT,CAAC;QACF,MAAM,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAC5C,qHAAqH,CACtH,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,QAAQ,GAAG;YACf,IAAI,EAAE,UAAU;YAChB,MAAM,EAAE,GAAG;YACX,UAAU,EAAE,uBAAuB;YACnC,GAAG,EAAE,4CAA4C;SAClD,CAAC;QACF,MAAM,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAC5C,gFAAgF,CACjF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,cAAc,CAAC,EAAE;YACzD,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,oCAAoC,CAAC,EAAE;gBACpE,IAAI,EAAE,cAAc;aACrB,CAAC;SACH,CAAC,CAAC;QACH,MAAM,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CACzC,uEAAuE,CACxE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,cAAc,CAAC,EAAE;YACzD,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,iCAAiC,CAAC,EAAE;gBACjE,IAAI,EAAE,WAAW;aAClB,CAAC;SACH,CAAC,CAAC;QACH,MAAM,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CACzC,qEAAqE,CACtE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,cAAc,CAAC,EAAE;YACzD,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,yBAAyB,CAAC,EAAE;gBACzD,IAAI,EAAE,6BAA6B;aACpC,CAAC;SACH,CAAC,CAAC;QACH,MAAM,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CACzC,kGAAkG,CACnG,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;QAC1E,MAAM,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,uBAAuB,CAAC,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IACxF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -2,7 +2,18 @@ import { DefaultConfigRegistry } from '../config/registry.js';
|
|
|
2
2
|
import { HomeFileSource } from '../config/sources/home-file.js';
|
|
3
3
|
import { MacosKeychainSource } from '../config/sources/macos-keychain.js';
|
|
4
4
|
import { ProcessEnvSource } from '../config/sources/process-env.js';
|
|
5
|
-
import { runSetup } from '../setup-cli.js';
|
|
5
|
+
import { runSetup, } from '../setup-cli.js';
|
|
6
|
+
function makeArgs(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
host: undefined,
|
|
9
|
+
apiBasePath: undefined,
|
|
10
|
+
token: undefined,
|
|
11
|
+
defaultPageSize: undefined,
|
|
12
|
+
nonInteractive: false,
|
|
13
|
+
help: false,
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
6
17
|
const JIRA = {
|
|
7
18
|
id: 'jira',
|
|
8
19
|
envVars: {
|
|
@@ -67,14 +78,58 @@ class FakeHomeFile extends HomeFileSource {
|
|
|
67
78
|
return `home-file-fake(${product.id})`;
|
|
68
79
|
}
|
|
69
80
|
}
|
|
81
|
+
class StubCredentialValidator {
|
|
82
|
+
calls = [];
|
|
83
|
+
queue = [];
|
|
84
|
+
fallback = { ok: true };
|
|
85
|
+
returns(result) {
|
|
86
|
+
this.fallback = result;
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
reject(message) {
|
|
90
|
+
return this.returns({ ok: false, message });
|
|
91
|
+
}
|
|
92
|
+
enqueue(...results) {
|
|
93
|
+
this.queue.push(...results);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
asFn() {
|
|
97
|
+
return async (ctx) => {
|
|
98
|
+
this.calls.push(ctx);
|
|
99
|
+
return this.queue.shift() ?? this.fallback;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function scriptedConfirms(script) {
|
|
104
|
+
return (message, fallbackDefault) => {
|
|
105
|
+
for (const [prefix, answer] of Object.entries(script)) {
|
|
106
|
+
if (message.startsWith(prefix))
|
|
107
|
+
return answer;
|
|
108
|
+
}
|
|
109
|
+
return fallbackDefault ?? false;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
70
112
|
function makeRegistry(keychain, home) {
|
|
71
113
|
return new DefaultConfigRegistry([new ProcessEnvSource(), home, keychain]);
|
|
72
114
|
}
|
|
73
|
-
|
|
115
|
+
const standardAnswers = {
|
|
116
|
+
host: 'j-host',
|
|
117
|
+
apiBasePath: '/rest/api/2',
|
|
118
|
+
pageSize: '25',
|
|
119
|
+
token: 'secret',
|
|
120
|
+
};
|
|
121
|
+
function makePrompts(overrides = {}, answers = {}, confirms) {
|
|
122
|
+
const filled = { ...standardAnswers, ...answers };
|
|
74
123
|
return {
|
|
75
|
-
input: async (opts) =>
|
|
76
|
-
|
|
77
|
-
|
|
124
|
+
input: async (opts) => {
|
|
125
|
+
if (opts.message.startsWith('Host'))
|
|
126
|
+
return filled.host;
|
|
127
|
+
if (opts.message.startsWith('API base path'))
|
|
128
|
+
return filled.apiBasePath;
|
|
129
|
+
return filled.pageSize;
|
|
130
|
+
},
|
|
131
|
+
password: async () => filled.token,
|
|
132
|
+
confirm: async (opts) => (confirms ?? ((_m, d) => d ?? false))(opts.message, opts.default),
|
|
78
133
|
...overrides,
|
|
79
134
|
};
|
|
80
135
|
}
|
|
@@ -95,18 +150,13 @@ describe('runSetup', () => {
|
|
|
95
150
|
process.env = originalEnv;
|
|
96
151
|
});
|
|
97
152
|
it('writes the token to keychain first on darwin and clears home file token', async () => {
|
|
98
|
-
const prompts = makePrompts({
|
|
99
|
-
input: async (opts) => {
|
|
100
|
-
if (opts.message.startsWith('Host'))
|
|
101
|
-
return 'j-host';
|
|
102
|
-
if (opts.message.startsWith('API base path'))
|
|
103
|
-
return '/rest/api/2';
|
|
104
|
-
return '25';
|
|
105
|
-
},
|
|
106
|
-
password: async () => 'secret',
|
|
107
|
-
});
|
|
108
153
|
const registry = makeRegistry(keychain, home);
|
|
109
|
-
await runSetup(JIRA, {
|
|
154
|
+
await runSetup(JIRA, {
|
|
155
|
+
registry,
|
|
156
|
+
log: (m) => logs.push(m),
|
|
157
|
+
exit: () => undefined,
|
|
158
|
+
prompts: makePrompts(),
|
|
159
|
+
});
|
|
110
160
|
expect(keychain.writeCalls).toBe(1);
|
|
111
161
|
expect(keychain.store).toBe('secret');
|
|
112
162
|
expect(home.clears).toContainEqual(['jira', 'token']);
|
|
@@ -114,53 +164,296 @@ describe('runSetup', () => {
|
|
|
114
164
|
});
|
|
115
165
|
it('falls back to home file when keychain is unavailable', async () => {
|
|
116
166
|
keychain.available = false;
|
|
117
|
-
const prompts = makePrompts({
|
|
118
|
-
input: async (opts) => {
|
|
119
|
-
if (opts.message.startsWith('Host'))
|
|
120
|
-
return 'j-host';
|
|
121
|
-
if (opts.message.startsWith('API base path'))
|
|
122
|
-
return '/rest/api/2';
|
|
123
|
-
return '25';
|
|
124
|
-
},
|
|
125
|
-
password: async () => 'secret',
|
|
126
|
-
});
|
|
127
167
|
const registry = makeRegistry(keychain, home);
|
|
128
|
-
await runSetup(JIRA, {
|
|
168
|
+
await runSetup(JIRA, {
|
|
169
|
+
registry,
|
|
170
|
+
log: (m) => logs.push(m),
|
|
171
|
+
exit: () => undefined,
|
|
172
|
+
prompts: makePrompts(),
|
|
173
|
+
});
|
|
129
174
|
expect(keychain.writeCalls).toBe(0);
|
|
130
175
|
const tokenWrites = home.writes.filter(([, k]) => k === 'token');
|
|
131
176
|
expect(tokenWrites).toHaveLength(1);
|
|
132
177
|
});
|
|
133
178
|
it('prints shadowing warning when env var is set higher than target', async () => {
|
|
134
179
|
process.env.JIRA_API_TOKEN = 'shadow';
|
|
135
|
-
const prompts = makePrompts({
|
|
136
|
-
input: async (opts) => {
|
|
137
|
-
if (opts.message.startsWith('Host'))
|
|
138
|
-
return 'j-host';
|
|
139
|
-
if (opts.message.startsWith('API base path'))
|
|
140
|
-
return '/rest/api/2';
|
|
141
|
-
return '25';
|
|
142
|
-
},
|
|
143
|
-
password: async () => 'secret',
|
|
144
|
-
});
|
|
145
180
|
const registry = makeRegistry(keychain, home);
|
|
146
|
-
await runSetup(JIRA, {
|
|
181
|
+
await runSetup(JIRA, {
|
|
182
|
+
registry,
|
|
183
|
+
log: (m) => logs.push(m),
|
|
184
|
+
exit: () => undefined,
|
|
185
|
+
prompts: makePrompts(),
|
|
186
|
+
});
|
|
147
187
|
expect(logs.some((l) => l.includes('JIRA_API_TOKEN') && l.includes('Warning'))).toBe(true);
|
|
148
188
|
});
|
|
189
|
+
it('calls validateCredentials with the entered values before saving', async () => {
|
|
190
|
+
const validator = new StubCredentialValidator();
|
|
191
|
+
const registry = makeRegistry(keychain, home);
|
|
192
|
+
await runSetup(JIRA, {
|
|
193
|
+
registry,
|
|
194
|
+
log: (m) => logs.push(m),
|
|
195
|
+
exit: () => undefined,
|
|
196
|
+
prompts: makePrompts(),
|
|
197
|
+
validateCredentials: validator.asFn(),
|
|
198
|
+
});
|
|
199
|
+
expect(validator.calls).toEqual([
|
|
200
|
+
{ host: 'j-host', apiBasePath: '/rest/api/2', token: 'secret' },
|
|
201
|
+
]);
|
|
202
|
+
expect(keychain.writeCalls).toBe(1);
|
|
203
|
+
});
|
|
204
|
+
it('exits without writing when validation fails and user declines retry or save-anyway', async () => {
|
|
205
|
+
const validator = new StubCredentialValidator().reject('401 Unauthorized');
|
|
206
|
+
const exitFn = jest.fn();
|
|
207
|
+
const registry = makeRegistry(keychain, home);
|
|
208
|
+
await runSetup(JIRA, {
|
|
209
|
+
registry,
|
|
210
|
+
log: (m) => logs.push(m),
|
|
211
|
+
exit: exitFn,
|
|
212
|
+
prompts: makePrompts({}, { token: 'bad-token' }, scriptedConfirms({
|
|
213
|
+
'Try again': false,
|
|
214
|
+
'Save configuration anyway': false,
|
|
215
|
+
})),
|
|
216
|
+
validateCredentials: validator.asFn(),
|
|
217
|
+
});
|
|
218
|
+
expect(validator.calls).toHaveLength(1);
|
|
219
|
+
expect(exitFn).toHaveBeenCalledWith(1);
|
|
220
|
+
expect(keychain.writeCalls).toBe(0);
|
|
221
|
+
expect(home.writes).toHaveLength(0);
|
|
222
|
+
expect(logs.some((l) => l.includes('401 Unauthorized'))).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
it('retries validation after a failure and writes once it succeeds', async () => {
|
|
225
|
+
const validator = new StubCredentialValidator().enqueue({ ok: false, message: '401 Unauthorized' }, { ok: true });
|
|
226
|
+
const registry = makeRegistry(keychain, home);
|
|
227
|
+
await runSetup(JIRA, {
|
|
228
|
+
registry,
|
|
229
|
+
log: (m) => logs.push(m),
|
|
230
|
+
exit: () => undefined,
|
|
231
|
+
prompts: makePrompts({}, {}, scriptedConfirms({ 'Try again': true })),
|
|
232
|
+
validateCredentials: validator.asFn(),
|
|
233
|
+
});
|
|
234
|
+
expect(validator.calls).toHaveLength(2);
|
|
235
|
+
expect(keychain.writeCalls).toBe(1);
|
|
236
|
+
expect(logs.some((l) => l.includes('401 Unauthorized'))).toBe(true);
|
|
237
|
+
expect(logs.some((l) => l.startsWith('Validation succeeded'))).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
it('stops offering retry after the third failure and lets the user save anyway', async () => {
|
|
240
|
+
const validator = new StubCredentialValidator().reject('401 Unauthorized');
|
|
241
|
+
let retryPromptsShown = 0;
|
|
242
|
+
const confirms = (message) => {
|
|
243
|
+
if (message.startsWith('Try again')) {
|
|
244
|
+
retryPromptsShown++;
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (message.startsWith('Save configuration anyway'))
|
|
248
|
+
return true;
|
|
249
|
+
return false;
|
|
250
|
+
};
|
|
251
|
+
const registry = makeRegistry(keychain, home);
|
|
252
|
+
await runSetup(JIRA, {
|
|
253
|
+
registry,
|
|
254
|
+
log: (m) => logs.push(m),
|
|
255
|
+
exit: () => undefined,
|
|
256
|
+
prompts: makePrompts({}, {}, confirms),
|
|
257
|
+
validateCredentials: validator.asFn(),
|
|
258
|
+
});
|
|
259
|
+
expect(validator.calls).toHaveLength(3);
|
|
260
|
+
expect(retryPromptsShown).toBe(2);
|
|
261
|
+
expect(keychain.writeCalls).toBe(1);
|
|
262
|
+
});
|
|
149
263
|
it('exits 130 on SIGINT cancellation before any write', async () => {
|
|
150
264
|
const exitErr = Object.assign(new Error('closed'), {
|
|
151
265
|
name: 'ExitPromptError',
|
|
152
266
|
});
|
|
153
|
-
const prompts = makePrompts({
|
|
154
|
-
input: async () => {
|
|
155
|
-
throw exitErr;
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
267
|
const exitFn = jest.fn();
|
|
159
268
|
const registry = makeRegistry(keychain, home);
|
|
160
|
-
await runSetup(JIRA, {
|
|
269
|
+
await runSetup(JIRA, {
|
|
270
|
+
registry,
|
|
271
|
+
log: (m) => logs.push(m),
|
|
272
|
+
exit: exitFn,
|
|
273
|
+
prompts: makePrompts({
|
|
274
|
+
input: async () => {
|
|
275
|
+
throw exitErr;
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
161
279
|
expect(exitFn).toHaveBeenCalledWith(130);
|
|
162
280
|
expect(keychain.writeCalls).toBe(0);
|
|
163
281
|
expect(home.writes).toHaveLength(0);
|
|
164
282
|
});
|
|
283
|
+
it('skips the host prompt when --host is passed and still prompts for the rest', async () => {
|
|
284
|
+
const inputCalls = [];
|
|
285
|
+
const passwordCalls = jest.fn(async () => 'secret');
|
|
286
|
+
const registry = makeRegistry(keychain, home);
|
|
287
|
+
await runSetup(JIRA, {
|
|
288
|
+
registry,
|
|
289
|
+
log: (m) => logs.push(m),
|
|
290
|
+
exit: () => undefined,
|
|
291
|
+
args: makeArgs({ host: 'cli-host.example.com' }),
|
|
292
|
+
prompts: {
|
|
293
|
+
input: async (opts) => {
|
|
294
|
+
inputCalls.push(opts.message);
|
|
295
|
+
if (opts.message.startsWith('API base path'))
|
|
296
|
+
return '/rest/api/2';
|
|
297
|
+
return '25';
|
|
298
|
+
},
|
|
299
|
+
password: passwordCalls,
|
|
300
|
+
confirm: async (opts) => opts.default ?? false,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
expect(inputCalls.some((m) => m.startsWith('Host'))).toBe(false);
|
|
304
|
+
expect(inputCalls.some((m) => m.startsWith('API base path'))).toBe(true);
|
|
305
|
+
expect(passwordCalls).toHaveBeenCalledTimes(1);
|
|
306
|
+
expect(home.values.jira.host).toBe('cli-host.example.com');
|
|
307
|
+
});
|
|
308
|
+
describe('non-interactive mode', () => {
|
|
309
|
+
function nonInteractivePrompts() {
|
|
310
|
+
const inputCalls = [];
|
|
311
|
+
let passwordCalls = 0;
|
|
312
|
+
return Object.assign({
|
|
313
|
+
input: async (opts) => {
|
|
314
|
+
inputCalls.push(opts.message);
|
|
315
|
+
return '';
|
|
316
|
+
},
|
|
317
|
+
password: async () => {
|
|
318
|
+
passwordCalls++;
|
|
319
|
+
return '';
|
|
320
|
+
},
|
|
321
|
+
confirm: async () => false,
|
|
322
|
+
}, {
|
|
323
|
+
inputCalls,
|
|
324
|
+
get passwordCalls() {
|
|
325
|
+
return passwordCalls;
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
it('writes everything without prompts when all required fields come from CLI args', async () => {
|
|
330
|
+
const validator = new StubCredentialValidator();
|
|
331
|
+
const exitFn = jest.fn();
|
|
332
|
+
const registry = makeRegistry(keychain, home);
|
|
333
|
+
const prompts = nonInteractivePrompts();
|
|
334
|
+
await runSetup(JIRA, {
|
|
335
|
+
registry,
|
|
336
|
+
log: (m) => logs.push(m),
|
|
337
|
+
exit: exitFn,
|
|
338
|
+
prompts,
|
|
339
|
+
validateCredentials: validator.asFn(),
|
|
340
|
+
args: makeArgs({
|
|
341
|
+
host: 'cli-host.example.com',
|
|
342
|
+
token: 'cli-token',
|
|
343
|
+
nonInteractive: true,
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
expect(prompts.inputCalls).toHaveLength(0);
|
|
347
|
+
expect(prompts.passwordCalls).toBe(0);
|
|
348
|
+
expect(exitFn).not.toHaveBeenCalled();
|
|
349
|
+
expect(validator.calls).toEqual([
|
|
350
|
+
{ host: 'cli-host.example.com', apiBasePath: '/rest/api/2', token: 'cli-token' },
|
|
351
|
+
]);
|
|
352
|
+
expect(home.values.jira.host).toBe('cli-host.example.com');
|
|
353
|
+
expect(home.values.jira.apiBasePath).toBe('/rest/api/2');
|
|
354
|
+
expect(keychain.store).toBe('cli-token');
|
|
355
|
+
});
|
|
356
|
+
it('exits 1 when --token is missing and there is no existing token', async () => {
|
|
357
|
+
const validator = new StubCredentialValidator();
|
|
358
|
+
const exitFn = jest.fn();
|
|
359
|
+
const registry = makeRegistry(keychain, home);
|
|
360
|
+
await runSetup(JIRA, {
|
|
361
|
+
registry,
|
|
362
|
+
log: (m) => logs.push(m),
|
|
363
|
+
exit: exitFn,
|
|
364
|
+
prompts: nonInteractivePrompts(),
|
|
365
|
+
validateCredentials: validator.asFn(),
|
|
366
|
+
args: makeArgs({ host: 'cli-host.example.com', nonInteractive: true }),
|
|
367
|
+
});
|
|
368
|
+
expect(exitFn).toHaveBeenCalledWith(1);
|
|
369
|
+
expect(validator.calls).toHaveLength(0);
|
|
370
|
+
expect(keychain.writeCalls).toBe(0);
|
|
371
|
+
expect(home.writes).toHaveLength(0);
|
|
372
|
+
expect(logs.some((l) => l.includes('JIRA_API_TOKEN'))).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
it('reuses an existing keychain token without rewriting it when --token is omitted', async () => {
|
|
375
|
+
keychain.store = 'kept-token';
|
|
376
|
+
const validator = new StubCredentialValidator();
|
|
377
|
+
const exitFn = jest.fn();
|
|
378
|
+
const registry = makeRegistry(keychain, home);
|
|
379
|
+
await runSetup(JIRA, {
|
|
380
|
+
registry,
|
|
381
|
+
log: (m) => logs.push(m),
|
|
382
|
+
exit: exitFn,
|
|
383
|
+
prompts: nonInteractivePrompts(),
|
|
384
|
+
validateCredentials: validator.asFn(),
|
|
385
|
+
args: makeArgs({ host: 'cli-host.example.com', nonInteractive: true }),
|
|
386
|
+
});
|
|
387
|
+
expect(exitFn).not.toHaveBeenCalled();
|
|
388
|
+
expect(validator.calls).toEqual([
|
|
389
|
+
{ host: 'cli-host.example.com', apiBasePath: '/rest/api/2', token: 'kept-token' },
|
|
390
|
+
]);
|
|
391
|
+
expect(keychain.writeCalls).toBe(0);
|
|
392
|
+
expect(keychain.store).toBe('kept-token');
|
|
393
|
+
const tokenWrites = home.writes.filter(([, k]) => k === 'token');
|
|
394
|
+
expect(tokenWrites).toHaveLength(0);
|
|
395
|
+
});
|
|
396
|
+
it('exits 1 with a format error when --default-page-size is not a positive integer', async () => {
|
|
397
|
+
const exitFn = jest.fn();
|
|
398
|
+
const registry = makeRegistry(keychain, home);
|
|
399
|
+
await runSetup(JIRA, {
|
|
400
|
+
registry,
|
|
401
|
+
log: (m) => logs.push(m),
|
|
402
|
+
exit: exitFn,
|
|
403
|
+
prompts: nonInteractivePrompts(),
|
|
404
|
+
args: makeArgs({
|
|
405
|
+
host: 'cli-host.example.com',
|
|
406
|
+
token: 'cli-token',
|
|
407
|
+
defaultPageSize: 'abc',
|
|
408
|
+
nonInteractive: true,
|
|
409
|
+
}),
|
|
410
|
+
});
|
|
411
|
+
expect(exitFn).toHaveBeenCalledWith(1);
|
|
412
|
+
expect(home.writes).toHaveLength(0);
|
|
413
|
+
expect(keychain.writeCalls).toBe(0);
|
|
414
|
+
expect(logs.some((l) => l.includes('default page size'))).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
it('exits 1 on credential rejection without retrying or asking to save anyway', async () => {
|
|
417
|
+
const validator = new StubCredentialValidator().reject('401 Unauthorized');
|
|
418
|
+
const exitFn = jest.fn();
|
|
419
|
+
const confirmFn = jest.fn(async () => false);
|
|
420
|
+
const registry = makeRegistry(keychain, home);
|
|
421
|
+
await runSetup(JIRA, {
|
|
422
|
+
registry,
|
|
423
|
+
log: (m) => logs.push(m),
|
|
424
|
+
exit: exitFn,
|
|
425
|
+
prompts: { ...nonInteractivePrompts(), confirm: confirmFn },
|
|
426
|
+
validateCredentials: validator.asFn(),
|
|
427
|
+
args: makeArgs({
|
|
428
|
+
host: 'cli-host.example.com',
|
|
429
|
+
token: 'cli-token',
|
|
430
|
+
nonInteractive: true,
|
|
431
|
+
}),
|
|
432
|
+
});
|
|
433
|
+
expect(validator.calls).toHaveLength(1);
|
|
434
|
+
expect(exitFn).toHaveBeenCalledWith(1);
|
|
435
|
+
expect(confirmFn).not.toHaveBeenCalled();
|
|
436
|
+
expect(keychain.writeCalls).toBe(0);
|
|
437
|
+
expect(home.writes).toHaveLength(0);
|
|
438
|
+
});
|
|
439
|
+
it('falls back to product.defaultApiBasePath and FALLBACK_PAGE_SIZE for unspecified optional fields', async () => {
|
|
440
|
+
const validator = new StubCredentialValidator();
|
|
441
|
+
const registry = makeRegistry(keychain, home);
|
|
442
|
+
await runSetup(JIRA, {
|
|
443
|
+
registry,
|
|
444
|
+
log: (m) => logs.push(m),
|
|
445
|
+
exit: () => undefined,
|
|
446
|
+
prompts: nonInteractivePrompts(),
|
|
447
|
+
validateCredentials: validator.asFn(),
|
|
448
|
+
args: makeArgs({
|
|
449
|
+
host: 'cli-host.example.com',
|
|
450
|
+
token: 'cli-token',
|
|
451
|
+
nonInteractive: true,
|
|
452
|
+
}),
|
|
453
|
+
});
|
|
454
|
+
expect(home.values.jira.apiBasePath).toBe('/rest/api/2');
|
|
455
|
+
expect(home.values.jira.defaultPageSize).toBe('25');
|
|
456
|
+
});
|
|
457
|
+
});
|
|
165
458
|
});
|
|
166
459
|
//# sourceMappingURL=setup-cli.test.js.map
|