Skip to content
This repository was archived by the owner on Jun 28, 2022. It is now read-only.

Commit

Permalink
fix: stack name tests
Browse files Browse the repository at this point in the history
  • Loading branch information
arantespp committed Feb 3, 2021
1 parent 82af45c commit b7b9464
Show file tree
Hide file tree
Showing 17 changed files with 298 additions and 186 deletions.
40 changes: 21 additions & 19 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,30 @@ export const getConfig = () => {
return finalConfig;
};

export const options = {
config: {
alias: 'c',
describe: 'Path to JavaScript, JSON or YAML file.',
require: false,
type: 'string',
},
environment: {
alias: ['e', 'env'],
coerce: (environment: string) => {
if (environment) {
setEnvironment(environment);
}
return environment;
},
type: 'string',
},
environments: {},
} as const;

yargs
.scriptName(NAME)
.env(constantCase(NAME))
.options({
config: {
alias: 'c',
describe: 'Path to JavaScript, JSON or YAML file.',
require: false,
type: 'string',
},
environment: {
alias: ['e', 'env'],
coerce: (environment) => {
if (environment) {
setEnvironment(environment);
}
return environment;
},
type: 'string',
},
environments: {},
})
.options(options)
.middleware((argv) => {
const { environment, environments } = argv as any;

Expand Down
139 changes: 86 additions & 53 deletions packages/cli/src/deploy/stackName.spec.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,102 @@
// import { pascalCase } from 'change-case';
// import faker from 'faker';
// /* eslint-disable import/first */
import { pascalCase, paramCase } from 'change-case';
import faker from 'faker';

// // import { getEnvironment } from '../utils';
import { getCurrentBranch, getEnvironment, getPackageName } from '../utils';

// // import { getStackName } from './stackName';
import { getStackName, setPreDefinedStackName } from './stackName';

// const branchName = [
// faker.random.words(1),
// faker.random.words(1),
// faker.random.words(1),
// faker.random.words(1),
// ].join('/');
const mockMath = Object.create(global.Math);
const randomNumber = 0.12345;
mockMath.random = () => randomNumber;
global.Math = mockMath;

// const packageName = `@${faker.random.words(1)}/${faker.random.words(1)}`;
const branchName = [
faker.random.words(1),
faker.random.words(1),
faker.random.words(1),
faker.random.words(1),
].join('/');

// const packageNamePascalCase = pascalCase(packageName);
const environment = faker.random.word();

// jest.mock('../utils', () => ({
// getCurrentBranch: jest.fn(() => branchName),
// getEnvironment: jest.fn(),
// getPackageName: jest.fn(() => packageName),
// }));
const packageName = `@${faker.random.word()}/${faker.random.word()}`;

jest.mock('../utils', () => ({
getCurrentBranch: jest.fn(),
getEnvironment: jest.fn(),
getPackageName: jest.fn(),
}));

describe('testing getStackName', () => {
test('should return correct name for testing environment', async () => {
// (getEnvironment as jest.Mock).mockReturnValueOnce('Testing');
// const stackName = await getStackName();
// expect(stackName).toEqual(
// `Test${pascalCase(branchName)}-${packageNamePascalCase}`
// );
expect(1).toEqual(1);
test('documentation case #1', async () => {
(getCurrentBranch as jest.Mock).mockReturnValueOnce(branchName);
(getEnvironment as jest.Mock).mockReturnValueOnce(environment);
(getPackageName as jest.Mock).mockReturnValueOnce(packageName);
const preDefinedStackName = faker.random.word();
setPreDefinedStackName(preDefinedStackName);
const stackName = await getStackName();
expect(stackName).toEqual(preDefinedStackName);
});

// test('should return correct name for development environment', () => {
// (getEnvironment as jest.Mock).mockReturnValueOnce('Development');
// return expect(getStackName()).resolves.toEqual(
// `Dev${packageNamePascalCase}`
// );
// });
describe('pre-defined stack name not defined', () => {
beforeEach(() => {
jest.resetAllMocks();
setPreDefinedStackName('');
});

test('documentation case #2', async () => {
(getCurrentBranch as jest.Mock).mockReturnValueOnce(branchName);
(getEnvironment as jest.Mock).mockReturnValueOnce(environment);
(getPackageName as jest.Mock).mockReturnValueOnce(packageName);
const stackName = await getStackName();
expect(stackName).toEqual(`${pascalCase(packageName)}-${environment}`);
});

// test('should return correct name for staging environment', () => {
// (getEnvironment as jest.Mock).mockReturnValueOnce('Staging');
// return expect(getStackName()).resolves.toEqual(packageNamePascalCase);
// });
test('documentation case #3', async () => {
(getCurrentBranch as jest.Mock).mockReturnValueOnce(branchName);
(getEnvironment as jest.Mock).mockReturnValueOnce(undefined);
(getPackageName as jest.Mock).mockReturnValueOnce(packageName);
const stackName = await getStackName();
expect(stackName).toEqual(
`${pascalCase(packageName)}-${paramCase(branchName)}`,
);
});

// test('should return correct name for production environment', () => {
// (getEnvironment as jest.Mock).mockReturnValueOnce('Production');
// return expect(getStackName()).resolves.toEqual(packageNamePascalCase);
// });
test('documentation case #4', async () => {
(getCurrentBranch as jest.Mock).mockReturnValueOnce(undefined);
(getEnvironment as jest.Mock).mockReturnValueOnce(undefined);
(getPackageName as jest.Mock).mockReturnValueOnce(packageName);
const stackName = await getStackName();
expect(stackName).toEqual(`${pascalCase(packageName)}`);
});

// describe('should return correct name when pre defined stack name is provided', () => {
// const preDefinedStackName = faker.random.word();
test('documentation case #5', async () => {
(getCurrentBranch as jest.Mock).mockReturnValueOnce(branchName);
(getEnvironment as jest.Mock).mockReturnValueOnce(environment);
(getPackageName as jest.Mock).mockReturnValueOnce(undefined);
const stackName = await getStackName();
expect(stackName).toEqual(
`Stack-${randomNumber * 100000}-${environment}`,
);
});

// test('Testing environment', () => {
// (getEnvironment as jest.Mock).mockReturnValueOnce('Testing');
// return expect(getStackName({ preDefinedStackName })).resolves.toEqual(
// `Test${preDefinedStackName}`
// );
// });
test('documentation case #6', async () => {
(getCurrentBranch as jest.Mock).mockReturnValueOnce(branchName);
(getEnvironment as jest.Mock).mockReturnValueOnce(undefined);
(getPackageName as jest.Mock).mockReturnValueOnce(undefined);
const stackName = await getStackName();
expect(stackName).toEqual(
`Stack-${randomNumber * 100000}-${paramCase(branchName)}`,
);
});

// test('Development environment', () => {
// (getEnvironment as jest.Mock).mockReturnValueOnce('Development');
// return expect(getStackName({ preDefinedStackName })).resolves.toEqual(
// `Dev${preDefinedStackName}`
// );
// });
// });
test('documentation case #6', async () => {
(getCurrentBranch as jest.Mock).mockReturnValueOnce(undefined);
(getEnvironment as jest.Mock).mockReturnValueOnce(undefined);
(getPackageName as jest.Mock).mockReturnValueOnce(undefined);
const stackName = await getStackName();
expect(stackName).toEqual(`Stack-${randomNumber * 100000}`);
});
});
});
56 changes: 48 additions & 8 deletions packages/cli/src/deploy/stackName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,56 @@ import { getCurrentBranch, getEnvironment, getPackageName } from '../utils';

let preDefinedStackName = '';

/**
* Used by CLI set stack name when it is defined.
*/
export const setPreDefinedStackName = (name: string) => {
preDefinedStackName = name;
};

/**
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-using-console-create-stack-parameters.html
* https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html
*/
export const STACK_NAME_MAX_LENGTH = 128;

export const limitStackName = (stackName: string) =>
`${stackName}`.substring(0, STACK_NAME_MAX_LENGTH);

/**
* If stack name isn't previously defined, the name will be created accordingly
* with the following rules:
*
* 1. The name has to parts.
*
* 1. The first part is defined by the package.json name, if it is defined.
* Else, it'll be a random name starting with the string "Stack-", , e.g. **Stack-96830**.
*
* 1. The second part will be defined by, whichever is defined first:
* 1. environment;
* 1. branch name in param-case;
* 1. `undefined`.
*
* Example:
*
* | Case | Package Name | Environment | Branch Name | `--stack-name` | Stack Name |
* | ---- | ------------ | ----------- | ---------- | -------------- | ---------- |
* | #1 | @package/name | prod | main | MyStackName | **MyStackName** |
* | #2 | @package/name | prod | main | | **PackageName-prod** |
* | #3 | @package/name | | main | | **PackageName-main** |
* | #4 | @package/name | | | | **PackageName** |
* | #5 | | prod | main | | **Stack-96820-prod** |
* | #6 | | | main | | **Stack-96820-main** |
* | #7 | | | | | **Stack-96820** |
*
* CAUTION!!!
*
* This method is a BREAKING CHANGE for **carlin**, I hope we never have to
* change this algorithm, ever. Stack name is how we track the stacks on AWS.
* Suppose we change this algorithm. If we perform an update or destroy
* operation, **carlin** will create another stack or do nothing because the
* old stack won't be found due to stack name changing.
*
*/
export const getStackName = async () => {
if (preDefinedStackName) {
return preDefinedStackName;
Expand All @@ -27,21 +65,23 @@ export const getStackName = async () => {
getPackageName(),
]);

const name = (() => {
if (!packageName) {
return `Stack-${Math.round(Math.random() * 100000)}`;
}
const firstName = packageName
? pascalCase(packageName)
: `Stack-${Math.round(Math.random() * 100000)}`;

const secondName = (() => {
if (environment) {
return `${pascalCase(packageName)}-${environment}`;
return environment;
}

if (currentBranch) {
return `${pascalCase(packageName)}-${paramCase(currentBranch)}`;
return paramCase(currentBranch);
}

return pascalCase(packageName);
return undefined;
})();

const name = [firstName, secondName].filter((word) => !!word).join('-');

return limitStackName(name);
};
6 changes: 6 additions & 0 deletions packages/website/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
module.exports = {
extends: '../../config/eslintrc.react',
rules: {
'no-use-before-define': 'off',
'react/jsx-filename-extension': 'off',
'react/jsx-one-expression-per-line': 'off',
'react/no-danger': 'off',
},
};
1 change: 0 additions & 1 deletion packages/website/carlin/carlin.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const cli = require('carlin/dist/cli').default;
// const { options: deployOptions } = require('carlin/dist/deploy/command');

const cliApi = async (cmd) =>
new Promise((resolve) => {
Expand Down
6 changes: 4 additions & 2 deletions packages/website/carlin/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ const getComment = ([pathFromDist, longname]) => {
return description;
};

const toHtml = (comment) => renderToString(compiler(comment));

const getComments = (commentsDir, { html } = { html: true }) =>
Object.entries(commentsDir).reduce((acc, [key, value]) => {
const comment = getComment(value);
return {
...acc,
[key]: html ? renderToString(compiler(comment)) : comment,
[key]: html ? toHtml(comment) : comment,
};
}, {});

module.exports = { getComments };
module.exports = { getComment, getComments, toHtml };
37 changes: 27 additions & 10 deletions packages/website/carlin/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable global-require */
const carlin = require('./carlin');
const { getComments } = require('./comments');
const { getComment, getComments, toHtml } = require('./comments');

module.exports = () => {
return {
Expand All @@ -10,15 +11,31 @@ module.exports = () => {
api: {
deploy: await carlin.cliApi('deploy'),
},
comments: getComments({
deploy: ['deploy/cloudFormation.js', 'deploy'],
deployCloudFormationDeployLambdaCode: [
'deploy/cloudFormation.js',
'deployCloudFormation~deployCloudFormationDeployLambdaCode',
],
deployLambdaCode: ['deploy/lambda.js', 'deployLambdaCode'],
destroy: ['deploy/cloudFormation.js', 'destroy'],
}),
comments: {
...getComments({
deploy: ['deploy/cloudFormation.js', 'deploy'],
deployCloudFormationDeployLambdaCode: [
'deploy/cloudFormation.js',
'deployCloudFormation~deployCloudFormationDeployLambdaCode',
],
deployLambdaCode: ['deploy/lambda.js', 'deployLambdaCode'],
destroy: ['deploy/cloudFormation.js', 'destroy'],
}),
stackName: toHtml(
getComment(['deploy/stackName.js', 'getStackName']).split(
'CAUTION!!!',
)[0],
),
stackNameWarning: toHtml(
getComment(['deploy/stackName.js', 'getStackName']).split(
'CAUTION!!!',
)[1],
),
},
options: {
cli: require('carlin/dist/cli').options,
deploy: require('carlin/dist/deploy/command').options,
},
};
},
contentLoaded: async ({ actions, content }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
title: CLI
---

import OptionAliasesInline from '../src/components/OptionAliasesInline';

import carlin from '../.docusaurus/carlin/default/carlin.json';

## Environments

**carlin** was projected to work with environments. As we've been building a lot of Apps, we realized that we commonly have some environments, like production and staging. The difference between these environments was some options values that we passed to the deploy command. To handle these options, we've created the `environments` option. It receives an object whose keys are the environment name and the values are an object containing the command options.
**carlin** was projected to work with environments. As we've been building a lot of Apps, we realized that we commonly have some environments, like production and staging. The difference between these environments was some options values that we passed to the deploy command. To handle these options, we've created the <OptionAliasesInline option="environments" options={carlin.options.cli} /> option. It receives an object whose keys are the environment name and the values are an object containing the command options.

Besides `environments`, if we provide the option `-e`, `--env` or `--environment`, **carlin** searches if such environment exists inside `environments` object and assign the values to the command. For instance, suppose that we have the `carlin.yml` below.
Besides <OptionAliasesInline option="environments" options={carlin.options.cli} />, if we provide the option <OptionAliasesInline option="environment" options={carlin.options.cli} />, **carlin** searches if such environment exists inside environments object and assign the values to the command. For instance, suppose that we have the `carlin.yml` below.

```yaml title="carlin.yml"
region: us-east-1
Expand Down
Loading

0 comments on commit b7b9464

Please sign in to comment.