Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/sca-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
jobs:
security-sca:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@master
Comment on lines +8 to 12
- name: Run Snyk to check for vulnerabilities
Expand Down
6 changes: 5 additions & 1 deletion .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ fileignoreconfig:
- filename: test/unit/commands/rollback.test.ts
checksum: d1f931f2d9a397131409399ad6463653e28b5a2224e870b641d9ba57c4418f18
- filename: package-lock.json
checksum: ffb77989c6fe554cdd79ebe0cbd07c0ce4329dd65b3e9a9089db4abb7b9be2b3
checksum: c94ce0fbbbedcbad4dceef8168971831ed82aad85824db579f2497dcd49aecab
- filename: src/adapters/github.test.ts
checksum: b3d3d3a3c14adde103152d2ac4e1c4b7121a5f7533a87c3cc600f6d25fb59d1b
- filename: src/adapters/file-upload.test.ts
checksum: 962fd8b45578914926d20857ddc3d1bc2fa19e8b192c6dfdc03d214ae1898b59
Comment on lines 11 to +16
version: "1.0"
1,893 changes: 656 additions & 1,237 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/cli-launch",
"version": "1.11.1",
"version": "1.11.2",
"description": "Launch related operations",
"author": "Contentstack CLI",
"bin": {
Expand All @@ -21,8 +21,8 @@
],
"dependencies": {
"@apollo/client": "^3.14.0",
"@contentstack/cli-command": "^1.8.2",
"@contentstack/cli-utilities": "^1.18.3",
"@contentstack/cli-command": "^1.8.4",
"@contentstack/cli-utilities": "^1.18.5",
"@oclif/core": "^4.2.7",
"@oclif/plugin-help": "^6.2.25",
"@rollup/plugin-commonjs": "^28.0.2",
Expand All @@ -31,7 +31,7 @@
"@rollup/plugin-typescript": "^12.1.2",
"@types/express": "^4.17.21",
"@types/express-serve-static-core": "^4.17.34",
"adm-zip": "^0.5.16",
"adm-zip": "^0.5.18",
"chalk": "^4.1.2",
"cross-fetch": "^4.1.0",
"dotenv": "^16.4.7",
Expand Down
67 changes: 67 additions & 0 deletions src/adapters/base-class.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,4 +685,71 @@ describe('BaseClass', () => {
expect(exitMock).toHaveBeenCalledWith(1);
});
});

describe('detectFramework', () => {
it('should scope the framework query to the selected GitHub connection namespace', async () => {
const apolloClient = {
query: jest.fn().mockResolvedValueOnce({ data: { framework: { framework: 'GATSBY' } } }),
} as any;
baseClass = new BaseClass({
log: logMock,
exit: exitMock,
apolloClient,
config: {
provider: 'GitHub',
listOfFrameWorks: [],
repository: { fullName: 'test-user/eleventy-sample', defaultBranch: 'main' },
userConnection: { provider: 'GitHub', namespace: 'org-account' },
},
} as any);
(ux.inquire as jest.Mock).mockResolvedValueOnce('GATSBY');

await baseClass.detectFramework();

expect(apolloClient.query).toHaveBeenCalledWith({
query: expect.anything(),
variables: {
query: {
provider: 'GitHub',
repoName: 'test-user/eleventy-sample',
branchName: 'main',
namespace: 'org-account',
},
},
});
});
});

describe('selectBranch', () => {
it('should scope the branches query to the selected GitHub connection namespace', async () => {
const apolloClient = {
query: jest.fn().mockResolvedValueOnce({
data: { branches: { edges: [], pageData: { page: 1 }, pageInfo: { hasNextPage: false } } },
}),
} as any;
baseClass = new BaseClass({
log: logMock,
exit: exitMock,
apolloClient,
config: {
provider: 'GitHub',
flags: {},
repository: { fullName: 'test-user/eleventy-sample' },
userConnection: { provider: 'GitHub', namespace: 'org-account' },
},
} as any);
(ux.inquire as jest.Mock).mockResolvedValueOnce('main');

await baseClass.selectBranch();

expect(apolloClient.query).toHaveBeenCalledWith({
query: expect.anything(),
variables: {
page: 1,
first: 100,
query: { provider: 'GitHub', repoName: 'test-user/eleventy-sample', namespace: 'org-account' },
},
});
});
});
});
6 changes: 5 additions & 1 deletion src/adapters/base-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export default class BaseClass {
*/
async detectFramework(): Promise<void> {
const { fullName, defaultBranch } = this.config.repository || {};
const namespace = this.config.userConnection?.namespace;
const query = this.config.provider === 'FileUpload' ? fileFrameworkQuery : frameworkQuery;
const variables =
this.config.provider === 'FileUpload'
Expand All @@ -171,6 +172,7 @@ export default class BaseClass {
provider: this.config.provider,
repoName: fullName,
branchName: defaultBranch,
...(namespace ? { namespace } : {}),
},
};
this.config.framework = (await this.apolloClient
Expand Down Expand Up @@ -414,7 +416,7 @@ export default class BaseClass {
if (includes(this.config.supportedAdapters, this.config.provider)) {
const baseUrl = this.config.host.startsWith('http') ? this.config.host : `https://${this.config.host}`;

const gitHubConnectUrl = `${baseUrl.replace('api', 'app').replace('io', 'com')}/#!/launch`;
const gitHubConnectUrl = `${baseUrl.replace('api', 'app').replace('io', 'com')}/#!/launch/settings/connected-accounts`;
this.log(`You can connect your ${this.config.provider} account to the UI using the following URL:`, 'info');
this.log(gitHubConnectUrl, { color: 'green' });
open(gitHubConnectUrl);
Expand Down Expand Up @@ -464,12 +466,14 @@ export default class BaseClass {
* @memberof BaseClass
*/
async selectBranch(): Promise<void> {
const namespace = this.config.userConnection?.namespace;
const variables = {
page: 1,
first: 100,
query: {
provider: this.config.provider,
repoName: this.config.repository?.fullName,
...(namespace ? { namespace } : {}),
},
};

Expand Down
154 changes: 154 additions & 0 deletions src/adapters/file-upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,160 @@ describe('FileUpload Adapter', () => {
uploadFileMock.mockRestore();
handleEnvImportFlowMock.mockRestore();
});

it.each([
['a "yes" answer maps to enabled', true, true],
['a "no" answer maps to disabled', false, false],
])(
'should prompt Enable Contentstack Authentication (default enabled) when disable-cs-auth is not provided — %s',
async (_label, answer, expected) => {
(cliux.inquire as jest.Mock).mockResolvedValueOnce('test-project');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('Default');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('npm run build');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('./public');
(cliux.inquire as jest.Mock).mockResolvedValueOnce(answer);

const createSignedUploadUrlMock = jest
.spyOn(FileUpload.prototype as any, 'createSignedUploadUrl')
.mockResolvedValue({ uploadUid: 'test-upload-uid' });
const archiveMock = jest
.spyOn(FileUpload.prototype as any, 'archive')
.mockResolvedValue({ zipName: 'test.zip', zipPath: '/path/to/test.zip', projectName: 'test-project' });
const uploadFileMock = jest
.spyOn(FileUpload.prototype as any, 'uploadFile')
.mockResolvedValue(undefined);

const fileUploadInstance = new FileUpload({
config: {
flags: {
'response-mode': 'buffered',
},
framework: 'GATSBY',
supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX', 'NUXT'],
outputDirectories: { GATSBY: './public' },
},
log: logMock,
exit: exitMock,
} as any);

const handleEnvImportFlowMock = jest
.spyOn(fileUploadInstance, 'handleEnvImportFlow' as any)
.mockResolvedValue(undefined);

await fileUploadInstance.prepareAndUploadNewProjectFile();

expect(cliux.inquire).toHaveBeenCalledWith(
expect.objectContaining({
type: 'confirm',
name: 'contentstackAuth',
default: true,
}),
);
expect(fileUploadInstance.config.isContentstackAuthenticationEnabled).toBe(expected);

createSignedUploadUrlMock.mockRestore();
archiveMock.mockRestore();
uploadFileMock.mockRestore();
handleEnvImportFlowMock.mockRestore();
},
);


it('should disable Contentstack Authentication without prompt when --disable-cs-auth is passed', async () => {
(cliux.inquire as jest.Mock).mockResolvedValueOnce('test-project');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('Default');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('npm run build');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('./public');

const createSignedUploadUrlMock = jest
.spyOn(FileUpload.prototype as any, 'createSignedUploadUrl')
.mockResolvedValue({ uploadUid: 'test-upload-uid' });
const archiveMock = jest
.spyOn(FileUpload.prototype as any, 'archive')
.mockResolvedValue({ zipName: 'test.zip', zipPath: '/path/to/test.zip', projectName: 'test-project' });
const uploadFileMock = jest
.spyOn(FileUpload.prototype as any, 'uploadFile')
.mockResolvedValue(undefined);

const fileUploadInstance = new FileUpload({
config: {
flags: {
'response-mode': 'buffered',
'disable-cs-auth': true,
},
framework: 'GATSBY',
supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX', 'NUXT'],
outputDirectories: { GATSBY: './public' },
},
log: logMock,
exit: exitMock,
} as any);

const handleEnvImportFlowMock = jest
.spyOn(fileUploadInstance, 'handleEnvImportFlow' as any)
.mockResolvedValue(undefined);

await fileUploadInstance.prepareAndUploadNewProjectFile();

const contentstackAuthCalls = (cliux.inquire as jest.Mock).mock.calls.filter(
(call) => call[0]?.name === 'contentstackAuth',
);
expect(contentstackAuthCalls.length).toBe(0);
expect(fileUploadInstance.config.isContentstackAuthenticationEnabled).toBe(false);

createSignedUploadUrlMock.mockRestore();
archiveMock.mockRestore();
uploadFileMock.mockRestore();
handleEnvImportFlowMock.mockRestore();
});

it('should enable Contentstack Authentication without prompt when --enable-cs-auth is passed', async () => {
(cliux.inquire as jest.Mock).mockResolvedValueOnce('test-project');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('Default');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('npm run build');
(cliux.inquire as jest.Mock).mockResolvedValueOnce('./public');

const createSignedUploadUrlMock = jest
.spyOn(FileUpload.prototype as any, 'createSignedUploadUrl')
.mockResolvedValue({ uploadUid: 'test-upload-uid' });
const archiveMock = jest
.spyOn(FileUpload.prototype as any, 'archive')
.mockResolvedValue({ zipName: 'test.zip', zipPath: '/path/to/test.zip', projectName: 'test-project' });
const uploadFileMock = jest
.spyOn(FileUpload.prototype as any, 'uploadFile')
.mockResolvedValue(undefined);

const fileUploadInstance = new FileUpload({
config: {
flags: {
'response-mode': 'buffered',
'enable-cs-auth': true,
},
framework: 'GATSBY',
supportedFrameworksForServerCommands: ['ANGULAR', 'OTHER', 'REMIX', 'NUXT'],
outputDirectories: { GATSBY: './public' },
},
log: logMock,
exit: exitMock,
} as any);

const handleEnvImportFlowMock = jest
.spyOn(fileUploadInstance, 'handleEnvImportFlow' as any)
.mockResolvedValue(undefined);

await fileUploadInstance.prepareAndUploadNewProjectFile();

const contentstackAuthCalls = (cliux.inquire as jest.Mock).mock.calls.filter(
(call) => call[0]?.name === 'contentstackAuth',
);
expect(contentstackAuthCalls.length).toBe(0);
expect(fileUploadInstance.config.isContentstackAuthenticationEnabled).toBe(true);

createSignedUploadUrlMock.mockRestore();
archiveMock.mockRestore();
uploadFileMock.mockRestore();
handleEnvImportFlowMock.mockRestore();
});
});
});

34 changes: 26 additions & 8 deletions src/adapters/file-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,15 @@ export default class FileUpload extends BaseClass {
* @memberof FileUpload
*/
async createNewProject(uploadUid: string): Promise<void> {
const {
framework,
projectName,
buildCommand,
outputDirectory,
environmentName,
serverCommand,
isStreamingEnabled
const {
framework,
projectName,
buildCommand,
outputDirectory,
environmentName,
serverCommand,
isStreamingEnabled,
isContentstackAuthenticationEnabled,
} = this.config;
await this.apolloClient
.mutate({
Expand All @@ -139,6 +140,7 @@ export default class FileUpload extends BaseClass {
buildCommand: buildCommand === undefined || buildCommand === null ? 'npm run build' : buildCommand,
...(serverCommand && serverCommand.trim() !== '' ? { serverCommand } : {}),
isStreamingEnabled: isStreamingEnabled ?? false,
isContentstackAuthenticationEnabled: isContentstackAuthenticationEnabled ?? true,
},
},
skipGitData: true,
Expand Down Expand Up @@ -177,6 +179,8 @@ export default class FileUpload extends BaseClass {
'env-variables': envVariables,
'server-command': serverCommand,
'response-mode': responseMode,
'enable-cs-auth': enableCsAuth,
'disable-cs-auth': disableCsAuth,
alias,
} = this.config.flags;
const { token, apiKey } = configHandler.get(`tokens.${alias}`) ?? {};
Expand Down Expand Up @@ -263,6 +267,20 @@ export default class FileUpload extends BaseClass {
} else {
this.config.isStreamingEnabled = responseMode === 'streaming';
}
if (enableCsAuth) {
this.config.isContentstackAuthenticationEnabled = true;
} else if (disableCsAuth) {
this.config.isContentstackAuthenticationEnabled = false;
} else {
this.config.isContentstackAuthenticationEnabled = (await cliux.inquire({
type: 'confirm',
name: 'contentstackAuth',
message:
// eslint-disable-next-line max-len
'Enable Contentstack Authentication? Restricts access to this environment to members of your Contentstack organization.',
default: true,
})) as boolean;
}
Comment on lines +270 to +283
this.config.variableType = variableType as unknown as string;
this.config.envVariables = envVariables;
await this.handleEnvImportFlow();
Expand Down
Loading
Loading