@redocly/cli 1.6.0 → 1.8.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 (126) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +5 -5
  3. package/lib/__tests__/commands/build-docs.test.js +3 -3
  4. package/lib/__tests__/commands/bundle.test.js +5 -5
  5. package/lib/__tests__/commands/join.test.js +11 -11
  6. package/lib/__tests__/commands/lint.test.js +14 -14
  7. package/lib/__tests__/commands/push-region.test.js +1 -1
  8. package/lib/__tests__/commands/push.test.js +11 -11
  9. package/lib/__tests__/fetch-with-timeout.test.js +4 -13
  10. package/lib/__tests__/spinner.test.js +43 -0
  11. package/lib/__tests__/utils.test.js +54 -36
  12. package/lib/__tests__/wrapper.test.js +8 -8
  13. package/lib/cms/api/__tests__/api-keys.test.d.ts +1 -0
  14. package/lib/cms/api/__tests__/api-keys.test.js +26 -0
  15. package/lib/cms/api/__tests__/api.client.test.d.ts +1 -0
  16. package/lib/cms/api/__tests__/api.client.test.js +217 -0
  17. package/lib/cms/api/__tests__/domains.test.d.ts +1 -0
  18. package/lib/cms/api/__tests__/domains.test.js +13 -0
  19. package/lib/cms/api/api-client.d.ts +50 -0
  20. package/lib/cms/api/api-client.js +148 -0
  21. package/lib/cms/api/api-keys.d.ts +1 -0
  22. package/lib/cms/api/api-keys.js +24 -0
  23. package/lib/cms/api/domains.d.ts +1 -0
  24. package/lib/cms/api/domains.js +12 -0
  25. package/lib/cms/api/index.d.ts +3 -0
  26. package/lib/cms/api/index.js +19 -0
  27. package/lib/cms/api/types.d.ts +91 -0
  28. package/lib/cms/api/types.js +2 -0
  29. package/lib/cms/commands/__tests__/push-status.test.d.ts +1 -0
  30. package/lib/cms/commands/__tests__/push-status.test.js +164 -0
  31. package/lib/cms/commands/__tests__/push.test.d.ts +1 -0
  32. package/lib/cms/commands/__tests__/push.test.js +226 -0
  33. package/lib/cms/commands/push-status.d.ts +12 -0
  34. package/lib/cms/commands/push-status.js +150 -0
  35. package/lib/cms/commands/push.d.ts +23 -0
  36. package/lib/cms/commands/push.js +142 -0
  37. package/lib/cms/utils.d.ts +2 -0
  38. package/lib/cms/utils.js +6 -0
  39. package/lib/commands/build-docs/index.js +4 -4
  40. package/lib/commands/build-docs/utils.js +2 -2
  41. package/lib/commands/bundle.js +13 -13
  42. package/lib/commands/join.js +25 -25
  43. package/lib/commands/lint.js +10 -10
  44. package/lib/commands/login.js +2 -2
  45. package/lib/commands/preview-docs/index.js +4 -4
  46. package/lib/commands/preview-docs/preview-server/preview-server.js +2 -2
  47. package/lib/commands/preview-project/constants.d.ts +14 -0
  48. package/lib/commands/preview-project/constants.js +22 -0
  49. package/lib/commands/preview-project/index.d.ts +2 -0
  50. package/lib/commands/preview-project/index.js +58 -0
  51. package/lib/commands/preview-project/types.d.ts +10 -0
  52. package/lib/commands/preview-project/types.js +2 -0
  53. package/lib/commands/push.d.ts +5 -0
  54. package/lib/commands/push.js +25 -17
  55. package/lib/commands/split/__tests__/index.test.js +2 -2
  56. package/lib/commands/split/index.js +20 -20
  57. package/lib/commands/stats.js +4 -4
  58. package/lib/index.d.ts +1 -1
  59. package/lib/index.js +169 -25
  60. package/lib/types.d.ts +9 -1
  61. package/lib/{__mocks__/utils.js → utils/__mocks__/miscellaneous.js} +1 -1
  62. package/lib/utils/assert-node-version.d.ts +1 -0
  63. package/lib/{fetch-with-timeout.js → utils/fetch-with-timeout.js} +2 -7
  64. package/lib/{utils.d.ts → utils/miscellaneous.d.ts} +1 -1
  65. package/lib/{utils.js → utils/miscellaneous.js} +20 -2
  66. package/lib/utils/spinner.d.ts +10 -0
  67. package/lib/utils/spinner.js +42 -0
  68. package/lib/{update-version-notifier.js → utils/update-version-notifier.js} +4 -4
  69. package/lib/wrapper.js +5 -5
  70. package/package.json +5 -3
  71. package/src/__tests__/commands/build-docs.test.ts +2 -2
  72. package/src/__tests__/commands/bundle.test.ts +2 -2
  73. package/src/__tests__/commands/join.test.ts +2 -2
  74. package/src/__tests__/commands/lint.test.ts +3 -3
  75. package/src/__tests__/commands/push-region.test.ts +1 -1
  76. package/src/__tests__/commands/push.test.ts +2 -2
  77. package/src/__tests__/fetch-with-timeout.test.ts +4 -16
  78. package/src/__tests__/spinner.test.ts +51 -0
  79. package/src/__tests__/utils.test.ts +20 -5
  80. package/src/__tests__/wrapper.test.ts +2 -2
  81. package/src/cms/api/__tests__/api-keys.test.ts +37 -0
  82. package/src/cms/api/__tests__/api.client.test.ts +275 -0
  83. package/src/cms/api/__tests__/domains.test.ts +15 -0
  84. package/src/cms/api/api-client.ts +199 -0
  85. package/src/cms/api/api-keys.ts +26 -0
  86. package/src/cms/api/domains.ts +11 -0
  87. package/src/cms/api/index.ts +3 -0
  88. package/src/cms/api/types.ts +101 -0
  89. package/src/cms/commands/__tests__/push-status.test.ts +212 -0
  90. package/src/cms/commands/__tests__/push.test.ts +293 -0
  91. package/src/cms/commands/push-status.ts +203 -0
  92. package/src/cms/commands/push.ts +215 -0
  93. package/src/cms/utils.ts +1 -0
  94. package/src/commands/build-docs/index.ts +1 -1
  95. package/src/commands/build-docs/utils.ts +1 -1
  96. package/src/commands/bundle.ts +2 -2
  97. package/src/commands/join.ts +2 -2
  98. package/src/commands/lint.ts +1 -1
  99. package/src/commands/login.ts +1 -1
  100. package/src/commands/preview-docs/index.ts +5 -1
  101. package/src/commands/preview-docs/preview-server/preview-server.ts +1 -1
  102. package/src/commands/preview-project/constants.ts +23 -0
  103. package/src/commands/preview-project/index.ts +58 -0
  104. package/src/commands/preview-project/types.ts +12 -0
  105. package/src/commands/push.ts +15 -1
  106. package/src/commands/split/__tests__/index.test.ts +3 -4
  107. package/src/commands/split/index.ts +5 -5
  108. package/src/commands/stats.ts +2 -2
  109. package/src/index.ts +184 -28
  110. package/src/types.ts +12 -1
  111. package/src/{__mocks__/utils.ts → utils/__mocks__/miscellaneous.ts} +1 -1
  112. package/src/{fetch-with-timeout.ts → utils/fetch-with-timeout.ts} +1 -6
  113. package/src/{utils.ts → utils/miscellaneous.ts} +20 -2
  114. package/src/utils/spinner.ts +50 -0
  115. package/src/{update-version-notifier.ts → utils/update-version-notifier.ts} +2 -2
  116. package/src/wrapper.ts +7 -2
  117. package/tsconfig.tsbuildinfo +1 -1
  118. /package/lib/{assert-node-version.d.ts → __tests__/spinner.test.d.ts} +0 -0
  119. /package/lib/{__mocks__/utils.d.ts → utils/__mocks__/miscellaneous.d.ts} +0 -0
  120. /package/lib/{assert-node-version.js → utils/assert-node-version.js} +0 -0
  121. /package/lib/{fetch-with-timeout.d.ts → utils/fetch-with-timeout.d.ts} +0 -0
  122. /package/lib/{js-utils.d.ts → utils/js-utils.d.ts} +0 -0
  123. /package/lib/{js-utils.js → utils/js-utils.js} +0 -0
  124. /package/lib/{update-version-notifier.d.ts → utils/update-version-notifier.d.ts} +0 -0
  125. /package/src/{assert-node-version.ts → utils/assert-node-version.ts} +0 -0
  126. /package/src/{js-utils.ts → utils/js-utils.ts} +0 -0
package/lib/wrapper.js CHANGED
@@ -11,8 +11,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.commandWrapper = void 0;
13
13
  const openapi_core_1 = require("@redocly/openapi-core");
14
- const update_version_notifier_1 = require("./update-version-notifier");
15
- const utils_1 = require("./utils");
14
+ const update_version_notifier_1 = require("./utils/update-version-notifier");
15
+ const miscellaneous_1 = require("./utils/miscellaneous");
16
16
  const lint_1 = require("./commands/lint");
17
17
  function commandWrapper(commandHandler) {
18
18
  return (argv) => __awaiter(this, void 0, void 0, function* () {
@@ -21,9 +21,9 @@ function commandWrapper(commandHandler) {
21
21
  let telemetry;
22
22
  try {
23
23
  if (argv.config && !(0, openapi_core_1.doesYamlFileExist)(argv.config)) {
24
- (0, utils_1.exitWithError)('Please, provide valid path to the configuration file');
24
+ (0, miscellaneous_1.exitWithError)('Please, provide valid path to the configuration file');
25
25
  }
26
- const config = (yield (0, utils_1.loadConfigAndHandleErrors)({
26
+ const config = (yield (0, miscellaneous_1.loadConfigAndHandleErrors)({
27
27
  configPath: argv.config,
28
28
  customExtends: argv.extends,
29
29
  region: argv.region,
@@ -41,7 +41,7 @@ function commandWrapper(commandHandler) {
41
41
  }
42
42
  finally {
43
43
  if (process.env.REDOCLY_TELEMETRY !== 'off' && telemetry !== 'off') {
44
- yield (0, utils_1.sendTelemetry)(argv, code, hasConfig);
44
+ yield (0, miscellaneous_1.sendTelemetry)(argv, code, hasConfig);
45
45
  }
46
46
  process.once('beforeExit', () => {
47
47
  process.exit(code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/cli",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -36,10 +36,12 @@
36
36
  "Roman Hotsiy <roman@redoc.ly> (https://redoc.ly/)"
37
37
  ],
38
38
  "dependencies": {
39
- "@redocly/openapi-core": "1.6.0",
39
+ "@redocly/openapi-core": "1.8.0",
40
+ "abort-controller": "^3.0.0",
40
41
  "chokidar": "^3.5.1",
41
42
  "colorette": "^1.2.0",
42
43
  "core-js": "^3.32.1",
44
+ "form-data": "^4.0.0",
43
45
  "get-port-please": "^3.0.1",
44
46
  "glob": "^7.1.6",
45
47
  "handlebars": "^4.7.6",
@@ -59,7 +61,7 @@
59
61
  "@types/react": "^17.0.0 || ^18.2.21",
60
62
  "@types/react-dom": "^17.0.0 || ^18.2.7",
61
63
  "@types/semver": "^7.5.0",
62
- "@types/yargs": "17.0.5",
64
+ "@types/yargs": "17.0.32",
63
65
  "typescript": "^5.2.2"
64
66
  }
65
67
  }
@@ -3,11 +3,11 @@ import { renderToString } from 'react-dom/server';
3
3
  import { handlerBuildCommand } from '../../commands/build-docs';
4
4
  import { BuildDocsArgv } from '../../commands/build-docs/types';
5
5
  import { getPageHTML } from '../../commands/build-docs/utils';
6
- import { getFallbackApisOrExit } from '../../utils';
6
+ import { getFallbackApisOrExit } from '../../utils/miscellaneous';
7
7
 
8
8
  jest.mock('redoc');
9
9
  jest.mock('fs');
10
- jest.mock('../../utils');
10
+ jest.mock('../../utils/miscellaneous');
11
11
 
12
12
  const config = {
13
13
  output: '',
@@ -1,13 +1,13 @@
1
1
  import { lint, bundle, getTotals, getMergedConfig } from '@redocly/openapi-core';
2
2
 
3
3
  import { BundleOptions, handleBundle } from '../../commands/bundle';
4
- import { handleError } from '../../utils';
4
+ import { handleError } from '../../utils/miscellaneous';
5
5
  import { commandWrapper } from '../../wrapper';
6
6
  import SpyInstance = jest.SpyInstance;
7
7
  import { Arguments } from 'yargs';
8
8
 
9
9
  jest.mock('@redocly/openapi-core');
10
- jest.mock('../../utils');
10
+ jest.mock('../../utils/miscellaneous');
11
11
 
12
12
  (getMergedConfig as jest.Mock).mockImplementation((config) => config);
13
13
 
@@ -1,11 +1,11 @@
1
1
  import { handleJoin } from '../../commands/join';
2
- import { exitWithError, writeToFileByExtension, writeYaml } from '../../utils';
2
+ import { exitWithError, writeToFileByExtension, writeYaml } from '../../utils/miscellaneous';
3
3
  import { yellow } from 'colorette';
4
4
  import { detectSpec } from '@redocly/openapi-core';
5
5
  import { loadConfig } from '../../__mocks__/@redocly/openapi-core';
6
6
  import { ConfigFixture } from '../fixtures/config';
7
7
 
8
- jest.mock('../../utils');
8
+ jest.mock('../../utils/miscellaneous');
9
9
 
10
10
  jest.mock('colorette');
11
11
 
@@ -14,7 +14,7 @@ import {
14
14
  exitWithError,
15
15
  loadConfigAndHandleErrors,
16
16
  checkIfRulesetExist,
17
- } from '../../utils';
17
+ } from '../../utils/miscellaneous';
18
18
  import { ConfigFixture } from '../fixtures/config';
19
19
  import { performance } from 'perf_hooks';
20
20
  import { commandWrapper } from '../../wrapper';
@@ -22,10 +22,10 @@ import { Arguments } from 'yargs';
22
22
  import { blue } from 'colorette';
23
23
 
24
24
  jest.mock('@redocly/openapi-core');
25
- jest.mock('../../utils');
25
+ jest.mock('../../utils/miscellaneous');
26
26
  jest.mock('perf_hooks');
27
27
 
28
- jest.mock('../../update-version-notifier', () => ({
28
+ jest.mock('../../utils/update-version-notifier', () => ({
29
29
  version: '1.0.0',
30
30
  }));
31
31
 
@@ -12,7 +12,7 @@ jest.mock('node-fetch', () => ({
12
12
  }));
13
13
  jest.mock('@redocly/openapi-core');
14
14
  jest.mock('../../commands/login');
15
- jest.mock('../../utils');
15
+ jest.mock('../../utils/miscellaneous');
16
16
 
17
17
  (getMergedConfig as jest.Mock).mockImplementation((config) => config);
18
18
 
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import { Config, getMergedConfig } from '@redocly/openapi-core';
3
- import { exitWithError } from '../../utils';
3
+ import { exitWithError } from '../../utils/miscellaneous';
4
4
  import { getApiRoot, getDestinationProps, handlePush, transformPush } from '../../commands/push';
5
5
  import { ConfigFixture } from '../fixtures/config';
6
6
  import { yellow } from 'colorette';
@@ -13,7 +13,7 @@ jest.mock('node-fetch', () => ({
13
13
  })),
14
14
  }));
15
15
  jest.mock('@redocly/openapi-core');
16
- jest.mock('../../utils');
16
+ jest.mock('../../utils/miscellaneous');
17
17
 
18
18
  (getMergedConfig as jest.Mock).mockImplementation((config) => config);
19
19
 
@@ -1,4 +1,5 @@
1
- import fetchWithTimeout from '../fetch-with-timeout';
1
+ import AbortController from 'abort-controller';
2
+ import fetchWithTimeout from '../utils/fetch-with-timeout';
2
3
  import nodeFetch from 'node-fetch';
3
4
 
4
5
  jest.mock('node-fetch');
@@ -8,20 +9,7 @@ describe('fetchWithTimeout', () => {
8
9
  jest.clearAllMocks();
9
10
  });
10
11
 
11
- it('should use bare node-fetch if AbortController is not available', async () => {
12
- // @ts-ignore
13
- global.AbortController = undefined;
14
- // @ts-ignore
15
- global.setTimeout = jest.fn();
16
- await fetchWithTimeout('url', { method: 'GET' });
17
-
18
- expect(nodeFetch).toHaveBeenCalledWith('url', { method: 'GET' });
19
-
20
- expect(global.setTimeout).toHaveBeenCalledTimes(0);
21
- });
22
-
23
- it('should call node-fetch with signal if AbortController is available', async () => {
24
- global.AbortController = jest.fn().mockImplementation(() => ({ signal: 'something' }));
12
+ it('should call node-fetch with signal', async () => {
25
13
  // @ts-ignore
26
14
  global.setTimeout = jest.fn();
27
15
 
@@ -29,7 +17,7 @@ describe('fetchWithTimeout', () => {
29
17
  await fetchWithTimeout('url');
30
18
 
31
19
  expect(global.setTimeout).toHaveBeenCalledTimes(1);
32
- expect(nodeFetch).toHaveBeenCalledWith('url', { signal: 'something' });
20
+ expect(nodeFetch).toHaveBeenCalledWith('url', { signal: new AbortController().signal });
33
21
  expect(global.clearTimeout).toHaveBeenCalledTimes(1);
34
22
  });
35
23
  });
@@ -0,0 +1,51 @@
1
+ import { Spinner } from '../utils/spinner';
2
+ import * as process from 'process';
3
+
4
+ jest.useFakeTimers();
5
+
6
+ describe('Spinner', () => {
7
+ const IS_TTY = process.stdout.isTTY;
8
+
9
+ let writeMock: jest.SpyInstance;
10
+ let spinner: Spinner;
11
+
12
+ beforeEach(() => {
13
+ process.stdout.isTTY = true;
14
+ writeMock = jest.spyOn(process.stdout, 'write').mockImplementation(jest.fn());
15
+ spinner = new Spinner();
16
+ });
17
+
18
+ afterEach(() => {
19
+ writeMock.mockRestore();
20
+ jest.clearAllTimers();
21
+ });
22
+
23
+ afterAll(() => {
24
+ process.stdout.isTTY = IS_TTY;
25
+ });
26
+
27
+ it('starts the spinner', () => {
28
+ spinner.start('Loading');
29
+ jest.advanceTimersByTime(100);
30
+ expect(writeMock).toHaveBeenCalledWith('\r⠋ Loading');
31
+ });
32
+
33
+ it('stops the spinner', () => {
34
+ spinner.start('Loading');
35
+ spinner.stop();
36
+ expect(writeMock).toHaveBeenCalledWith('\r');
37
+ });
38
+
39
+ it('should write 3 frames', () => {
40
+ spinner.start('Loading');
41
+ jest.advanceTimersByTime(300);
42
+ expect(writeMock).toHaveBeenCalledTimes(3);
43
+ });
44
+
45
+ it('should call write 1 times if CI set to true', () => {
46
+ process.stdout.isTTY = false;
47
+ spinner.start('Loading');
48
+ jest.advanceTimersByTime(300);
49
+ expect(writeMock).toHaveBeenCalledTimes(1);
50
+ });
51
+ });
@@ -13,10 +13,8 @@ import {
13
13
  cleanArgs,
14
14
  cleanRawInput,
15
15
  getAndValidateFileExtension,
16
- writeYaml,
17
- writeJson,
18
16
  writeToFileByExtension,
19
- } from '../utils';
17
+ } from '../utils/miscellaneous';
20
18
  import {
21
19
  ResolvedApi,
22
20
  Totals,
@@ -26,10 +24,9 @@ import {
26
24
  stringifyYaml,
27
25
  } from '@redocly/openapi-core';
28
26
  import { blue, red, yellow } from 'colorette';
29
- import { existsSync, statSync, writeFileSync } from 'fs';
27
+ import { existsSync, statSync } from 'fs';
30
28
  import * as path from 'path';
31
29
  import * as process from 'process';
32
- import * as utils from '../utils';
33
30
 
34
31
  jest.mock('os');
35
32
  jest.mock('colorette');
@@ -295,6 +292,24 @@ describe('langToExt', () => {
295
292
  ['javascript', '.js'],
296
293
  ['js', '.js'],
297
294
  ['python', '.py'],
295
+ ['c', '.c'],
296
+ ['c++', '.cpp'],
297
+ ['coffeescript', '.litcoffee'],
298
+ ['dart', '.dart'],
299
+ ['elixir', '.ex'],
300
+ ['go', '.go'],
301
+ ['groovy', '.groovy'],
302
+ ['java', '.java'],
303
+ ['kotlin', '.kt'],
304
+ ['objective-c', '.m'],
305
+ ['perl', '.pl'],
306
+ ['powershell', '.ps1'],
307
+ ['ruby', '.rb'],
308
+ ['rust', '.rs'],
309
+ ['scala', '.sc'],
310
+ ['swift', '.swift'],
311
+ ['typescript', '.ts'],
312
+ ['tsx', '.tsx'],
298
313
  ])('should infer file extension from lang - %s', (lang, expected) => {
299
314
  expect(langToExt(lang)).toBe(expected);
300
315
  });
@@ -1,4 +1,4 @@
1
- import { loadConfigAndHandleErrors, sendTelemetry } from '../utils';
1
+ import { loadConfigAndHandleErrors, sendTelemetry } from '../utils/miscellaneous';
2
2
  import * as process from 'process';
3
3
  import { commandWrapper } from '../wrapper';
4
4
  import { handleLint } from '../commands/lint';
@@ -6,7 +6,7 @@ import { Arguments } from 'yargs';
6
6
  import { handlePush, PushOptions } from '../commands/push';
7
7
 
8
8
  jest.mock('node-fetch');
9
- jest.mock('../utils', () => ({
9
+ jest.mock('../utils/miscellaneous', () => ({
10
10
  sendTelemetry: jest.fn(),
11
11
  loadConfigAndHandleErrors: jest.fn(),
12
12
  }));
@@ -0,0 +1,37 @@
1
+ import { getApiKeys } from '../api-keys';
2
+ import * as fs from 'fs';
3
+
4
+ describe('getApiKeys()', () => {
5
+ afterEach(() => {
6
+ jest.resetAllMocks();
7
+ });
8
+
9
+ it('should return api key from environment variable', () => {
10
+ process.env.REDOCLY_AUTHORIZATION = 'test-api-key';
11
+
12
+ expect(getApiKeys('test-domain')).toEqual('test-api-key');
13
+ });
14
+
15
+ it('should return api key from credentials file', () => {
16
+ process.env.REDOCLY_AUTHORIZATION = '';
17
+
18
+ jest.spyOn(fs, 'existsSync').mockReturnValue(true);
19
+ jest.spyOn(fs, 'readFileSync').mockReturnValue(
20
+ JSON.stringify({
21
+ ['test-domain']: 'test-api-key-from-credentials-file',
22
+ })
23
+ );
24
+
25
+ expect(getApiKeys('test-domain')).toEqual('test-api-key-from-credentials-file');
26
+ });
27
+
28
+ it('should throw an error if no api key provided', () => {
29
+ process.env.REDOCLY_AUTHORIZATION = '';
30
+
31
+ jest.spyOn(fs, 'existsSync').mockReturnValue(false);
32
+
33
+ expect(() => getApiKeys('test-domain')).toThrowError(
34
+ 'No api key provided, please use environment variable REDOCLY_AUTHORIZATION.'
35
+ );
36
+ });
37
+ });
@@ -0,0 +1,275 @@
1
+ import fetch, { Response } from 'node-fetch';
2
+ import * as FormData from 'form-data';
3
+
4
+ import { ReuniteApiClient, PushPayload } from '../api-client';
5
+
6
+ jest.mock('node-fetch', () => ({
7
+ default: jest.fn(),
8
+ }));
9
+
10
+ function mockFetchResponse(response: any) {
11
+ (fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue(response as unknown as Response);
12
+ }
13
+
14
+ describe('ApiClient', () => {
15
+ const testToken = 'test-token';
16
+ const testDomain = 'test-domain.com';
17
+ const testOrg = 'test-org';
18
+ const testProject = 'test-project';
19
+
20
+ describe('getDefaultBranch()', () => {
21
+ let apiClient: ReuniteApiClient;
22
+
23
+ beforeEach(() => {
24
+ apiClient = new ReuniteApiClient(testDomain, testToken);
25
+ });
26
+
27
+ it('should get default project branch', async () => {
28
+ mockFetchResponse({
29
+ ok: true,
30
+ json: jest.fn().mockResolvedValue({
31
+ branchName: 'test-branch',
32
+ }),
33
+ });
34
+
35
+ const result = await apiClient.remotes.getDefaultBranch(testOrg, testProject);
36
+
37
+ expect(fetch).toHaveBeenCalledWith(
38
+ `${testDomain}/api/orgs/${testOrg}/projects/${testProject}/source`,
39
+ {
40
+ method: 'GET',
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ Authorization: `Bearer ${testToken}`,
44
+ },
45
+ }
46
+ );
47
+
48
+ expect(result).toEqual('test-branch');
49
+ });
50
+
51
+ it('should throw parsed error if response is not ok', async () => {
52
+ mockFetchResponse({
53
+ ok: false,
54
+ json: jest.fn().mockResolvedValue({
55
+ type: 'about:blank',
56
+ title: 'Project source not found',
57
+ status: 404,
58
+ detail: 'Not Found',
59
+ object: 'problem',
60
+ }),
61
+ });
62
+
63
+ await expect(apiClient.remotes.getDefaultBranch(testOrg, testProject)).rejects.toThrow(
64
+ new Error('Failed to fetch default branch: Project source not found')
65
+ );
66
+ });
67
+
68
+ it('should throw statusText error if response is not ok', async () => {
69
+ mockFetchResponse({
70
+ ok: false,
71
+ statusText: 'Not found',
72
+ json: jest.fn().mockResolvedValue({
73
+ unknownField: 'unknown-error',
74
+ }),
75
+ });
76
+
77
+ await expect(apiClient.remotes.getDefaultBranch(testOrg, testProject)).rejects.toThrow(
78
+ new Error('Failed to fetch default branch: Not found')
79
+ );
80
+ });
81
+ });
82
+
83
+ describe('upsert()', () => {
84
+ const remotePayload = {
85
+ mountBranchName: 'remote-mount-branch-name',
86
+ mountPath: 'remote-mount-path',
87
+ };
88
+ let apiClient: ReuniteApiClient;
89
+
90
+ beforeEach(() => {
91
+ apiClient = new ReuniteApiClient(testDomain, testToken);
92
+ });
93
+
94
+ it('should upsert remote', async () => {
95
+ const responseMock = {
96
+ id: 'remote-id',
97
+ type: 'CICD',
98
+ mountPath: 'remote-mount-path',
99
+ mountBranchName: 'remote-mount-branch-name',
100
+ organizationId: testOrg,
101
+ projectId: testProject,
102
+ };
103
+
104
+ mockFetchResponse({
105
+ ok: true,
106
+ json: jest.fn().mockResolvedValue(responseMock),
107
+ });
108
+
109
+ const result = await apiClient.remotes.upsert(testOrg, testProject, remotePayload);
110
+
111
+ expect(fetch).toHaveBeenCalledWith(
112
+ `${testDomain}/api/orgs/${testOrg}/projects/${testProject}/remotes`,
113
+ {
114
+ method: 'POST',
115
+ headers: {
116
+ 'Content-Type': 'application/json',
117
+ Authorization: `Bearer ${testToken}`,
118
+ },
119
+ body: JSON.stringify({
120
+ mountPath: remotePayload.mountPath,
121
+ mountBranchName: remotePayload.mountBranchName,
122
+ type: 'CICD',
123
+ autoMerge: true,
124
+ }),
125
+ }
126
+ );
127
+
128
+ expect(result).toEqual(responseMock);
129
+ });
130
+
131
+ it('should throw parsed error if response is not ok', async () => {
132
+ mockFetchResponse({
133
+ ok: false,
134
+ json: jest.fn().mockResolvedValue({
135
+ type: 'about:blank',
136
+ title: 'Not allowed to mount remote outside of project content path: /docs',
137
+ status: 403,
138
+ detail: 'Forbidden',
139
+ object: 'problem',
140
+ }),
141
+ });
142
+
143
+ await expect(apiClient.remotes.upsert(testOrg, testProject, remotePayload)).rejects.toThrow(
144
+ new Error(
145
+ 'Failed to upsert remote: Not allowed to mount remote outside of project content path: /docs'
146
+ )
147
+ );
148
+ });
149
+
150
+ it('should throw statusText error if response is not ok', async () => {
151
+ mockFetchResponse({
152
+ ok: false,
153
+ statusText: 'Not found',
154
+ json: jest.fn().mockResolvedValue({
155
+ unknownField: 'unknown-error',
156
+ }),
157
+ });
158
+
159
+ await expect(apiClient.remotes.upsert(testOrg, testProject, remotePayload)).rejects.toThrow(
160
+ new Error('Failed to upsert remote: Not found')
161
+ );
162
+ });
163
+ });
164
+
165
+ describe('push()', () => {
166
+ const testRemoteId = 'test-remote-id';
167
+ const pushPayload = {
168
+ remoteId: testRemoteId,
169
+ commit: {
170
+ message: 'test-message',
171
+ author: {
172
+ name: 'test-name',
173
+ email: 'test-email',
174
+ },
175
+ branchName: 'test-branch-name',
176
+ },
177
+ } as unknown as PushPayload;
178
+
179
+ const filesMock = [{ path: 'some-file.yaml', stream: Buffer.from('fefef') }];
180
+
181
+ const responseMock = {
182
+ branchName: 'rem/cicd/rem_01he7sr6ys2agb7w0g9t7978fn-main',
183
+ hasChanges: true,
184
+ files: [
185
+ {
186
+ type: 'file',
187
+ name: 'some-file.yaml',
188
+ path: 'docs/remotes/some-file.yaml',
189
+ lastModified: 1698925132394.2993,
190
+ mimeType: 'text/yaml',
191
+ },
192
+ ],
193
+ commitSha: 'bb23a2f8e012ac0b7b9961b57fb40d8686b21b43',
194
+ outdated: false,
195
+ };
196
+
197
+ let apiClient: ReuniteApiClient;
198
+
199
+ beforeEach(() => {
200
+ apiClient = new ReuniteApiClient(testDomain, testToken);
201
+ });
202
+
203
+ it('should push to remote', async () => {
204
+ let passedFormData = new FormData();
205
+
206
+ (fetch as jest.MockedFunction<typeof fetch>).mockImplementationOnce(
207
+ async (_: any, options: any): Promise<Response> => {
208
+ passedFormData = options.body as FormData;
209
+
210
+ return {
211
+ ok: true,
212
+ json: jest.fn().mockResolvedValue(responseMock),
213
+ } as unknown as Response;
214
+ }
215
+ );
216
+
217
+ const formData = new FormData();
218
+
219
+ formData.append('remoteId', testRemoteId);
220
+ formData.append('commit[message]', pushPayload.commit.message);
221
+ formData.append('commit[author][name]', pushPayload.commit.author.name);
222
+ formData.append('commit[author][email]', pushPayload.commit.author.email);
223
+ formData.append('commit[branchName]', pushPayload.commit.branchName);
224
+ formData.append('files[some-file.yaml]', filesMock[0].stream);
225
+
226
+ const result = await apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock);
227
+
228
+ expect(fetch).toHaveBeenCalledWith(
229
+ `${testDomain}/api/orgs/${testOrg}/projects/${testProject}/pushes`,
230
+ expect.objectContaining({
231
+ method: 'POST',
232
+ headers: {
233
+ Authorization: `Bearer ${testToken}`,
234
+ },
235
+ })
236
+ );
237
+
238
+ expect(
239
+ JSON.stringify(passedFormData).replace(new RegExp(passedFormData.getBoundary(), 'g'), '')
240
+ ).toEqual(JSON.stringify(formData).replace(new RegExp(formData.getBoundary(), 'g'), ''));
241
+ expect(result).toEqual(responseMock);
242
+ });
243
+
244
+ it('should throw parsed error if response is not ok', async () => {
245
+ mockFetchResponse({
246
+ ok: false,
247
+ json: jest.fn().mockResolvedValue({
248
+ type: 'about:blank',
249
+ title: 'Cannot push to remote',
250
+ status: 403,
251
+ detail: 'Forbidden',
252
+ object: 'problem',
253
+ }),
254
+ });
255
+
256
+ await expect(
257
+ apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock)
258
+ ).rejects.toThrow(new Error('Failed to push: Cannot push to remote'));
259
+ });
260
+
261
+ it('should throw statusText error if response is not ok', async () => {
262
+ mockFetchResponse({
263
+ ok: false,
264
+ statusText: 'Not found',
265
+ json: jest.fn().mockResolvedValue({
266
+ unknownField: 'unknown-error',
267
+ }),
268
+ });
269
+
270
+ await expect(
271
+ apiClient.remotes.push(testOrg, testProject, pushPayload, filesMock)
272
+ ).rejects.toThrow(new Error('Failed to push: Not found'));
273
+ });
274
+ });
275
+ });
@@ -0,0 +1,15 @@
1
+ import { getDomain } from '../domains';
2
+
3
+ describe('getDomain()', () => {
4
+ it('should return the domain from environment variable', () => {
5
+ process.env.REDOCLY_DOMAIN = 'test-domain';
6
+
7
+ expect(getDomain()).toBe('test-domain');
8
+ });
9
+
10
+ it('should return the default domain if no domain provided', () => {
11
+ process.env.REDOCLY_DOMAIN = '';
12
+
13
+ expect(getDomain()).toBe('https://app.cloud.redocly.com');
14
+ });
15
+ });