Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Créer une application Scalingo avec une configuration valide via Slack #168

Merged
merged 4 commits into from
Dec 2, 2022
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
6 changes: 4 additions & 2 deletions common/models/ScalingoAppName.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
//TODO use EnvVars
const config = require('../../config');
const alphanumericAndDashOnly = /^([a-zA-Z0-9]+-)+[a-zA-Z0-9]+$/;
const prefix = 'pix-';
const prefix = config.scalingo.validAppPrefix;
class ScalingoAppName {
static isApplicationNameValid(applicationName) {
const suffix = config.scalingo.validAppSuffix;
const appNameMatchesRegex = applicationName.search(alphanumericAndDashOnly) >= 0;
const appNameHasCorrectLength = applicationName.length >= 6 && applicationName.length <= 46;
const appNameHasCorrectLength =
applicationName.length >= config.scalingo.validAppNbCharMin &&
applicationName.length <= config.scalingo.validAppNbCharMax;
const appNameStartsWithPix = applicationName.startsWith(prefix);
const appNameEndsWithCorrectSuffix = suffix.includes(applicationName.split('-').slice(-1)[0]);
return appNameMatchesRegex && appNameHasCorrectLength && appNameStartsWithPix && appNameEndsWithCorrectSuffix;
Expand Down
7 changes: 6 additions & 1 deletion common/services/scalingo-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class ScalingoClient {
throw new Error(`Unable to deploy ${scalingoApp} ${releaseTag}`);
}

return `Deployement of ${scalingoApp} ${releaseTag} has been requested`;
return `Deployment of ${scalingoApp} ${releaseTag} has been requested`;
}

async getAppInfo(target) {
Expand Down Expand Up @@ -108,8 +108,13 @@ class ScalingoClient {
const app = {
name: name,
};
const appSettings = {
force_https: true,
router_logs: true,
};
try {
const { id } = await this.client.Apps.create(app);
await this.client.Apps.update(id, appSettings);
return id;
} catch (e) {
console.error(JSON.stringify(e));
Expand Down
3 changes: 3 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ module.exports = (function () {
'router',
'test',
],
validAppPrefix: 'pix',
validAppNbCharMax: 46,
validAppNbCharMin: 6,
maxLogLength: process.env.MAX_LOG_LENGTH || 1000,
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function modal(applicationName, applicationEnvironment, applicationEnvironmentNa
close: 'Annuler',
}).blocks([
Blocks.Section({
text: `Vous vous apprêtez à créer l'application *${applicationName}* dans la région : *${applicationEnvironmentName}* et à inviter cet adesse email en tant que collaborateur : *${userEmail}*`,
text: `Vous vous apprêtez à créer l'application *${applicationName}* dans la région : *${applicationEnvironmentName}* et à inviter cet adresse email en tant que collaborateur : *${userEmail}*`,
}),
]);
}
Expand Down
9 changes: 8 additions & 1 deletion run/services/slack/view-submissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const slackPostMessageService = require('../../../common/services/slack/surfaces
const slackGetUserInfos = require('../../../common/services/slack/surfaces/user-infos/get-user-infos');
const ScalingoClient = require('../../../common/services/scalingo-client');
const { ScalingoAppName } = require('../../../common/models/ScalingoAppName');
const config = require('../../../config');

module.exports = {
async submitReleaseTagSelection(payload) {
Expand All @@ -20,8 +21,14 @@ module.exports = {
const applicationEnvironmentName =
payload.view.state.values['application-env']['item']['selected_option']['text']['text'];
const userEmail = await slackGetUserInfos.getUserEmail(payload.user.id);
const appSufixList = config.scalingo.validAppSuffix.toString();
if (!ScalingoAppName.isApplicationNameValid(applicationName)) {
return `${applicationName} is incorrect`;
return {
response_action: 'errors',
errors: {
'create-app-name': `${applicationName} is incorrect, it should start with "${config.scalingo.validAppPrefix}-" and end with one of the following : ${appSufixList}. Also the length should be between ${config.scalingo.validAppNbCharMin} and ${config.scalingo.validAppNbCharMax} characters.`,
},
};
}
return openModalApplicationCreationConfirmation(
applicationName,
Expand Down
2 changes: 1 addition & 1 deletion test/acceptance/run/slack_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ describe('Acceptance | Run | Slack', function () {
{
text: {
type: 'mrkdwn',
text: "Vous vous apprêtez à créer l'application *pix-application-de-folie-recette* dans la région : *recette* et à inviter cet adesse email en tant que collaborateur : *john.doe@pix.fr*",
text: "Vous vous apprêtez à créer l'application *pix-application-de-folie-recette* dans la région : *recette* et à inviter cet adresse email en tant que collaborateur : *john.doe@pix.fr*",
},
type: 'section',
},
Expand Down
130 changes: 130 additions & 0 deletions test/integration/common/services/slack/view-submissions_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
const { expect, sinon } = require('../../../../test-helper');
const viewSubmissions = require('../../../../../run/services/slack/view-submissions');
const slackGetUserInfos = require('../../../../../common/services/slack/surfaces/user-infos/get-user-infos');

describe('view-submissions', function () {
describe('#submitApplicationNameSelection', function () {
context('when application name is invalid', function () {
it('should return error message', async function () {
// given
const applicationName = 'foo-bar-production';
const applicationEnvironment = 'production';
const applicationEnvironmentName = 'production';
const userId = 1;
const userEmail = 'john.doe@pix.fr';

const getUserEmailStub = sinon.stub(slackGetUserInfos, 'getUserEmail');
getUserEmailStub.resolves(userEmail);

const payload = {
view: {
state: {
values: {
'create-app-name': {
'scalingo-app-name': {
value: applicationName,
},
},
'application-env': {
item: {
selected_option: {
value: applicationEnvironment,
text: {
text: applicationEnvironmentName,
},
},
},
},
},
},
},
user: { id: userId },
};

// when
const actual = await viewSubmissions.submitApplicationNameSelection(payload);

// then
expect(actual).to.deep.equal({
response_action: 'errors',
errors: {
'create-app-name': `foo-bar-production is incorrect, it should start with "pix-" and end with one of the following : production,review,integration,recette,sandbox,dev,router,test. Also the length should be between 6 and 46 characters.`,
},
});
});
});
context('when application name is valid', function () {
it('should return response', async function () {
// given
const applicationName = 'pix-foo-production';
const applicationEnvironment = 'production';
const applicationEnvironmentName = 'production';
const userId = 1;
const userEmail = 'john.doe@pix.fr';

const getUserEmailStub = sinon.stub(slackGetUserInfos, 'getUserEmail');
getUserEmailStub.resolves(userEmail);

const payload = {
view: {
state: {
values: {
'create-app-name': {
'scalingo-app-name': {
value: applicationName,
},
},
'application-env': {
item: {
selected_option: {
value: applicationEnvironment,
text: {
text: applicationEnvironmentName,
},
},
},
},
},
},
},
user: { id: userId },
};

// when
const actual = await viewSubmissions.submitApplicationNameSelection(payload);

// then
expect(actual).to.deep.equal({
response_action: 'push',
view: {
blocks: [
{
text: {
text: "Vous vous apprêtez à créer l'application *pix-foo-production* dans la région : *production* et à inviter cet adresse email en tant que collaborateur : *john.doe@pix.fr*",
type: 'mrkdwn',
},
type: 'section',
},
],
callback_id: 'application-creation-confirmation',
close: {
text: 'Annuler',
type: 'plain_text',
},
private_metadata:
'{"applicationName":"pix-foo-production","applicationEnvironment":"production","userEmail":"john.doe@pix.fr"}',
submit: {
text: '🚀 Go !',
type: 'plain_text',
},
title: {
text: 'Confirmation',
type: 'plain_text',
},
type: 'modal',
},
});
});
});
});
});
56 changes: 56 additions & 0 deletions test/integration/run/services/slack/view-submissions_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const slackViewSubmissions = require('../../../../../run/services/slack/view-submissions');
const { expect, nock, createScalingoTokenNock } = require('../../../../test-helper');

describe('Integration | Run | Services | Slack | Commands', function () {
describe('#submitCreateAppOnScalingoConfirmation', function () {
it('creates a scalingo application', async function () {
const payload = {
user: { id: 'idslack' },
view: {
private_metadata:
'{"applicationName": "foobar","applicationEnvironment": "recette", "userEmail": "foo@bar.fr"}',
},
};
const expectedResponse = {
response_action: 'clear',
};

const expectedBody = { app: { name: 'foobar' } };
const expectedUpdateBody = {
app: {
force_https: true,
router_logs: true,
},
};
const expectedInviteCollaboratorBody = {
collaborator: { email: 'foo@bar.fr' },
};
createScalingoTokenNock();
nock(`https://scalingo.recette`)
.post('/v1/apps', JSON.stringify(expectedBody))
.reply(201, { app: { id: 1 } });
nock(`https://scalingo.recette`)
.patch('/v1/apps/1', JSON.stringify(expectedUpdateBody))
.reply(200, { app: { name: 'foobar' } });
nock(`https://scalingo.recette`)
.post('/v1/apps/1/collaborators', JSON.stringify(expectedInviteCollaboratorBody))
.reply(201, {
collaborator: [
{
email: 'collaborator@example.com',
id: '54101e25736f7563d5060000',
status: 'pending',
username: 'n/a',
invitation_link:
'https://my.scalingo.com/apps/collaboration?token=8415965b809c928c807dc99790e5745d97f05b8c',
app_id: '5343eccd646173000a140000',
},
],
});

const response = await slackViewSubmissions.submitCreateAppOnScalingoConfirmation(payload);

expect(response).to.deep.equal(expectedResponse);
});
});
});
4 changes: 4 additions & 0 deletions test/test-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,17 @@ function nockGithubWithConfigChanges() {
.reply(200, [{}]);
}

function createScalingoTokenNock() {
nock(`https://auth.scalingo.com`).post('/v1/tokens/exchange').reply(200, {});
}
// eslint-disable-next-line mocha/no-exports
module.exports = {
catchErr,
expect,
nock,
sinon,
createGithubWebhookSignatureHeader,
createScalingoTokenNock,
createSlackWebhookSignatureHeaders,
nockGithubWithConfigChanges,
nockGithubWithNoConfigChanges,
Expand Down
79 changes: 79 additions & 0 deletions test/unit/common/services/scalingo-client_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,4 +406,83 @@ describe('Scalingo client', () => {
expect(manualReviewApp.called).to.be.true;
});
});

describe('#Scalingo.createApplication', () => {
it('should return application identifier', async () => {
// given
const createApplicationStub = sinon.stub();
const updateApplicationStub = sinon.stub();
sinon
.stub(scalingo, 'clientFromToken')
.resolves({ Apps: { create: createApplicationStub, update: updateApplicationStub } });
createApplicationStub.resolves({ id: 1 });
updateApplicationStub.resolves();
const scalingoClient = await ScalingoClient.getInstance('recette');

// when
const actual = await scalingoClient.createApplication('pix-application-recette');

// then
expect(actual).to.equal(1);
});
it('should call create with application name', async () => {
// given
const createApplicationStub = sinon.stub();
const updateApplicationStub = sinon.stub();
sinon
.stub(scalingo, 'clientFromToken')
.resolves({ Apps: { create: createApplicationStub, update: updateApplicationStub } });
createApplicationStub.resolves({ id: 1 });
updateApplicationStub.resolves();
const scalingoClient = await ScalingoClient.getInstance('recette');

// when
await scalingoClient.createApplication('pix-application-recette');

// then
expect(createApplicationStub).to.have.been.calledOnceWithExactly({ name: 'pix-application-recette' });
});
it('should call update with valid options', async () => {
// given
const createApplicationStub = sinon.stub();
const updateApplicationStub = sinon.stub();
sinon
.stub(scalingo, 'clientFromToken')
.resolves({ Apps: { create: createApplicationStub, update: updateApplicationStub } });
createApplicationStub.resolves({ id: 1 });
updateApplicationStub.resolves();
const scalingoClient = await ScalingoClient.getInstance('recette');

// when
await scalingoClient.createApplication('pix-application-recette');

// then
expect(updateApplicationStub).to.have.been.calledOnceWithExactly(1, { force_https: true, router_logs: true });
});

it('should throw when scalingo client throw an error', async () => {
// given
const createApplicationStub = sinon.stub();
const updateApplicationStub = sinon.stub();
sinon
.stub(scalingo, 'clientFromToken')
.resolves({ Apps: { create: createApplicationStub, update: updateApplicationStub } });
createApplicationStub.resolves({ id: 1 });
updateApplicationStub.rejects({
name: 'foo',
});
const scalingoClient = await ScalingoClient.getInstance('recette');

// when
let actual;
try {
await scalingoClient.createApplication('pix-application-recette');
} catch (error) {
actual = error;
}

// then
expect(actual.message).to.equal('Impossible to create pix-application-recette, foo');
});
});
});