diff --git a/.talismanrc b/.talismanrc index 7efa0e61f..557172718 100644 --- a/.talismanrc +++ b/.talismanrc @@ -4,7 +4,7 @@ fileignoreconfig: - filename: packages/contentstack-audit/test/unit/mock/am-contents/environments/environments.json checksum: ffaaee9269a6e833cd3dbe337ddc5c060cce948519873c7a2777754519a31b52 - filename: pnpm-lock.yaml - checksum: 649aae748e3ae8c157439dc6c7c9ea402cc19b4dab7ca64b7e4d839d1375df53 + checksum: fda8964d238e0d30577ce8f5bf879a2102c69a7f9e83067ab1627b9b1db9d066 - filename: packages/contentstack-audit/src/types/content-types.ts checksum: d16a65415c3184f15a807d58e5858310aeb5633794fc9075e36f89c94da636c3 - filename: packages/contentstack-audit/src/audit-base-command.ts @@ -39,4 +39,6 @@ fileignoreconfig: checksum: a64a4d396eddd936a63b799eff58c5c6660b5dcaa3a310fd8b09a027932f1789 - filename: packages/contentstack-migration/README.md checksum: e96006c1a948f766c88ae972b29582fa58eaf8184606bf011eebddc5a06cd7b6 +- filename: packages/contentstack-bootstrap/test/github.test.js + checksum: b7badfcd3bbad0cb876364542bba26cdfd854f1b138be2896b5f84c219767040 version: "" diff --git a/packages/contentstack-bootstrap/messages/index.json b/packages/contentstack-bootstrap/messages/index.json index 3a56ee430..280e2246f 100644 --- a/packages/contentstack-bootstrap/messages/index.json +++ b/packages/contentstack-bootstrap/messages/index.json @@ -4,6 +4,7 @@ "CLI_BOOTSTRAP_GITHUB_ACCESS_NOT_FOUND": "No Github access token found", "CLI_BOOTSTRAP_START_CLONE_APP": "Cloning the selected app", "CLI_BOOTSTRAP_REPO_NOT_FOUND": "Unable to find a repo for \"%s\"", + "CLI_BOOTSTRAP_APP_UNAVAILABLE": "Unable to download \"%s\": branch \"cli-use\" not found. Ensure the branch exists on the GitHub repository.", "CLI_BOOTSTRAP_NO_API_KEY_FOUND": "No API key generated for the stack", "CLI_BOOTSTRAP_STACK_CREATION_FAILED": "Unable to create stack for content \"%s\"", "CLI_BOOTSTRAP_APP_SELECTION_ENQUIRY": "Select an App", diff --git a/packages/contentstack-bootstrap/src/bootstrap/github/client.ts b/packages/contentstack-bootstrap/src/bootstrap/github/client.ts index bf300bc04..fa91d3735 100644 --- a/packages/contentstack-bootstrap/src/bootstrap/github/client.ts +++ b/packages/contentstack-bootstrap/src/bootstrap/github/client.ts @@ -74,13 +74,23 @@ export default class GitHubClient { } const response = await HttpClient.create().options(options).get(url); + + if (response.status >= 400) { + throw new GithubError( + messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', `${this.repo.user}/${this.repo.name}`), + response.status, + ); + } + return response.data as Stream; } async extract(destination: string, stream: Stream): Promise { return new Promise((resolve, reject) => { + const unzip = zlib.createUnzip(); + unzip.on('error', reject); stream - .pipe(zlib.createUnzip()) + .pipe(unzip) .pipe( tar.extract({ cwd: destination, diff --git a/packages/contentstack-bootstrap/src/bootstrap/index.ts b/packages/contentstack-bootstrap/src/bootstrap/index.ts index c68bcdcaa..90f056bbd 100644 --- a/packages/contentstack-bootstrap/src/bootstrap/index.ts +++ b/packages/contentstack-bootstrap/src/bootstrap/index.ts @@ -71,15 +71,13 @@ export default class Bootstrap { try { await this.ghClient.getLatest(this.cloneDirectory); + cliux.loader(); } catch (error) { - if (error instanceof GithubError) { - if (error.status === 404) { - cliux.error(messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', this.appConfig.source)); - } + cliux.loader(); + if (error instanceof GithubError && error.status === 404) { + throw new Error(messageHandler.parse('CLI_BOOTSTRAP_APP_UNAVAILABLE', this.appConfig.source)); } throw error; - } finally { - cliux.loader(); } // seed plugin start diff --git a/packages/contentstack-bootstrap/test/github.test.js b/packages/contentstack-bootstrap/test/github.test.js index 8af88949a..d004921a5 100644 --- a/packages/contentstack-bootstrap/test/github.test.js +++ b/packages/contentstack-bootstrap/test/github.test.js @@ -1,5 +1,9 @@ const { expect } = require('chai'); +const sinon = require('sinon'); +const { Readable } = require('stream'); +const { HttpClient } = require('@contentstack/cli-utilities'); const GitHubClient = require('../lib/bootstrap/github/client').default; +const GithubError = require('../lib/bootstrap/github/github-error').default; describe('Github Client', function () { it('Parse github url', () => { @@ -16,4 +20,106 @@ describe('Github Client', function () { 'https://api.github.com/repos/contentstack/contentstack-nextjs-react-universal-demo/tarball/cli-use', ); }); + + describe('streamRelease', function () { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should throw GithubError with status 404 when the branch does not exist', async () => { + const notFoundStream = new Readable({ read() {} }); + notFoundStream.push(Buffer.from('404: Not Found')); + notFoundStream.push(null); + + const httpStub = { get: sandbox.stub().resolves({ status: 404, data: notFoundStream }), options: sandbox.stub().returnsThis() }; + sandbox.stub(HttpClient, 'create').returns(httpStub); + + const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next')); + + try { + await client.streamRelease(client.gitTarBallUrl); + throw new Error('Expected GithubError to be thrown'); + } catch (err) { + expect(err).to.be.instanceOf(GithubError); + expect(err.status).to.equal(404); + } + }); + + it('should throw GithubError with status 500 on server error', async () => { + const errStream = new Readable({ read() {} }); + errStream.push(Buffer.from('Internal Server Error')); + errStream.push(null); + + const httpStub = { get: sandbox.stub().resolves({ status: 500, data: errStream }), options: sandbox.stub().returnsThis() }; + sandbox.stub(HttpClient, 'create').returns(httpStub); + + const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next')); + + try { + await client.streamRelease(client.gitTarBallUrl); + throw new Error('Expected GithubError to be thrown'); + } catch (err) { + expect(err).to.be.instanceOf(GithubError); + expect(err.status).to.equal(500); + } + }); + + it('should return the response stream when status is 200', async () => { + const mockStream = new Readable({ read() {} }); + const httpStub = { get: sandbox.stub().resolves({ status: 200, data: mockStream }), options: sandbox.stub().returnsThis() }; + sandbox.stub(HttpClient, 'create').returns(httpStub); + + const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next')); + const result = await client.streamRelease(client.gitTarBallUrl); + + expect(result).to.equal(mockStream); + }); + + it('should pass Authorization header for private repos', async () => { + const mockStream = new Readable({ read() {} }); + const httpStub = { get: sandbox.stub().resolves({ status: 200, data: mockStream }), options: sandbox.stub().returnsThis() }; + sandbox.stub(HttpClient, 'create').returns(httpStub); + + const client = new GitHubClient(GitHubClient.parsePath('contentstack/private-repo'), true, 'my-token'); + await client.streamRelease(client.gitTarBallUrl); + + const callOptions = httpStub.options.firstCall.args[0]; + expect(callOptions.headers).to.deep.equal({ Authorization: 'token my-token' }); + }); + + it('should throw GithubError immediately for private repos with no access token', async () => { + const client = new GitHubClient(GitHubClient.parsePath('contentstack/private-repo'), true, undefined); + + try { + await client.streamRelease(client.gitTarBallUrl); + throw new Error('Expected GithubError to be thrown'); + } catch (err) { + expect(err).to.be.instanceOf(GithubError); + expect(err.status).to.equal(1); + } + }); + }); + + describe('extract', function () { + it('should reject (not crash the process) when the stream contains invalid gzip data', async () => { + const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next')); + + const badStream = new Readable({ read() {} }); + badStream.push(Buffer.from('404: Not Found')); + badStream.push(null); + + try { + await client.extract('/tmp', badStream); + throw new Error('Expected extraction error to be thrown'); + } catch (err) { + expect(err.code).to.equal('Z_DATA_ERROR'); + } + }); + }); });