From 6471b4e718cf6f89b6762e8f7cac92658948da53 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sat, 23 Jul 2022 14:24:09 -0700 Subject: [PATCH] Closes #335 - Added Slack/Settings ignored/frozen envs --- README.md | 1 + src/api-docs/schemas/slack.js | 20 ++++++---- src/models/slack.js | 11 +++--- src/models/slack_ticket.js | 10 +++-- src/routers/slack.js | 6 +-- src/services/slack.js | 24 ++++++++---- tests/fixtures/db_api.js | 22 +++++++++++ tests/slack.test.js | 71 +++++++++++++++++++++++++++++++++-- 8 files changed, 135 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 94590ca..4d4ebd6 100644 --- a/README.md +++ b/README.md @@ -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/) diff --git a/src/api-docs/schemas/slack.js b/src/api-docs/schemas/slack.js index 1ee004e..869593f 100644 --- a/src/api-docs/schemas/slack.js +++ b/src/api-docs/schemas/slack.js @@ -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' @@ -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' + } } } }, diff --git a/src/models/slack.js b/src/models/slack.js index 6807955..47b3649 100644 --- a/src/models/slack.js +++ b/src/models/slack.js @@ -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] }, { diff --git a/src/models/slack_ticket.js b/src/models/slack_ticket.js index 0952155..caa08ae 100644 --- a/src/models/slack_ticket.js +++ b/src/models/slack_ticket.js @@ -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, @@ -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 } diff --git a/src/routers/slack.js b/src/routers/slack.js index f608c02..8e0e38d 100644 --- a/src/routers/slack.js +++ b/src/routers/slack.js @@ -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); } diff --git a/src/services/slack.js b/src/services/slack.js index 689929d..e6a9124 100644 --- a/src/services/slack.js +++ b/src/services/slack.js @@ -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'; @@ -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) { @@ -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]); diff --git a/tests/fixtures/db_api.js b/tests/fixtures/db_api.js index f1ff129..621a0a9 100644 --- a/tests/fixtures/db_api.js +++ b/tests/fixtures/db_api.js @@ -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, @@ -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' @@ -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(); diff --git a/tests/slack.test.js b/tests/slack.test.js index 19848c9..347b470 100644 --- a/tests/slack.test.js +++ b/tests/slack.test.js @@ -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'; @@ -17,6 +18,7 @@ import { groupConfigDocument, adminAccountToken } from './fixtures/db_api'; +import { TicketValidationType } from '../src/models/slack_ticket'; afterAll(async () => { await Slack.deleteMany(); @@ -474,7 +476,7 @@ 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({ @@ -482,8 +484,13 @@ describe('Slack Route - Create Ticket', () => { 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({ @@ -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, @@ -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)