Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Comment on lines +42 to +43

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The false positive is unavoidable here — the test asserts that the exact string 'Authorization' is used as the header key, which is the correct and required HTTP header name. Removing or obfuscating it would reduce test fidelity. The talismanrc entry is scoped to this specific file with a checksum, so any future edits to the file will force a new checksum update and re-review. Risk is low and accepted.

version: ""
2 changes: 2 additions & 0 deletions packages/contentstack-bootstrap/messages/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"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\": the repository or branch \"cli-use\" was not found. Ensure both exist on GitHub.",
"CLI_BOOTSTRAP_GITHUB_SERVER_ERROR": "Failed to download \"%s\": GitHub returned HTTP %s. Please try again later.",
"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",
Expand Down
13 changes: 12 additions & 1 deletion packages/contentstack-bootstrap/src/bootstrap/github/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,24 @@ export default class GitHubClient {
}

const response = await HttpClient.create().options(options).get(url);

if (response.status < 200 || response.status >= 400) {
const message = response.status === 404
? messageHandler.parse('CLI_BOOTSTRAP_REPO_NOT_FOUND', `${this.repo.user}/${this.repo.name}`)
: messageHandler.parse('CLI_BOOTSTRAP_GITHUB_SERVER_ERROR', `${this.repo.user}/${this.repo.name}`, response.status);
throw new GithubError(message, response.status);
}

return response.data as Stream;
}

async extract(destination: string, stream: Stream): Promise<any> {
return new Promise((resolve, reject) => {
const unzip = zlib.createUnzip();
stream.on('error', reject);
unzip.on('error', reject);
stream
.pipe(zlib.createUnzip())
.pipe(unzip)
.pipe(
tar.extract({
cwd: destination,
Expand Down
10 changes: 4 additions & 6 deletions packages/contentstack-bootstrap/src/bootstrap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Comment thread
cs-raj marked this conversation as resolved.
Comment on lines 72 to 79
throw error;
} finally {
cliux.loader();
}

// seed plugin start
Expand Down
106 changes: 106 additions & 0 deletions packages/contentstack-bootstrap/test/github.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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);
});
Comment on lines +73 to +82

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');
}
});
});
});
Loading