UNPKG

@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

654 lines (590 loc) 18 kB
import { handlePushStatus } from '../push-status'; import { PushResponse } from '../../api/types'; const remotes = { getPush: jest.fn(), getRemotesList: jest.fn(), }; jest.mock('colorette', () => ({ green: (str: string) => str, yellow: (str: string) => str, red: (str: string) => str, gray: (str: string) => str, magenta: (str: string) => str, cyan: (str: string) => str, })); jest.mock('../../api', () => ({ ...jest.requireActual('../../api'), ReuniteApi: jest.fn().mockImplementation(function (this: any, ...args) { this.remotes = remotes; this.reportSunsetWarnings = jest.fn(); }), })); jest.mock('@redocly/openapi-core', () => ({ pause: jest.requireActual('@redocly/openapi-core').pause, })); describe('handlePushStatus()', () => { const mockConfig = { apis: {} } as any; const commitStub: PushResponse['commit'] = { message: 'test-commit-message', branchName: 'test-branch-name', sha: null, url: null, createdAt: null, namespaceId: null, repositoryId: null, author: { name: 'test-author-name', email: 'test-author-email', image: null, }, statuses: [], }; const pushResponseStub: PushResponse = { id: 'test-push-id', remoteId: 'test-remote-id', replace: false, scoutJobId: null, uploadedFiles: [], commit: commitStub, remote: { commits: [] }, isOutdated: false, isMainBranch: false, hasChanges: true, status: { preview: { scorecard: [], deploy: { url: 'https://preview-test-url', status: 'success', }, }, production: { scorecard: [], deploy: { url: 'https://production-test-url', status: 'success', }, }, }, }; beforeEach(() => { jest.spyOn(process.stderr, 'write').mockImplementation(() => true); jest.spyOn(process.stdout, 'write').mockImplementation(() => true); }); afterEach(() => { jest.clearAllMocks(); }); it('should throw error if organization not provided', async () => { await expect( handlePushStatus({ argv: { domain: 'test-domain', organization: '', project: 'test-project', pushId: 'test-push-id', }, config: mockConfig, version: 'cli-version', }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"No organization provided, please use --organization option or specify the 'organization' field in the config file."` ); expect(process.stderr.write).toHaveBeenCalledWith( `No organization provided, please use --organization option or specify the 'organization' field in the config file.` + '\n\n' ); }); it('should print success push status for preview-build', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValueOnce(pushResponseStub); await handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', }, config: mockConfig, version: 'cli-version', }); expect(process.stdout.write).toHaveBeenCalledTimes(1); expect(process.stdout.write).toHaveBeenCalledWith( '🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n' ); }); it('should print success push status for preview and production builds', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true }); await handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', }, config: mockConfig, version: 'cli-version', }); expect(process.stdout.write).toHaveBeenCalledTimes(2); expect(process.stdout.write).toHaveBeenCalledWith( '🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n' ); expect(process.stdout.write).toHaveBeenCalledWith( '🚀 Production deploy success.\nProduction URL: https://production-test-url\n' ); }); it('should print failed push status for preview build', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValue({ isOutdated: false, hasChanges: true, status: { preview: { deploy: { status: 'failed', url: 'https://preview-test-url' }, scorecard: [] }, }, }); await expect( handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', }, config: mockConfig, version: 'cli-version', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` "❌ Preview deploy fail. Preview URL: https://preview-test-url" `); expect(process.stderr.write).toHaveBeenCalledWith( '❌ Preview deploy fail.\nPreview URL: https://preview-test-url' + '\n\n' ); }); it('should print success push status for preview build and print scorecards', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValue({ isOutdated: false, hasChanges: true, status: { preview: { deploy: { status: 'success', url: 'https://preview-test-url' }, scorecard: [ { name: 'test-name', status: 'success', description: 'test-description', url: 'test-url', }, ], }, }, }); await handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', }, config: mockConfig, version: 'cli-version', }); expect(process.stdout.write).toHaveBeenCalledTimes(4); expect(process.stdout.write).toHaveBeenCalledWith( '🚀 Preview deploy success.\nPreview URL: https://preview-test-url\n' ); expect(process.stdout.write).toHaveBeenCalledWith('\nScorecard:'); expect(process.stdout.write).toHaveBeenCalledWith( '\n Name: test-name\n Status: success\n URL: test-url\n Description: test-description\n' ); expect(process.stdout.write).toHaveBeenCalledWith('\n'); }); it('should print message if there is no changes', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValueOnce({ isOutdated: false, hasChanges: false, status: { preview: { deploy: { status: 'skipped', url: 'https://preview-test-url' }, scorecard: [] }, production: { deploy: { status: 'skipped', url: null }, scorecard: [], }, }, }); await handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', wait: true, }, config: mockConfig, version: 'cli-version', }); expect(process.stderr.write).toHaveBeenCalledWith( 'Files not added to your project. Reason: no changes.\n' ); }); describe('return value', () => { it('should return preview deployment info', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: false }); const result = await handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', }, config: mockConfig, version: 'cli-version', }); expect(result).toEqual({ preview: { deploy: { status: 'success', url: 'https://preview-test-url', }, scorecard: [], }, production: null, commit: commitStub, }); }); it('should return preview and production deployment info', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValue({ ...pushResponseStub, isMainBranch: true }); const result = await handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', }, config: mockConfig, version: 'cli-version', }); expect(result).toEqual({ preview: { deploy: { status: 'success', url: 'https://preview-test-url', }, scorecard: [], }, production: { deploy: { status: 'success', url: 'https://production-test-url', }, scorecard: [], }, commit: commitStub, }); }); }); describe('"wait" option', () => { it('should wait for preview "success" deployment status', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, status: { preview: { deploy: { status: 'pending', url: 'https://preview-test-url' }, scorecard: [], }, }, }); remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, status: { preview: { deploy: { status: 'running', url: 'https://preview-test-url' }, scorecard: [], }, }, }); remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, status: { preview: { deploy: { status: 'success', url: 'https://preview-test-url' }, scorecard: [], }, }, }); const result = await handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', 'retry-interval': 0.5, // 500 ms wait: true, }, config: mockConfig, version: 'cli-version', }); expect(result).toEqual({ preview: { deploy: { status: 'success', url: 'https://preview-test-url', }, scorecard: [], }, production: null, commit: commitStub, }); }); it('should wait for production "success" status after preview "success" status', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, isMainBranch: true, status: { preview: { deploy: { status: 'success', url: 'https://preview-test-url' }, scorecard: [], }, production: { deploy: { status: 'pending', url: 'https://production-test-url' }, scorecard: [], }, }, }); remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, isMainBranch: true, status: { preview: { deploy: { status: 'success', url: 'https://preview-test-url' }, scorecard: [], }, production: { deploy: { status: 'running', url: 'https://production-test-url' }, scorecard: [], }, }, }); remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, isMainBranch: true, status: { preview: { deploy: { status: 'success', url: 'https://preview-test-url' }, scorecard: [], }, production: { deploy: { status: 'success', url: 'https://production-test-url' }, scorecard: [], }, }, }); const result = await handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', 'retry-interval': 0.5, // 500 ms wait: true, }, config: mockConfig, version: 'cli-version', }); expect(result).toEqual({ preview: { deploy: { status: 'success', url: 'https://preview-test-url' }, scorecard: [], }, production: { deploy: { status: 'success', url: 'https://production-test-url' }, scorecard: [], }, commit: commitStub, }); }); }); describe('"continue-on-deploy-failures" option', () => { it('should throw error if option value is false', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, status: { preview: { deploy: { status: 'failed', url: 'https://preview-test-url' }, scorecard: [], }, }, }); await expect( handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', 'continue-on-deploy-failures': false, }, config: mockConfig, version: 'cli-version', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` "❌ Preview deploy fail. Preview URL: https://preview-test-url" `); }); it('should not throw error if option value is true', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, status: { preview: { deploy: { status: 'failed', url: 'https://preview-test-url' }, scorecard: [], }, }, }); await expect( handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', 'continue-on-deploy-failures': true, }, config: mockConfig, version: 'cli-version', }) ).resolves.toStrictEqual({ preview: { deploy: { status: 'failed', url: 'https://preview-test-url' }, scorecard: [], }, production: null, commit: commitStub, }); }); }); describe('"onRetry" callback', () => { it('should be called when command retries request to API in wait mode for preview deploy', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, status: { preview: { deploy: { status: 'pending', url: 'https://preview-test-url' }, scorecard: [], }, }, }); remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, status: { preview: { deploy: { status: 'running', url: 'https://preview-test-url' }, scorecard: [], }, }, }); remotes.getPush.mockResolvedValueOnce({ ...pushResponseStub, status: { preview: { deploy: { status: 'success', url: 'https://preview-test-url' }, scorecard: [], }, }, }); const onRetrySpy = jest.fn(); const result = await handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', wait: true, 'retry-interval': 0.5, // 500 ms onRetry: onRetrySpy, }, config: mockConfig, version: 'cli-version', }); expect(onRetrySpy).toBeCalledTimes(2); // first retry expect(onRetrySpy).toHaveBeenNthCalledWith(1, { preview: { deploy: { status: 'pending', url: 'https://preview-test-url', }, scorecard: [], }, production: null, commit: commitStub, }); // second retry expect(onRetrySpy).toHaveBeenNthCalledWith(2, { preview: { deploy: { status: 'running', url: 'https://preview-test-url', }, scorecard: [], }, production: null, commit: commitStub, }); // final result expect(result).toEqual({ preview: { deploy: { status: 'success', url: 'https://preview-test-url', }, scorecard: [], }, production: null, commit: commitStub, }); }); }); describe('"max-execution-time" option', () => { it('should throw error in case "max-execution-time" was exceeded', async () => { process.env.REDOCLY_AUTHORIZATION = 'test-api-key'; // Stuck deployment simulation remotes.getPush.mockResolvedValue({ ...pushResponseStub, status: { preview: { deploy: { status: 'pending', url: 'https://preview-test-url' }, scorecard: [], }, }, }); await expect( handlePushStatus({ argv: { domain: 'test-domain', organization: 'test-org', project: 'test-project', pushId: 'test-push-id', 'retry-interval': 2, // seconds 'max-execution-time': 1, // seconds wait: true, }, config: mockConfig, version: 'cli-version', }) ).rejects.toThrowErrorMatchingInlineSnapshot(` "✗ Failed to get push status. Reason: Timeout exceeded. " `); }); }); });