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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Switching fast. Adapt everywhere.
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=switcherapi_switcher-api&metric=alert_status)](https://sonarcloud.io/dashboard?id=switcherapi_switcher-api)
[![Known Vulnerabilities](https://snyk.io/test/github/switcherapi/switcher-api/badge.svg)](https://snyk.io/test/github/switcherapi/switcher-api)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker Hub](https://img.shields.io/docker/pulls/trackerforce/switcher-api.svg)](https://hub.docker.com/r/trackerforce/switcher-api)
[![Slack: Switcher-HQ](https://img.shields.io/badge/slack-@switcher/hq-blue.svg?logo=slack)](https://switcher-hq.slack.com/)

</div>
Expand Down
20 changes: 13 additions & 7 deletions src/api-docs/schemas/slack.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ const ticket = {
enum: Object.values(TicketStatusType),
default: TicketStatusType.OPENED
},
ticket_approvals: {
type: 'number',
description: 'The number of approvals'
},
date_closed: {
type: 'string',
description: 'The date closed'
Expand Down Expand Up @@ -175,9 +171,19 @@ const slack = {
settings: {
type: 'object',
properties: {
approvals: {
type: 'number',
description: 'The number of approvals required to approve a ticket'
ignored_environments: {
type: 'array',
description: 'Environments that should be ignored for the approval request',
items: {
type: 'string'
}
},
frozen_environments: {
type: 'array',
description: 'Environments that should not change',
items: {
type: 'string'
}
}
}
},
Expand Down
11 changes: 6 additions & 5 deletions src/models/slack.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ const slackSchema = new mongoose.Schema({
type: Object
},
settings: {
approvals: {
type: Number,
required: true,
default: 1
}
ignored_environments: [{
type: String
}],
frozen_environments: [{
type: String
}]
},
tickets: [slackTicketSchema]
}, {
Expand Down
10 changes: 6 additions & 4 deletions src/models/slack_ticket.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export const TicketStatusType = Object.freeze({
DENIED: 'DENIED'
});

export const TicketValidationType = Object.freeze({
VALIDATED: 'VALIDATED',
IGNORED_ENVIRONMENT: 'IGNORED_ENVIRONMENT',
FROZEN_ENVIRONMENT: 'FROZEN_ENVIRONMENT'
});

export const slackTicketSchema = new mongoose.Schema({
environment: {
type: String,
Expand All @@ -32,10 +38,6 @@ export const slackTicketSchema = new mongoose.Schema({
default: TicketStatusType.OPENED,
required: true
},
ticket_approvals: {
type: Number,
default: 0
},
date_closed: {
type: Date
}
Expand Down
6 changes: 3 additions & 3 deletions src/routers/slack.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ router.post('/slack/v1/ticket/validate', slackAuth, [
], validate, async (req, res) => {
try {
const ticket_content = createTicketContent(req);
await Services.validateTicket(
const validation = await Services.validateTicket(
ticket_content, req.body.enterprise_id, req.body.team_id);
res.status(200).send({ message: 'Ticket verified' });

res.status(200).send({ message: 'Ticket validated', result: validation.result });
} catch (e) {
responseException(res, e, 400);
}
Expand Down
24 changes: 16 additions & 8 deletions src/services/slack.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Slack from '../models/slack';
import { checkValue, Switcher } from 'switcher-client';
import { TicketStatusType, SLACK_SUB } from '../models/slack_ticket';
import { TicketStatusType, SLACK_SUB, TicketValidationType } from '../models/slack_ticket';
import { NotFoundError, PermissionError } from '../exceptions';
import { checkSlackIntegration, checkFeature, SwitcherKeys } from '../external/switcher-api-facade';
import { getConfig } from './config';
Expand Down Expand Up @@ -141,7 +141,19 @@ export async function resetTicketHistory(enterprise_id, team_id, admin) {

export async function validateTicket(ticket_content, enterprise_id, team_id) {
const slack = await getSlackOrError({ enterprise_id, team_id });
return canCreateTicket(slack, ticket_content);

const ticket = await canCreateTicket(slack, ticket_content);
const { ignored_environments, frozen_environments } = slack.settings;

if (frozen_environments?.includes(ticket_content.environment))
return { result: TicketValidationType.FROZEN_ENVIRONMENT };

if (ignored_environments?.includes(ticket_content.environment)) {
await approveChange(slack.domain, ticket_content);
return { result: TicketValidationType.IGNORED_ENVIRONMENT };
}

return { result: TicketValidationType.VALIDATED, ticket};
}

export async function createTicket(ticket_content, enterprise_id, team_id) {
Expand Down Expand Up @@ -176,12 +188,8 @@ export async function processTicket(enterprise_id, team_id, ticket_id, approved)
throw new NotFoundError('Ticket not found');

if (approved) {
ticket[0].ticket_approvals += 1;

if (slack.settings.approvals >= ticket[0].ticket_approvals) {
ticket[0].ticket_status = TicketStatusType.APPROVED;
await closeTicket(slack.domain, ticket[0]);
}
ticket[0].ticket_status = TicketStatusType.APPROVED;
await closeTicket(slack.domain, ticket[0]);
} else {
ticket[0].ticket_status = TicketStatusType.DENIED;
await closeTicket(null, ticket[0]);
Expand Down
22 changes: 22 additions & 0 deletions tests/fixtures/db_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ export const environment1 = {
owner: adminMasterAccountId
};

export const environment2Id = new mongoose.Types.ObjectId();
export const environment2 = {
_id: environment2Id,
name: 'dev',
domain: domainId,
owner: adminMasterAccountId
};

export const environment3Id = new mongoose.Types.ObjectId();
export const environment3 = {
_id: environment3Id,
name: 'staging',
domain: domainId,
owner: adminMasterAccountId
};

export const groupConfigId = new mongoose.Types.ObjectId();
export const groupConfigDocument = {
_id: groupConfigId,
Expand Down Expand Up @@ -178,6 +194,10 @@ export const slack = {
team_id: 'TEAM_ID',
user_id: 'USER_ID',
domain: domainId,
settings: {
ignored_environments: ['dev'],
frozen_environments: ['staging']
},
installation_payload : {
incoming_webhook_channel : 'Approval Team',
incoming_webhook_channel_id : 'CHANNEL_ID'
Expand Down Expand Up @@ -208,6 +228,8 @@ export const setupDatabase = async () => {
await new Admin(adminAccount).save();

await new Environment(environment1).save();
await new Environment(environment2).save();
await new Environment(environment3).save();
await new Domain(domainDocument).save();
await new GroupConfig(groupConfigDocument).save();
await new Config(config1Document).save();
Expand Down
71 changes: 68 additions & 3 deletions tests/slack.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import jwt from 'jsonwebtoken';
import app from '../src/app';
import * as Services from '../src/services/slack';
import { getDomainById } from '../src/services/domain';
import { getConfig } from '../src/services/config';
import { mock1_slack_installation } from './fixtures/db_slack';
import { EnvType } from '../src/models/environment';
import Slack from '../src/models/slack';
Expand All @@ -17,6 +18,7 @@ import {
groupConfigDocument,
adminAccountToken
} from './fixtures/db_api';
import { TicketValidationType } from '../src/models/slack_ticket';

afterAll(async () => {
await Slack.deleteMany();
Expand Down Expand Up @@ -474,16 +476,21 @@ describe('Slack Route - Create Ticket', () => {
};

//validate
await request(app)
let response = await request(app)
.post('/slack/v1/ticket/validate')
.set('Authorization', `Bearer ${generateToken('30s')}`)
.send({
team_id: slack.team_id,
ticket_content
}).expect(200);

expect(response.body).toMatchObject({
message: 'Ticket validated',
result: TicketValidationType.VALIDATED
});

//test - create
const response = await request(app)
response = await request(app)
.post('/slack/v1/ticket/create')
.set('Authorization', `Bearer ${generateToken('30s')}`)
.send({
Expand All @@ -498,6 +505,64 @@ describe('Slack Route - Create Ticket', () => {
});
});

test('SLACK_SUITE - Should NOT create a ticket - Environment Ignored', async () => {
//given
const ticket_content = {
environment: 'dev',
group: groupConfigDocument.name,
switcher: config1Document.key,
status: false
};

//validate
let switcher = await getConfig({ key: config1Document.key, domain: slack.domain });
expect(switcher.activated.get('dev')).toBe(undefined);

const response = await request(app)
.post('/slack/v1/ticket/validate')
.set('Authorization', `Bearer ${generateToken('30s')}`)
.send({
team_id: slack.team_id,
ticket_content
}).expect(200);

switcher = await getConfig({ key: config1Document.key, domain: slack.domain });
expect(switcher.activated.get('dev')).toBe(false);
expect(response.body).toMatchObject({
message: 'Ticket validated',
result: TicketValidationType.IGNORED_ENVIRONMENT
});
});

test('SLACK_SUITE - Should NOT create a ticket - Environment frozen', async () => {
//given
const ticket_content = {
environment: 'staging',
group: groupConfigDocument.name,
switcher: config1Document.key,
status: false
};

//validate
let switcher = await getConfig({ key: config1Document.key, domain: slack.domain });
expect(switcher.activated.get('staging')).toBe(undefined);

const response = await request(app)
.post('/slack/v1/ticket/validate')
.set('Authorization', `Bearer ${generateToken('30s')}`)
.send({
team_id: slack.team_id,
ticket_content
}).expect(200);

switcher = await getConfig({ key: config1Document.key, domain: slack.domain });
expect(switcher.activated.get('staging')).toBe(undefined);
expect(response.body).toMatchObject({
message: 'Ticket validated',
result: TicketValidationType.FROZEN_ENVIRONMENT
});
});

test('SLACK_SUITE - Should NOT create a ticket - Invalid', async () => {
const ticket_content = {
environment: EnvType.DEFAULT,
Expand Down Expand Up @@ -527,7 +592,7 @@ describe('Slack Route - Create Ticket', () => {
};

// Retrieve existing ticket
const ticket = await Services.validateTicket(
const { ticket } = await Services.validateTicket(
ticket_content, undefined, slack.team_id);

const response = await request(app)
Expand Down