@redocly/cli 1.22.1 → 1.23.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/__tests__/commands/bundle.test.js +110 -1
  3. package/lib/__tests__/fetch-with-timeout.test.js +29 -5
  4. package/lib/__tests__/utils.test.js +54 -32
  5. package/lib/cms/api/__tests__/api.client.test.js +17 -9
  6. package/lib/cms/api/api-client.d.ts +26 -7
  7. package/lib/cms/api/api-client.js +103 -72
  8. package/lib/cms/commands/__tests__/push-status.test.js +1 -1
  9. package/lib/cms/commands/__tests__/push.test.js +41 -1
  10. package/lib/cms/commands/__tests__/utils.test.js +1 -1
  11. package/lib/cms/commands/push-status.d.ts +1 -1
  12. package/lib/cms/commands/push-status.js +3 -7
  13. package/lib/cms/commands/push.js +4 -4
  14. package/lib/cms/commands/utils.d.ts +3 -0
  15. package/lib/cms/commands/utils.js +8 -1
  16. package/lib/commands/bundle.d.ts +1 -1
  17. package/lib/commands/bundle.js +9 -9
  18. package/lib/commands/eject.d.ts +1 -1
  19. package/lib/commands/eject.js +1 -1
  20. package/lib/commands/preview-project/index.js +1 -1
  21. package/lib/index.js +1 -2
  22. package/lib/types.d.ts +1 -0
  23. package/lib/utils/__mocks__/miscellaneous.d.ts +1 -0
  24. package/lib/utils/__mocks__/miscellaneous.js +2 -1
  25. package/lib/utils/fetch-with-timeout.d.ts +6 -1
  26. package/lib/utils/fetch-with-timeout.js +16 -14
  27. package/lib/utils/miscellaneous.d.ts +4 -1
  28. package/lib/utils/miscellaneous.js +24 -29
  29. package/lib/utils/update-version-notifier.js +8 -4
  30. package/package.json +2 -2
  31. package/src/__tests__/commands/bundle.test.ts +131 -4
  32. package/src/__tests__/fetch-with-timeout.test.ts +36 -6
  33. package/src/__tests__/utils.test.ts +58 -33
  34. package/src/cms/api/__tests__/api.client.test.ts +20 -11
  35. package/src/cms/api/api-client.ts +158 -91
  36. package/src/cms/commands/__tests__/push-status.test.ts +1 -1
  37. package/src/cms/commands/__tests__/push.test.ts +49 -2
  38. package/src/cms/commands/__tests__/utils.test.ts +1 -1
  39. package/src/cms/commands/push-status.ts +5 -9
  40. package/src/cms/commands/push.ts +5 -6
  41. package/src/cms/commands/utils.ts +15 -1
  42. package/src/commands/bundle.ts +14 -12
  43. package/src/commands/eject.ts +2 -2
  44. package/src/commands/preview-project/index.ts +1 -1
  45. package/src/index.ts +1 -2
  46. package/src/types.ts +1 -0
  47. package/src/utils/__mocks__/miscellaneous.ts +1 -0
  48. package/src/utils/fetch-with-timeout.ts +23 -14
  49. package/src/utils/miscellaneous.ts +32 -37
  50. package/src/utils/update-version-notifier.ts +11 -5
  51. package/tsconfig.tsbuildinfo +1 -1
@@ -50,12 +50,12 @@ const push_1 = require("../commands/push");
50
50
  const fetch_with_timeout_1 = require("./fetch-with-timeout");
51
51
  async function getFallbackApisOrExit(argsApis, config) {
52
52
  const { apis } = config;
53
- const shouldFallbackToAllDefinitions = !isNotEmptyArray(argsApis) && apis && Object.keys(apis).length > 0;
53
+ const shouldFallbackToAllDefinitions = !(0, utils_1.isNotEmptyArray)(argsApis) && (0, utils_1.isNotEmptyObject)(apis);
54
54
  const res = shouldFallbackToAllDefinitions
55
55
  ? fallbackToAllDefinitions(apis, config)
56
56
  : await expandGlobsInEntrypoints(argsApis, config);
57
57
  const filteredInvalidEntrypoints = res.filter(({ path }) => !isApiPathValid(path));
58
- if (isNotEmptyArray(filteredInvalidEntrypoints)) {
58
+ if ((0, utils_1.isNotEmptyArray)(filteredInvalidEntrypoints)) {
59
59
  for (const { path } of filteredInvalidEntrypoints) {
60
60
  process.stderr.write((0, colorette_1.yellow)(`\n${(0, path_1.relative)(process.cwd(), path)} ${(0, colorette_1.red)(`does not exist or is invalid.\n\n`)}`));
61
61
  }
@@ -66,9 +66,6 @@ async function getFallbackApisOrExit(argsApis, config) {
66
66
  function getConfigDirectory(config) {
67
67
  return config.configFile ? (0, path_1.dirname)(config.configFile) : process.cwd();
68
68
  }
69
- function isNotEmptyArray(args) {
70
- return Array.isArray(args) && !!args.length;
71
- }
72
69
  function isApiPathValid(apiPath) {
73
70
  if (!apiPath.trim()) {
74
71
  exitWithError('Path cannot be empty.');
@@ -77,14 +74,20 @@ function isApiPathValid(apiPath) {
77
74
  return fs.existsSync(apiPath) || (0, openapi_core_1.isAbsoluteUrl)(apiPath) ? apiPath : undefined;
78
75
  }
79
76
  function fallbackToAllDefinitions(apis, config) {
80
- return Object.entries(apis).map(([alias, { root }]) => ({
77
+ return Object.entries(apis).map(([alias, { root, output }]) => ({
81
78
  path: (0, openapi_core_1.isAbsoluteUrl)(root) ? root : (0, path_1.resolve)(getConfigDirectory(config), root),
82
79
  alias,
80
+ output: output && (0, path_1.resolve)(getConfigDirectory(config), output),
83
81
  }));
84
82
  }
85
83
  function getAliasOrPath(config, aliasOrPath) {
86
- return config.apis[aliasOrPath]
87
- ? { path: config.apis[aliasOrPath]?.root, alias: aliasOrPath }
84
+ const aliasApi = config.apis[aliasOrPath];
85
+ return aliasApi
86
+ ? {
87
+ path: aliasApi.root,
88
+ alias: aliasOrPath,
89
+ output: aliasApi.output,
90
+ }
88
91
  : {
89
92
  path: aliasOrPath,
90
93
  // find alias by path, take the first match
@@ -93,8 +96,8 @@ function getAliasOrPath(config, aliasOrPath) {
93
96
  })?.[0] ?? undefined,
94
97
  };
95
98
  }
96
- async function expandGlobsInEntrypoints(args, config) {
97
- return (await Promise.all(args.map(async (aliasOrPath) => {
99
+ async function expandGlobsInEntrypoints(argApis, config) {
100
+ return (await Promise.all(argApis.map(async (aliasOrPath) => {
98
101
  return glob.hasMagic(aliasOrPath) && !(0, openapi_core_1.isAbsoluteUrl)(aliasOrPath)
99
102
  ? (await (0, util_1.promisify)(glob)(aliasOrPath)).map((g) => getAliasOrPath(config, g))
100
103
  : getAliasOrPath(config, aliasOrPath);
@@ -303,28 +306,19 @@ function printConfigLintTotals(totals, command) {
303
306
  process.stderr.write((0, colorette_1.green)('✅ Your config is valid.\n'));
304
307
  }
305
308
  }
306
- function getOutputFileName(entrypoint, entries, output, ext) {
307
- if (!output) {
308
- return { outputFile: 'stdout', ext: ext || 'yaml' };
309
- }
309
+ function getOutputFileName(entrypoint, output, ext) {
310
310
  let outputFile = output;
311
- if (entries > 1) {
312
- ext = ext || (0, path_1.extname)(entrypoint).substring(1);
313
- if (!types_1.outputExtensions.includes(ext)) {
314
- throw new Error(`Invalid file extension: ${ext}.`);
315
- }
316
- outputFile = (0, path_1.join)(output, (0, path_1.basename)(entrypoint, (0, path_1.extname)(entrypoint))) + '.' + ext;
311
+ if (!outputFile) {
312
+ return { ext: ext || 'yaml' };
317
313
  }
318
- else {
319
- if (output) {
320
- ext = ext || (0, path_1.extname)(output).substring(1);
321
- }
322
- ext = ext || (0, path_1.extname)(entrypoint).substring(1);
323
- if (!types_1.outputExtensions.includes(ext)) {
324
- throw new Error(`Invalid file extension: ${ext}.`);
325
- }
326
- outputFile = (0, path_1.join)((0, path_1.dirname)(outputFile), (0, path_1.basename)(outputFile, (0, path_1.extname)(outputFile))) + '.' + ext;
314
+ if (outputFile) {
315
+ ext = ext || (0, path_1.extname)(outputFile).substring(1);
316
+ }
317
+ ext = ext || (0, path_1.extname)(entrypoint).substring(1);
318
+ if (!types_1.outputExtensions.includes(ext)) {
319
+ throw new Error(`Invalid file extension: ${ext}.`);
327
320
  }
321
+ outputFile = (0, path_1.join)((0, path_1.dirname)(outputFile), (0, path_1.basename)(outputFile, (0, path_1.extname)(outputFile))) + '.' + ext;
328
322
  return { outputFile, ext };
329
323
  }
330
324
  function printUnusedWarnings(config) {
@@ -462,6 +456,7 @@ async function sendTelemetry(argv, exit_code, has_config, spec_version, spec_key
462
456
  spec_full_version,
463
457
  };
464
458
  await (0, fetch_with_timeout_1.default)(`https://api.redocly.com/registry/telemetry/cli`, {
459
+ timeout: fetch_with_timeout_1.DEFAULT_FETCH_TIMEOUT,
465
460
  method: 'POST',
466
461
  headers: {
467
462
  'content-type': 'application/json',
@@ -32,11 +32,15 @@ exports.notifyUpdateCliVersion = notifyUpdateCliVersion;
32
32
  const isNewVersionAvailable = (current, latest) => (0, semver_1.compare)(current, latest) < 0;
33
33
  const getLatestVersion = async (packageName) => {
34
34
  const latestUrl = `http://registry.npmjs.org/${packageName}/latest`;
35
- const response = await (0, fetch_with_timeout_1.default)(latestUrl);
36
- if (!response)
35
+ try {
36
+ const response = await (0, fetch_with_timeout_1.default)(latestUrl, { timeout: fetch_with_timeout_1.DEFAULT_FETCH_TIMEOUT });
37
+ const info = await response.json();
38
+ return info.version;
39
+ }
40
+ catch {
41
+ // Do nothing
37
42
  return;
38
- const info = await response.json();
39
- return info.version;
43
+ }
40
44
  };
41
45
  const cacheLatestVersion = () => {
42
46
  if (!isNeedToBeCached() || SHOULD_NOT_NOTIFY) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/cli",
3
- "version": "1.22.1",
3
+ "version": "1.23.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "Roman Hotsiy <roman@redoc.ly> (https://redoc.ly/)"
37
37
  ],
38
38
  "dependencies": {
39
- "@redocly/openapi-core": "1.22.1",
39
+ "@redocly/openapi-core": "1.23.0",
40
40
  "abort-controller": "^3.0.0",
41
41
  "chokidar": "^3.5.1",
42
42
  "colorette": "^1.2.0",
@@ -1,7 +1,12 @@
1
- import { bundle, getTotals, getMergedConfig } from '@redocly/openapi-core';
1
+ import { bundle, getTotals, getMergedConfig, Config } from '@redocly/openapi-core';
2
2
 
3
3
  import { BundleOptions, handleBundle } from '../../commands/bundle';
4
- import { handleError } from '../../utils/miscellaneous';
4
+ import {
5
+ getFallbackApisOrExit,
6
+ getOutputFileName,
7
+ handleError,
8
+ saveBundle,
9
+ } from '../../utils/miscellaneous';
5
10
  import { commandWrapper } from '../../wrapper';
6
11
  import SpyInstance = jest.SpyInstance;
7
12
  import { Arguments } from 'yargs';
@@ -9,24 +14,31 @@ import { Arguments } from 'yargs';
9
14
  jest.mock('@redocly/openapi-core');
10
15
  jest.mock('../../utils/miscellaneous');
11
16
 
17
+ // @ts-ignore
18
+ getOutputFileName = jest.requireActual('../../utils/miscellaneous').getOutputFileName;
19
+
12
20
  (getMergedConfig as jest.Mock).mockImplementation((config) => config);
13
21
 
14
22
  describe('bundle', () => {
15
23
  let processExitMock: SpyInstance;
16
24
  let exitCb: any;
17
-
25
+ let stderrWriteMock: any;
26
+ let stdoutWriteMock: any;
18
27
  beforeEach(() => {
19
28
  processExitMock = jest.spyOn(process, 'exit').mockImplementation();
20
29
  jest.spyOn(process, 'once').mockImplementation((_e, cb) => {
21
30
  exitCb = cb;
22
31
  return process.on(_e, cb);
23
32
  });
24
- jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
33
+ stderrWriteMock = jest.spyOn(process.stderr, 'write').mockImplementation(jest.fn());
34
+ stdoutWriteMock = jest.spyOn(process.stdout, 'write').mockImplementation(jest.fn());
25
35
  });
26
36
 
27
37
  afterEach(() => {
28
38
  (bundle as jest.Mock).mockClear();
29
39
  (getTotals as jest.Mock).mockReset();
40
+ stderrWriteMock.mockRestore();
41
+ stdoutWriteMock.mockRestore();
30
42
  });
31
43
 
32
44
  it('bundles definitions', async () => {
@@ -114,4 +126,119 @@ describe('bundle', () => {
114
126
 
115
127
  expect(handleError).toHaveBeenCalledTimes(0);
116
128
  });
129
+
130
+ it('should store bundled API descriptions in the output files described in the apis section of config IF no positional apis provided AND output is specified for both apis', async () => {
131
+ const apis = {
132
+ foo: {
133
+ root: 'foo.yaml',
134
+ output: 'output/foo.yaml',
135
+ },
136
+ bar: {
137
+ root: 'bar.yaml',
138
+ output: 'output/bar.json',
139
+ },
140
+ };
141
+ const config = {
142
+ apis,
143
+ styleguide: {
144
+ skipPreprocessors: jest.fn(),
145
+ skipDecorators: jest.fn(),
146
+ },
147
+ } as unknown as Config;
148
+ // @ts-ignore
149
+ getFallbackApisOrExit = jest
150
+ .fn()
151
+ .mockResolvedValueOnce(
152
+ Object.entries(apis).map(([alias, { root, ...api }]) => ({ ...api, path: root, alias }))
153
+ );
154
+ (getTotals as jest.Mock).mockReturnValue({
155
+ errors: 0,
156
+ warnings: 0,
157
+ ignored: 0,
158
+ });
159
+
160
+ await handleBundle({
161
+ argv: { apis: [] }, // positional
162
+ version: 'test',
163
+ config,
164
+ });
165
+
166
+ expect(saveBundle).toBeCalledTimes(2);
167
+ expect(saveBundle).toHaveBeenNthCalledWith(1, 'output/foo.yaml', expect.any(String));
168
+ expect(saveBundle).toHaveBeenNthCalledWith(2, 'output/bar.json', expect.any(String));
169
+ });
170
+
171
+ it('should store bundled API descriptions in the output files described in the apis section of config AND print the bundled api without the output specified to the terminal IF no positional apis provided AND output is specified for one api', async () => {
172
+ const apis = {
173
+ foo: {
174
+ root: 'foo.yaml',
175
+ output: 'output/foo.yaml',
176
+ },
177
+ bar: {
178
+ root: 'bar.yaml',
179
+ },
180
+ };
181
+ const config = {
182
+ apis,
183
+ styleguide: {
184
+ skipPreprocessors: jest.fn(),
185
+ skipDecorators: jest.fn(),
186
+ },
187
+ } as unknown as Config;
188
+ // @ts-ignore
189
+ getFallbackApisOrExit = jest
190
+ .fn()
191
+ .mockResolvedValueOnce(
192
+ Object.entries(apis).map(([alias, { root, ...api }]) => ({ ...api, path: root, alias }))
193
+ );
194
+ (getTotals as jest.Mock).mockReturnValue({
195
+ errors: 0,
196
+ warnings: 0,
197
+ ignored: 0,
198
+ });
199
+
200
+ await handleBundle({
201
+ argv: { apis: [] }, // positional
202
+ version: 'test',
203
+ config,
204
+ });
205
+
206
+ expect(saveBundle).toBeCalledTimes(1);
207
+ expect(saveBundle).toHaveBeenCalledWith('output/foo.yaml', expect.any(String));
208
+ expect(process.stdout.write).toHaveBeenCalledTimes(1);
209
+ });
210
+
211
+ describe('per api output', () => {
212
+ it('should NOT store bundled API descriptions in the output files described in the apis section of config IF no there is a positional api provided', async () => {
213
+ const apis = {
214
+ foo: {
215
+ root: 'foo.yaml',
216
+ output: 'output/foo.yaml',
217
+ },
218
+ };
219
+ const config = {
220
+ apis,
221
+ styleguide: {
222
+ skipPreprocessors: jest.fn(),
223
+ skipDecorators: jest.fn(),
224
+ },
225
+ } as unknown as Config;
226
+ // @ts-ignore
227
+ getFallbackApisOrExit = jest.fn().mockResolvedValueOnce([{ path: 'openapi.yaml' }]);
228
+ (getTotals as jest.Mock).mockReturnValue({
229
+ errors: 0,
230
+ warnings: 0,
231
+ ignored: 0,
232
+ });
233
+
234
+ await handleBundle({
235
+ argv: { apis: ['openapi.yaml'] }, // positional
236
+ version: 'test',
237
+ config,
238
+ });
239
+
240
+ expect(saveBundle).toBeCalledTimes(0);
241
+ expect(process.stdout.write).toHaveBeenCalledTimes(1);
242
+ });
243
+ });
117
244
  });
@@ -1,23 +1,53 @@
1
1
  import AbortController from 'abort-controller';
2
2
  import fetchWithTimeout from '../utils/fetch-with-timeout';
3
3
  import nodeFetch from 'node-fetch';
4
+ import { getProxyAgent } from '@redocly/openapi-core';
5
+ import { HttpsProxyAgent } from 'https-proxy-agent';
4
6
 
5
7
  jest.mock('node-fetch');
8
+ jest.mock('@redocly/openapi-core');
6
9
 
7
10
  describe('fetchWithTimeout', () => {
11
+ beforeAll(() => {
12
+ // @ts-ignore
13
+ global.setTimeout = jest.fn();
14
+ global.clearTimeout = jest.fn();
15
+ });
16
+
17
+ beforeEach(() => {
18
+ (getProxyAgent as jest.Mock).mockReturnValueOnce(undefined);
19
+ });
20
+
8
21
  afterEach(() => {
9
22
  jest.clearAllMocks();
10
23
  });
11
24
 
12
25
  it('should call node-fetch with signal', async () => {
13
- // @ts-ignore
14
- global.setTimeout = jest.fn();
15
-
16
- global.clearTimeout = jest.fn();
17
- await fetchWithTimeout('url');
26
+ await fetchWithTimeout('url', { timeout: 1000 });
18
27
 
19
28
  expect(global.setTimeout).toHaveBeenCalledTimes(1);
20
- expect(nodeFetch).toHaveBeenCalledWith('url', { signal: new AbortController().signal });
29
+ expect(nodeFetch).toHaveBeenCalledWith('url', {
30
+ signal: new AbortController().signal,
31
+ agent: undefined,
32
+ });
21
33
  expect(global.clearTimeout).toHaveBeenCalledTimes(1);
22
34
  });
35
+
36
+ it('should call node-fetch with proxy agent', async () => {
37
+ (getProxyAgent as jest.Mock).mockRestore();
38
+ const proxyAgent = new HttpsProxyAgent('http://localhost');
39
+ (getProxyAgent as jest.Mock).mockReturnValueOnce(proxyAgent);
40
+
41
+ await fetchWithTimeout('url');
42
+
43
+ expect(nodeFetch).toHaveBeenCalledWith('url', { agent: proxyAgent });
44
+ });
45
+
46
+ it('should call node-fetch without signal when timeout is not passed', async () => {
47
+ await fetchWithTimeout('url');
48
+
49
+ expect(global.setTimeout).not.toHaveBeenCalled();
50
+ expect(nodeFetch).toHaveBeenCalledWith('url', { agent: undefined });
51
+ expect(global.clearTimeout).not.toHaveBeenCalled();
52
+ });
23
53
  });
@@ -27,6 +27,7 @@ import { blue, red, yellow } from 'colorette';
27
27
  import { existsSync, statSync } from 'fs';
28
28
  import * as path from 'path';
29
29
  import * as process from 'process';
30
+ import { ConfigApis } from '../types';
30
31
 
31
32
  jest.mock('os');
32
33
  jest.mock('colorette');
@@ -79,20 +80,6 @@ describe('pathToFilename', () => {
79
80
  });
80
81
  });
81
82
 
82
- describe('getFallbackApisOrExit', () => {
83
- it('should find alias by filename', async () => {
84
- (existsSync as jest.Mock<any, any>).mockImplementationOnce(() => true);
85
- const entry = await getFallbackApisOrExit(['./test.yaml'], {
86
- apis: {
87
- main: {
88
- root: 'test.yaml',
89
- },
90
- },
91
- } as any);
92
- expect(entry).toEqual([{ path: './test.yaml', alias: 'main' }]);
93
- });
94
- });
95
-
96
83
  describe('printConfigLintTotals', () => {
97
84
  const totalProblemsMock: Totals = {
98
85
  errors: 1,
@@ -190,6 +177,7 @@ describe('getFallbackApisOrExit', () => {
190
177
  {
191
178
  alias: 'main',
192
179
  path: 'someFile.yaml',
180
+ output: undefined,
193
181
  },
194
182
  ]);
195
183
  });
@@ -277,6 +265,43 @@ describe('getFallbackApisOrExit', () => {
277
265
  {
278
266
  alias: 'main',
279
267
  path: 'https://someLinkt/petstore.yaml?main',
268
+ output: undefined,
269
+ },
270
+ ]);
271
+
272
+ (isAbsoluteUrl as jest.Mock<any, any>).mockReset();
273
+ });
274
+
275
+ it('should find alias by filename', async () => {
276
+ (existsSync as jest.Mock<any, any>).mockImplementationOnce(() => true);
277
+ const entry = await getFallbackApisOrExit(['./test.yaml'], {
278
+ apis: {
279
+ main: {
280
+ root: 'test.yaml',
281
+ styleguide: {},
282
+ },
283
+ },
284
+ });
285
+ expect(entry).toEqual([{ path: './test.yaml', alias: 'main' }]);
286
+ });
287
+
288
+ it('should return apis from config with paths and outputs resolved relatively to the config location', async () => {
289
+ (existsSync as jest.Mock<any, any>).mockImplementationOnce(() => true);
290
+ const entry = await getFallbackApisOrExit(undefined, {
291
+ apis: {
292
+ main: {
293
+ root: 'test.yaml',
294
+ output: 'output/test.yaml',
295
+ styleguide: {},
296
+ },
297
+ },
298
+ configFile: 'project-folder/redocly.yaml',
299
+ });
300
+ expect(entry).toEqual([
301
+ {
302
+ path: expect.stringMatching(/project\-folder\/test\.yaml$/),
303
+ output: expect.stringMatching(/project\-folder\/output\/test\.yaml$/),
304
+ alias: 'main',
280
305
  },
281
306
  ]);
282
307
  });
@@ -591,28 +616,28 @@ describe('cleanRawInput', () => {
591
616
  expect(stderrMock).toHaveBeenCalledWith(`Unsupported file extension: xml. Using yaml.\n`);
592
617
  });
593
618
  });
619
+ });
594
620
 
595
- describe('writeToFileByExtension', () => {
596
- beforeEach(() => {
597
- jest.spyOn(process.stderr, 'write').mockImplementation(jest.fn());
598
- (yellow as jest.Mock<any, any>).mockImplementation((text: string) => text);
599
- });
621
+ describe('writeToFileByExtension', () => {
622
+ beforeEach(() => {
623
+ jest.spyOn(process.stderr, 'write').mockImplementation(jest.fn());
624
+ (yellow as jest.Mock<any, any>).mockImplementation((text: string) => text);
625
+ });
600
626
 
601
- afterEach(() => {
602
- jest.restoreAllMocks();
603
- });
627
+ afterEach(() => {
628
+ jest.restoreAllMocks();
629
+ });
604
630
 
605
- it('should call stringifyYaml function', () => {
606
- writeToFileByExtension('test data', 'test.yaml');
607
- expect(stringifyYaml).toHaveBeenCalledWith('test data', { noRefs: false });
608
- expect(process.stderr.write).toHaveBeenCalledWith(`test data`);
609
- });
631
+ it('should call stringifyYaml function', () => {
632
+ writeToFileByExtension('test data', 'test.yaml');
633
+ expect(stringifyYaml).toHaveBeenCalledWith('test data', { noRefs: false });
634
+ expect(process.stderr.write).toHaveBeenCalledWith(`test data`);
635
+ });
610
636
 
611
- it('should call JSON.stringify function', () => {
612
- const stringifySpy = jest.spyOn(JSON, 'stringify').mockImplementation((data) => data);
613
- writeToFileByExtension('test data', 'test.json');
614
- expect(stringifySpy).toHaveBeenCalledWith('test data', null, 2);
615
- expect(process.stderr.write).toHaveBeenCalledWith(`test data`);
616
- });
637
+ it('should call JSON.stringify function', () => {
638
+ const stringifySpy = jest.spyOn(JSON, 'stringify').mockImplementation((data) => data);
639
+ writeToFileByExtension('test data', 'test.json');
640
+ expect(stringifySpy).toHaveBeenCalledWith('test data', null, 2);
641
+ expect(process.stderr.write).toHaveBeenCalledWith(`test data`);
617
642
  });
618
643
  });
@@ -1,7 +1,7 @@
1
1
  import fetch, { Response } from 'node-fetch';
2
2
  import * as FormData from 'form-data';
3
3
 
4
- import { ReuniteApiClient, PushPayload } from '../api-client';
4
+ import { ReuniteApiClient, PushPayload, ReuniteApiError } from '../api-client';
5
5
 
6
6
  jest.mock('node-fetch', () => ({
7
7
  default: jest.fn(),
@@ -16,12 +16,15 @@ describe('ApiClient', () => {
16
16
  const testDomain = 'test-domain.com';
17
17
  const testOrg = 'test-org';
18
18
  const testProject = 'test-project';
19
+ const version = '1.0.0';
20
+ const command = 'push';
21
+ const expectedUserAgent = `redocly-cli/${version} ${command}`;
19
22
 
20
23
  describe('getDefaultBranch()', () => {
21
24
  let apiClient: ReuniteApiClient;
22
25
 
23
26
  beforeEach(() => {
24
- apiClient = new ReuniteApiClient(testDomain, testToken);
27
+ apiClient = new ReuniteApiClient({ domain: testDomain, apiKey: testToken, version, command });
25
28
  });
26
29
 
27
30
  it('should get default project branch', async () => {
@@ -41,6 +44,7 @@ describe('ApiClient', () => {
41
44
  headers: {
42
45
  'Content-Type': 'application/json',
43
46
  Authorization: `Bearer ${testToken}`,
47
+ 'user-agent': expectedUserAgent,
44
48
  },
45
49
  signal: expect.any(Object),
46
50
  }
@@ -62,7 +66,7 @@ describe('ApiClient', () => {
62
66
  });
63
67
 
64
68
  await expect(apiClient.remotes.getDefaultBranch(testOrg, testProject)).rejects.toThrow(
65
- new Error('Failed to fetch default branch: Project source not found')
69
+ new ReuniteApiError('Failed to fetch default branch. Project source not found.', 404)
66
70
  );
67
71
  });
68
72
 
@@ -76,7 +80,7 @@ describe('ApiClient', () => {
76
80
  });
77
81
 
78
82
  await expect(apiClient.remotes.getDefaultBranch(testOrg, testProject)).rejects.toThrow(
79
- new Error('Failed to fetch default branch: Not found')
83
+ new ReuniteApiError('Failed to fetch default branch. Not found.', 404)
80
84
  );
81
85
  });
82
86
  });
@@ -89,7 +93,7 @@ describe('ApiClient', () => {
89
93
  let apiClient: ReuniteApiClient;
90
94
 
91
95
  beforeEach(() => {
92
- apiClient = new ReuniteApiClient(testDomain, testToken);
96
+ apiClient = new ReuniteApiClient({ domain: testDomain, apiKey: testToken, version, command });
93
97
  });
94
98
 
95
99
  it('should upsert remote', async () => {
@@ -116,6 +120,7 @@ describe('ApiClient', () => {
116
120
  headers: {
117
121
  'Content-Type': 'application/json',
118
122
  Authorization: `Bearer ${testToken}`,
123
+ 'user-agent': expectedUserAgent,
119
124
  },
120
125
  body: JSON.stringify({
121
126
  mountPath: remotePayload.mountPath,
@@ -144,8 +149,9 @@ describe('ApiClient', () => {
144
149
  });
145
150
 
146
151
  await expect(apiClient.remotes.upsert(testOrg, testProject, remotePayload)).rejects.toThrow(
147
- new Error(
148
- 'Failed to upsert remote: Not allowed to mount remote outside of project content path: /docs'
152
+ new ReuniteApiError(
153
+ 'Failed to upsert remote. Not allowed to mount remote outside of project content path: /docs.',
154
+ 403
149
155
  )
150
156
  );
151
157
  });
@@ -153,6 +159,7 @@ describe('ApiClient', () => {
153
159
  it('should throw statusText error if response is not ok', async () => {
154
160
  mockFetchResponse({
155
161
  ok: false,
162
+ status: 404,
156
163
  statusText: 'Not found',
157
164
  json: jest.fn().mockResolvedValue({
158
165
  unknownField: 'unknown-error',
@@ -160,7 +167,7 @@ describe('ApiClient', () => {
160
167
  });
161
168
 
162
169
  await expect(apiClient.remotes.upsert(testOrg, testProject, remotePayload)).rejects.toThrow(
163
- new Error('Failed to upsert remote: Not found')
170
+ new ReuniteApiError('Failed to upsert remote. Not found.', 404)
164
171
  );
165
172
  });
166
173
  });
@@ -200,7 +207,7 @@ describe('ApiClient', () => {
200
207
  let apiClient: ReuniteApiClient;
201
208
 
202
209
  beforeEach(() => {
203
- apiClient = new ReuniteApiClient(testDomain, testToken);
210
+ apiClient = new ReuniteApiClient({ domain: testDomain, apiKey: testToken, version, command });
204
211
  });
205
212
 
206
213
  it('should push to remote', async () => {
@@ -234,6 +241,7 @@ describe('ApiClient', () => {
234
241
  method: 'POST',
235
242
  headers: {
236
243
  Authorization: `Bearer ${testToken}`,
244
+ 'user-agent': expectedUserAgent,
237
245
  },
238
246
  })
239
247
  );
@@ -258,12 +266,13 @@ describe('ApiClient', () => {
258
266
 
259
267
  await expect(
260
268
  apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock)
261
- ).rejects.toThrow(new Error('Failed to push: Cannot push to remote'));
269
+ ).rejects.toThrow(new ReuniteApiError('Failed to push. Cannot push to remote.', 403));
262
270
  });
263
271
 
264
272
  it('should throw statusText error if response is not ok', async () => {
265
273
  mockFetchResponse({
266
274
  ok: false,
275
+ status: 404,
267
276
  statusText: 'Not found',
268
277
  json: jest.fn().mockResolvedValue({
269
278
  unknownField: 'unknown-error',
@@ -272,7 +281,7 @@ describe('ApiClient', () => {
272
281
 
273
282
  await expect(
274
283
  apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock)
275
- ).rejects.toThrow(new Error('Failed to push: Not found'));
284
+ ).rejects.toThrow(new ReuniteApiError('Failed to push. Not found.', 404));
276
285
  });
277
286
  });
278
287
  });