@redocly/cli
Version:
[@Redocly](https://redocly.com) CLI is your all-in-one OpenAPI utility. It builds, manages, improves, and quality-checks your OpenAPI descriptions, all of which comes in handy for various phases of the API Lifecycle. Create your own rulesets to make API g
497 lines (496 loc) • 17.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fs = require("fs");
const openapi_core_1 = require("@redocly/openapi-core");
const miscellaneous_1 = require("../../utils/miscellaneous");
const push_1 = require("../../commands/push");
const config_1 = require("../fixtures/config");
const colorette_1 = require("colorette");
const node_stream_1 = require("node:stream");
// Mock fs operations
jest.mock('fs', () => ({
...jest.requireActual('fs'),
createReadStream: jest.fn(() => {
const readable = new node_stream_1.Readable();
readable.push('test data');
readable.push(null);
return readable;
}),
statSync: jest.fn(() => ({ isDirectory: () => false, size: 10 })),
readFileSync: jest.fn(() => Buffer.from('test data')),
existsSync: jest.fn(() => false),
readdirSync: jest.fn(() => []),
}));
jest.mock('@redocly/openapi-core');
jest.mock('../../utils/miscellaneous');
// Mock fetch
const mockFetch = jest.fn(() => Promise.resolve({
ok: true,
status: 200,
json: () => Promise.resolve({}),
headers: new Headers(),
statusText: 'OK',
redirected: false,
type: 'default',
url: '',
clone: () => ({}),
body: null,
bodyUsed: false,
arrayBuffer: async () => new ArrayBuffer(0),
blob: async () => new Blob(),
formData: async () => new FormData(),
text: async () => '',
}));
const originalFetch = global.fetch;
openapi_core_1.getMergedConfig.mockImplementation((config) => config);
describe('push', () => {
const redoclyClient = require('@redocly/openapi-core').__redoclyClient;
beforeAll(() => {
global.fetch = mockFetch;
});
beforeEach(() => {
jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
});
afterAll(() => {
global.fetch = originalFetch;
});
it('pushes definition', async () => {
await (0, push_1.handlePush)({
argv: {
upsert: true,
api: 'spec.json',
destination: '@org/my-api@1.0.0',
branchName: 'test',
public: true,
'job-id': '123',
'batch-size': 2,
},
config: config_1.ConfigFixture,
version: 'cli-version',
});
expect(redoclyClient.registryApi.prepareFileUpload).toBeCalledTimes(1);
expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
expect(redoclyClient.registryApi.pushApi).toHaveBeenLastCalledWith({
branch: 'test',
filePaths: ['filePath'],
isUpsert: true,
isPublic: true,
name: 'my-api',
organizationId: 'org',
rootFilePath: 'filePath',
version: '1.0.0',
batchId: '123',
batchSize: 2,
});
});
it('fails if jobId value is an empty string', async () => {
await (0, push_1.handlePush)({
argv: {
upsert: true,
api: 'spec.json',
destination: '@org/my-api@1.0.0',
branchName: 'test',
public: true,
'job-id': ' ',
'batch-size': 2,
},
config: config_1.ConfigFixture,
version: 'cli-version',
});
expect(miscellaneous_1.exitWithError).toBeCalledTimes(1);
});
it('fails if batchSize value is less than 2', async () => {
await (0, push_1.handlePush)({
argv: {
upsert: true,
api: 'spec.json',
destination: '@org/my-api@1.0.0',
branchName: 'test',
public: true,
'job-id': '123',
'batch-size': 1,
},
config: config_1.ConfigFixture,
version: 'cli-version',
});
expect(miscellaneous_1.exitWithError).toBeCalledTimes(1);
});
it('push with --files', async () => {
const mockConfig = { ...config_1.ConfigFixture, files: ['./resouces/1.md', './resouces/2.md'] };
fs.statSync.mockImplementation(() => {
return { isDirectory: () => false, size: 10 };
});
await (0, push_1.handlePush)({
argv: {
upsert: true,
api: 'spec.json',
destination: '@org/my-api@1.0.0',
public: true,
files: ['./resouces/1.md', './resouces/2.md'],
},
config: mockConfig,
version: 'cli-version',
});
expect(redoclyClient.registryApi.pushApi).toHaveBeenLastCalledWith({
filePaths: ['filePath', 'filePath', 'filePath'],
isUpsert: true,
isPublic: true,
name: 'my-api',
organizationId: 'org',
rootFilePath: 'filePath',
version: '1.0.0',
});
expect(redoclyClient.registryApi.prepareFileUpload).toBeCalledTimes(3);
});
it('push should fail if organization not provided', async () => {
await (0, push_1.handlePush)({
argv: {
upsert: true,
api: 'spec.json',
destination: 'test@v1',
branchName: 'test',
public: true,
'job-id': '123',
'batch-size': 2,
},
config: config_1.ConfigFixture,
version: 'cli-version',
});
expect(miscellaneous_1.exitWithError).toBeCalledTimes(1);
expect(miscellaneous_1.exitWithError).toBeCalledWith(`No organization provided, please use --organization option or specify the 'organization' field in the config file.`);
});
it('push should work with organization in config', async () => {
const mockConfig = { ...config_1.ConfigFixture, organization: 'test_org' };
await (0, push_1.handlePush)({
argv: {
upsert: true,
api: 'spec.json',
destination: 'my-api@1.0.0',
branchName: 'test',
public: true,
'job-id': '123',
'batch-size': 2,
},
config: mockConfig,
version: 'cli-version',
});
expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
expect(redoclyClient.registryApi.pushApi).toHaveBeenLastCalledWith({
branch: 'test',
filePaths: ['filePath'],
isUpsert: true,
isPublic: true,
name: 'my-api',
organizationId: 'test_org',
rootFilePath: 'filePath',
version: '1.0.0',
batchId: '123',
batchSize: 2,
});
});
it('push should work if destination not provided but api in config is provided', async () => {
const mockConfig = {
...config_1.ConfigFixture,
organization: 'test_org',
apis: { 'my-api@1.0.0': { root: 'path' } },
};
await (0, push_1.handlePush)({
argv: {
upsert: true,
branchName: 'test',
public: true,
'job-id': '123',
'batch-size': 2,
},
config: mockConfig,
version: 'cli-version',
});
expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
});
it('push should fail if apis not provided', async () => {
const mockConfig = { organization: 'test_org', apis: {} };
await (0, push_1.handlePush)({
argv: {
upsert: true,
branchName: 'test',
public: true,
'job-id': '123',
'batch-size': 2,
},
config: mockConfig,
version: 'cli-version',
});
expect(miscellaneous_1.exitWithError).toBeCalledTimes(1);
expect(miscellaneous_1.exitWithError).toHaveBeenLastCalledWith('Api not found. Please make sure you have provided the correct data in the config file.');
});
it('push should fail if destination not provided', async () => {
const mockConfig = { organization: 'test_org', apis: {} };
await (0, push_1.handlePush)({
argv: {
upsert: true,
api: 'api.yaml',
branchName: 'test',
public: true,
'job-id': '123',
'batch-size': 2,
},
config: mockConfig,
version: 'cli-version',
});
expect(miscellaneous_1.exitWithError).toBeCalledTimes(1);
expect(miscellaneous_1.exitWithError).toHaveBeenLastCalledWith('No destination provided, please use --destination option to provide destination.');
});
it('push should fail if destination format is not valid', async () => {
const mockConfig = { organization: 'test_org', apis: {} };
await (0, push_1.handlePush)({
argv: {
upsert: true,
destination: 'name/v1',
branchName: 'test',
public: true,
'job-id': '123',
'batch-size': 2,
},
config: mockConfig,
version: 'cli-version',
});
expect(miscellaneous_1.exitWithError).toHaveBeenCalledWith(`Destination argument value is not valid, please use the right format: ${(0, colorette_1.yellow)('<api-name@api-version>')}.`);
});
it('push should work and encode name with spaces', async () => {
const encodeURIComponentSpy = jest.spyOn(global, 'encodeURIComponent');
const mockConfig = {
...config_1.ConfigFixture,
organization: 'test_org',
apis: { 'my test api@v1': { root: 'path' } },
};
await (0, push_1.handlePush)({
argv: {
upsert: true,
destination: 'my test api@v1',
branchName: 'test',
public: true,
'job-id': '123',
'batch-size': 2,
},
config: mockConfig,
version: 'cli-version',
});
expect(encodeURIComponentSpy).toHaveReturnedWith('my%20test%20api');
expect(redoclyClient.registryApi.pushApi).toBeCalledTimes(1);
});
});
describe('transformPush', () => {
it('should adapt the existing syntax', () => {
const cb = jest.fn();
(0, push_1.transformPush)(cb)({
argv: {
apis: ['openapi.yaml', '@testing_org/main@v1'],
},
config: {},
version: 'cli-version',
});
expect(cb).toBeCalledWith({
argv: {
api: 'openapi.yaml',
destination: '@testing_org/main@v1',
},
config: {},
version: 'cli-version',
});
});
it('should adapt the existing syntax (including branchName)', () => {
const cb = jest.fn();
(0, push_1.transformPush)(cb)({
argv: {
apis: ['openapi.yaml', '@testing_org/main@v1', 'other'],
},
config: {},
version: 'cli-version',
});
expect(cb).toBeCalledWith({
argv: {
api: 'openapi.yaml',
destination: '@testing_org/main@v1',
branchName: 'other',
},
config: {},
version: 'cli-version',
});
});
it('should use --branch option firstly', () => {
const cb = jest.fn();
(0, push_1.transformPush)(cb)({
argv: {
apis: ['openapi.yaml', '@testing_org/main@v1', 'other'],
branch: 'priority-branch',
},
config: {},
version: 'cli-version',
});
expect(cb).toBeCalledWith({
argv: {
api: 'openapi.yaml',
destination: '@testing_org/main@v1',
branchName: 'priority-branch',
},
config: {},
version: 'cli-version',
});
});
it('should work for a destination only', () => {
const cb = jest.fn();
(0, push_1.transformPush)(cb)({
argv: {
apis: ['main@v1'],
},
config: {},
version: 'cli-version',
});
expect(cb).toBeCalledWith({
argv: {
destination: 'main@v1',
},
config: {},
version: 'cli-version',
});
});
it('should work for a api only', () => {
const cb = jest.fn();
(0, push_1.transformPush)(cb)({
argv: {
apis: ['test.yaml'],
},
config: {},
version: 'cli-version',
});
expect(cb).toBeCalledWith({
argv: {
api: 'test.yaml',
},
config: {},
version: 'cli-version',
});
});
it('should use destination from option', () => {
const cb = jest.fn();
(0, push_1.transformPush)(cb)({
argv: {
apis: ['test.yaml', 'test@v1'],
destination: 'main@v1',
},
config: {},
version: 'cli-version',
});
expect(cb).toBeCalledWith({
argv: {
destination: 'main@v1',
api: 'test.yaml',
},
config: {},
version: 'cli-version',
});
});
it('should use --job-id option firstly', () => {
const cb = jest.fn();
(0, push_1.transformPush)(cb)({
argv: {
'batch-id': 'b-123',
'job-id': 'j-123',
apis: ['test'],
branch: 'test',
destination: 'main@v1',
},
config: {},
version: 'cli-version',
});
expect(cb).toBeCalledWith({
argv: {
'job-id': 'j-123',
api: 'test',
branchName: 'test',
destination: 'main@v1',
},
config: {},
version: 'cli-version',
});
});
it('should accept no arguments at all', () => {
const cb = jest.fn();
(0, push_1.transformPush)(cb)({ argv: {}, config: {}, version: 'cli-version' });
expect(cb).toBeCalledWith({ argv: {}, config: {}, version: 'cli-version' });
});
});
describe('getDestinationProps', () => {
it('should get valid destination props for the full destination syntax', () => {
expect((0, push_1.getDestinationProps)('@testing_org/main@v1', 'org-from-config')).toEqual({
organizationId: 'testing_org',
name: 'main',
version: 'v1',
});
});
it('should fallback the organizationId from a config for the short destination syntax', () => {
expect((0, push_1.getDestinationProps)('main@v1', 'org-from-config')).toEqual({
organizationId: 'org-from-config',
name: 'main',
version: 'v1',
});
});
it('should fallback the organizationId from a config if no destination provided', () => {
expect((0, push_1.getDestinationProps)(undefined, 'org-from-config')).toEqual({
organizationId: 'org-from-config',
});
});
it('should return empty organizationId if there is no one found', () => {
expect((0, push_1.getDestinationProps)('main@v1', undefined)).toEqual({
organizationId: undefined,
name: 'main',
version: 'v1',
});
});
it('should return organizationId from destination string', () => {
expect((0, push_1.getDestinationProps)('@test-org/main@main-v1', undefined)).toEqual({
organizationId: 'test-org',
name: 'main',
version: 'main-v1',
});
});
it('should return organizationId, version and empty name from destination string', () => {
expect((0, push_1.getDestinationProps)('@test_org/@main_v1', undefined)).toEqual({
organizationId: 'test_org',
name: '',
version: 'main_v1',
});
});
it('should validate organizationId with space and version with dot', () => {
expect((0, push_1.getDestinationProps)('@test org/simple_name@main.v1', undefined)).toEqual({
organizationId: 'test org',
name: 'simple_name',
version: 'main.v1',
});
});
it('should not work with "@" in destination name', () => {
expect((0, push_1.getDestinationProps)('@test org/simple@name@main.v1', undefined)).toEqual({
organizationId: undefined,
name: undefined,
version: undefined,
});
});
});
describe('getApiRoot', () => {
let config = {
apis: {
'main@v1': {
root: 'openapi.yaml',
},
main: {
root: 'latest.yaml',
},
},
};
it('should resolve the correct api for a valid name & version', () => {
expect((0, push_1.getApiRoot)({ name: 'main', version: 'v1', config })).toEqual('openapi.yaml');
});
it('should resolve the latest version of api if there is no matching version', () => {
expect((0, push_1.getApiRoot)({ name: 'main', version: 'latest', config })).toEqual('latest.yaml');
});
});