Skip to content

Commit 1125b7e

Browse files
authored
Merge pull request #356 from contentstack/fix/DX-10257
fix(bootstrap): reject on non-2xx tarball response and handle zlib errors
2 parents 836f17b + b4539b1 commit 1125b7e

7 files changed

Lines changed: 1186 additions & 999 deletions

File tree

‎.talismanrc‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
fileignoreconfig:
2-
3-
- filename: pnpm-lock.yaml
4-
checksum: 98236e2588d5cffcbdda3464179d91e9540fd8ddb4d4fb5251cf0f7379876f42
52
- filename: packages/contentstack-bulk-operations/src/utils/backup-dir-asset-fetcher.ts
63
checksum: fbf1ef882bf4ea8f06a7a28b9248a07e01f5a5d2d4da98dbb9ee52bc16ce318a
74
- filename: packages/contentstack-bulk-operations/test/unit/utils/backup-dir-asset-fetcher.test.ts
85
checksum: b905524c952aff0f089fa5347e0db18479b75a438cd9ae77e2dfe1cb203d7e59
9-
- filename: packages/contentstack-import/src/import/modules/assets.ts
10-
checksum: 014f612c3e8db21c891da0c17a1a3b4cd340c026c58034791d86cd5f73aa9f90
6+
- filename: pnpm-lock.yaml
7+
checksum: 65d0adc160e75d17f5c8de732d554d352fefdc30751509b9ec4c8b2d250e3e2e
118
- filename: packages/contentstack-bulk-operations/test/unit/utils/revert-retry-handler.test.ts
129
checksum: 142e7b67a06bc13b6b4191b17f2ea8ae31a3bce20086ea35a92f3e3671eb603d
13-
- filename: packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-assets.ts
14-
checksum: 9392511b337c361b86c36eb2431d8fea9129a61f6b7d70dcb4a633fea339ca61
1510
- filename: packages/contentstack-bulk-operations/src/utils/batch-helper.ts
1611
checksum: ddcd8ef7c56d1122df88075883a2768a28813147e8fdef5b8e72a1ec118c045a
17-
version: ""
12+
- filename: packages/contentstack-import/src/import/modules/assets.ts
13+
checksum: 014f612c3e8db21c891da0c17a1a3b4cd340c026c58034791d86cd5f73aa9f90
14+
- filename: packages/contentstack-cli-tsgen/src/lib/helper.ts
15+
checksum: cc2f88294ca026c29ca44ee7f9994ebc64e56d9a7a015c64070aaf271f1c3ba2
16+
- filename: packages/contentstack-bulk-operations/src/commands/cm/stacks/bulk-assets.ts
17+
checksum: 9392511b337c361b86c36eb2431d8fea9129a61f6b7d70dcb4a633fea339ca61
18+
version: ""

‎packages/contentstack-bootstrap/messages/index.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"CLI_BOOTSTRAP_GITHUB_ACCESS_NOT_FOUND": "No Github access token found",
55
"CLI_BOOTSTRAP_START_CLONE_APP": "Cloning the selected app",
66
"CLI_BOOTSTRAP_REPO_NOT_FOUND": "Unable to find a repo for \"%s\"",
7+
"CLI_BOOTSTRAP_APP_UNAVAILABLE": "Unable to download \"%s\": the repository or branch \"cli-use\" was not found. Ensure both exist on GitHub.",
8+
"CLI_BOOTSTRAP_GITHUB_SERVER_ERROR": "Failed to download \"%s\": GitHub returned HTTP %s. Please try again later.",
79
"CLI_BOOTSTRAP_NO_API_KEY_FOUND": "No API key generated for the stack",
810
"CLI_BOOTSTRAP_STACK_CREATION_FAILED": "Unable to create stack for content \"%s\"",
911
"CLI_BOOTSTRAP_APP_SELECTION_ENQUIRY": "Select an App",

‎packages/contentstack-bootstrap/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"mocha": "10.8.2",
3838
"nyc": "^15.1.0",
3939
"oclif": "^4.23.27",
40+
"sinon": "^21.1.2",
4041
"tmp": "^0.2.7",
4142
"ts-node": "^8.10.2",
4243
"typescript": "^5.9.3"

‎packages/contentstack-bootstrap/src/bootstrap/github/client.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,24 @@ export default class GitHubClient {
7474
}
7575

7676
const response = await HttpClient.create().options(options).get(url);
77+
78+
if (response.status < 200 || response.status >= 300) {
79+
const message = response.status === 404
80+
? messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', `${this.repo.user}/${this.repo.name}`)
81+
: messageHandler.parse('CLI_BOOTSTRAP_GITHUB_SERVER_ERROR', `${this.repo.user}/${this.repo.name}`, response.status);
82+
throw new GithubError(message, response.status);
83+
}
84+
7785
return response.data as Stream;
7886
}
7987

8088
async extract(destination: string, stream: Stream): Promise<any> {
8189
return new Promise((resolve, reject) => {
90+
const unzip = zlib.createUnzip();
91+
stream.on('error', reject);
92+
unzip.on('error', reject);
8293
stream
83-
.pipe(zlib.createUnzip())
94+
.pipe(unzip)
8495
.pipe(
8596
tar.extract({
8697
cwd: destination,

‎packages/contentstack-bootstrap/src/bootstrap/index.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,13 @@ export default class Bootstrap {
7171

7272
try {
7373
await this.ghClient.getLatest(this.cloneDirectory);
74+
cliux.loader();
7475
} catch (error) {
75-
if (error instanceof GithubError) {
76-
if (error.status === 404) {
77-
cliux.error(messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', this.appConfig.source));
78-
}
76+
cliux.loader();
77+
if (error instanceof GithubError && error.status === 404) {
78+
throw new Error(messageHandler.parse('CLI_BOOTSTRAP_APP_UNAVAILABLE', this.appConfig.source));
7979
}
8080
throw error;
81-
} finally {
82-
cliux.loader();
8381
}
8482

8583
// seed plugin start

‎packages/contentstack-bootstrap/test/github.test.js‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
const { expect } = require('chai');
2+
const sinon = require('sinon');
3+
const { Readable } = require('stream');
4+
const { HttpClient } = require('@contentstack/cli-utilities');
25
const GitHubClient = require('../lib/bootstrap/github/client').default;
6+
const GithubError = require('../lib/bootstrap/github/github-error').default;
37

48
describe('Github Client', function () {
59
it('Parse github url', () => {
@@ -16,4 +20,124 @@ describe('Github Client', function () {
1620
'https://api.github.com/repos/contentstack/contentstack-nextjs-react-universal-demo/tarball/cli-use',
1721
);
1822
});
23+
24+
describe('streamRelease', function () {
25+
let sandbox;
26+
27+
beforeEach(() => {
28+
sandbox = sinon.createSandbox();
29+
});
30+
31+
afterEach(() => {
32+
sandbox.restore();
33+
});
34+
35+
it('should throw GithubError with status 404 when the branch does not exist', async () => {
36+
const notFoundStream = new Readable({ read() {} });
37+
notFoundStream.push(Buffer.from('404: Not Found'));
38+
notFoundStream.push(null);
39+
40+
const httpStub = { get: sandbox.stub().resolves({ status: 404, data: notFoundStream }), options: sandbox.stub().returnsThis() };
41+
sandbox.stub(HttpClient, 'create').returns(httpStub);
42+
43+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next'));
44+
45+
try {
46+
await client.streamRelease(client.gitTarBallUrl);
47+
throw new Error('Expected GithubError to be thrown');
48+
} catch (err) {
49+
expect(err).to.be.instanceOf(GithubError);
50+
expect(err.status).to.equal(404);
51+
}
52+
});
53+
54+
it('should throw GithubError with status 500 on server error', async () => {
55+
const errStream = new Readable({ read() {} });
56+
errStream.push(Buffer.from('Internal Server Error'));
57+
errStream.push(null);
58+
59+
const httpStub = { get: sandbox.stub().resolves({ status: 500, data: errStream }), options: sandbox.stub().returnsThis() };
60+
sandbox.stub(HttpClient, 'create').returns(httpStub);
61+
62+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next'));
63+
64+
try {
65+
await client.streamRelease(client.gitTarBallUrl);
66+
throw new Error('Expected GithubError to be thrown');
67+
} catch (err) {
68+
expect(err).to.be.instanceOf(GithubError);
69+
expect(err.status).to.equal(500);
70+
}
71+
});
72+
73+
it('should throw GithubError with status 302 on unexpected redirect', async () => {
74+
const redirectStream = new Readable({ read() {} });
75+
redirectStream.push(null);
76+
77+
const httpStub = { get: sandbox.stub().resolves({ status: 302, data: redirectStream }), options: sandbox.stub().returnsThis() };
78+
sandbox.stub(HttpClient, 'create').returns(httpStub);
79+
80+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next'));
81+
82+
try {
83+
await client.streamRelease(client.gitTarBallUrl);
84+
throw new Error('Expected GithubError to be thrown');
85+
} catch (err) {
86+
expect(err).to.be.instanceOf(GithubError);
87+
expect(err.status).to.equal(302);
88+
}
89+
});
90+
91+
it('should return the response stream when status is 200', async () => {
92+
const mockStream = new Readable({ read() {} });
93+
const httpStub = { get: sandbox.stub().resolves({ status: 200, data: mockStream }), options: sandbox.stub().returnsThis() };
94+
sandbox.stub(HttpClient, 'create').returns(httpStub);
95+
96+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next'));
97+
const result = await client.streamRelease(client.gitTarBallUrl);
98+
99+
expect(result).to.equal(mockStream);
100+
});
101+
102+
it('should pass Authorization header for private repos', async () => {
103+
const mockStream = new Readable({ read() {} });
104+
const httpStub = { get: sandbox.stub().resolves({ status: 200, data: mockStream }), options: sandbox.stub().returnsThis() };
105+
sandbox.stub(HttpClient, 'create').returns(httpStub);
106+
107+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/private-repo'), true, 'my-token');
108+
await client.streamRelease(client.gitTarBallUrl);
109+
110+
const callOptions = httpStub.options.firstCall.args[0];
111+
expect(callOptions.headers).to.deep.equal({ Authorization: 'token my-token' });
112+
});
113+
114+
it('should throw GithubError immediately for private repos with no access token', async () => {
115+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/private-repo'), true, undefined);
116+
117+
try {
118+
await client.streamRelease(client.gitTarBallUrl);
119+
throw new Error('Expected GithubError to be thrown');
120+
} catch (err) {
121+
expect(err).to.be.instanceOf(GithubError);
122+
expect(err.status).to.equal(1);
123+
}
124+
});
125+
});
126+
127+
describe('extract', function () {
128+
it('should reject (not crash the process) when the stream contains invalid gzip data', async () => {
129+
const client = new GitHubClient(GitHubClient.parsePath('contentstack/kickstart-next'));
130+
131+
const badStream = new Readable({ read() {} });
132+
badStream.push(Buffer.from('404: Not Found'));
133+
badStream.push(null);
134+
135+
try {
136+
await client.extract('/tmp', badStream);
137+
throw new Error('Expected extraction error to be thrown');
138+
} catch (err) {
139+
expect(err.code).to.equal('Z_DATA_ERROR');
140+
}
141+
});
142+
});
19143
});

0 commit comments

Comments
 (0)