├── slackbot ├── middlewares │ ├── index.js │ ├── print-command.js │ └── load-issues.js ├── eventBus.js ├── response │ ├── index.js │ ├── patterns.js │ └── callbacks.js ├── commands │ ├── login.js │ ├── help.js │ ├── my-self.js │ ├── good-morning.js │ ├── hello.js │ ├── index.js │ ├── player.js │ ├── goal.js │ ├── score.js │ ├── issues.js │ └── top10.js ├── subscribe.js ├── messages │ ├── index.js │ └── types.js ├── auth.js └── index.js ├── .travis.yml ├── src ├── request.js ├── auth.js ├── url.js ├── db.js ├── query.js ├── configs.js ├── utils.js ├── init.js ├── pontuations.js ├── filters.js ├── parser.js └── spider.js ├── tests ├── fixtures │ ├── player.js │ ├── env_test │ ├── issues.js │ └── issues.json ├── src │ ├── auth.test.js │ ├── url.test.js │ ├── configs_dev.test.js │ ├── configs.test.js │ ├── query.test.js │ ├── utils.test.js │ ├── parser.test.js │ ├── parser_filter.test.js │ ├── filters.test.js │ └── pontuations.test.js └── slackbot │ ├── commands │ ├── help.test.js │ ├── good-morning.test.js │ ├── my-self.test.js │ ├── hello.test.js │ ├── goal.test.js │ ├── issues.test.js │ └── score.test.js │ ├── subscribe.test.js │ ├── messages.test.js │ └── callbacks.test.js ├── package.json ├── README.md ├── LICENSE ├── .gitignore └── index.js /slackbot/middlewares/index.js: -------------------------------------------------------------------------------- 1 | require('./load-issues') 2 | require('./print-command') 3 | -------------------------------------------------------------------------------- /slackbot/eventBus.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const emitter = new EventEmitter() 3 | 4 | module.exports = emitter -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | - "8" 5 | - "node" 6 | cache: 7 | directories: 8 | - "node_modules" 9 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const axios = require('axios') 4 | 5 | module.exports = ( url, options ) => { 6 | return axios.get( url, options ) 7 | } 8 | -------------------------------------------------------------------------------- /slackbot/response/index.js: -------------------------------------------------------------------------------- 1 | const patterns = require('./patterns') 2 | const callbacks = require('./callbacks') 3 | 4 | module.exports = { 5 | patterns, 6 | callbacks 7 | } -------------------------------------------------------------------------------- /slackbot/commands/login.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const { login: logMe } = require('../auth') 3 | 4 | const login = message => logMe( message ) 5 | 6 | console.log( 'on LOGIN' ) 7 | module.exports = login -------------------------------------------------------------------------------- /src/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { toBase64 } = require('./utils') 4 | const to64 = ( login, pass ) => toBase64( `${login}:${pass}` ) 5 | const auth = (login, pass) => to64( login, pass ) 6 | 7 | module.exports = auth -------------------------------------------------------------------------------- /slackbot/commands/help.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const emitter = require('../eventBus') 3 | 4 | const help = message => emitter.emit('SEND', messages('HELP'), message.channel ) 5 | 6 | console.log('on HELP') 7 | module.exports = help -------------------------------------------------------------------------------- /slackbot/commands/my-self.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const emitter = require('../eventBus') 3 | 4 | const mySelf = message => emitter.emit('SEND', messages('MY_SELF'), message.channel ) 5 | 6 | console.log('on MY_SELF') 7 | module.exports = mySelf -------------------------------------------------------------------------------- /slackbot/commands/good-morning.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const emitter = require('../eventBus') 3 | 4 | const goodMorning = message => emitter.emit('SEND', messages('GOOD_MORNING'), message.channel ) 5 | 6 | console.log('on GOOD_MORNING') 7 | module.exports = goodMorning -------------------------------------------------------------------------------- /slackbot/commands/hello.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const emitter = require('../eventBus') 3 | 4 | const sayHello = messages('HELLO') 5 | const hello = message => emitter.emit('SEND', sayHello( message.user ), message.channel ) 6 | 7 | console.log('on HELLO') 8 | module.exports = hello -------------------------------------------------------------------------------- /slackbot/subscribe.js: -------------------------------------------------------------------------------- 1 | const isString = ( value ) => typeof( value ) === 'string' 2 | 3 | const subscribe = ({ message, event = null}) => ( pattern, callback ) => { 4 | if ( isString(pattern) ) { 5 | if ( message.text !== pattern ) return 6 | } else { 7 | if ( !pattern.test( message.text ) ) return 8 | } 9 | callback( message, event ); 10 | } 11 | 12 | module.exports = subscribe -------------------------------------------------------------------------------- /slackbot/middlewares/print-command.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../eventBus') 2 | const { crud } = require('../../src/db') 3 | 4 | const printCommand = ( message ) => { 5 | const { user , text } = message 6 | const player = { slackId: user } 7 | 8 | crud.findOne( player, (err, doc) => { 9 | const username = doc ? doc.username : user 10 | console.log(`${username} runs -> ${text}`) 11 | }) 12 | } 13 | 14 | emitter.on( 'PRINT_COMMAND', printCommand ) 15 | console.log('middleware PRINT_COMMAND') -------------------------------------------------------------------------------- /tests/fixtures/player.js: -------------------------------------------------------------------------------- 1 | const mockIssues = require('./issues') 2 | 3 | const player = { 4 | slackIid: '123465', 5 | username: 'fellipe.dsn.cir', 6 | channel: 'a1', 7 | updated: new Date(), 8 | isAdmin: false, 9 | months: { 10 | aug: { 11 | issues: mockIssues, 12 | goal: 3528, 13 | workdays: 21, 14 | pointsPerHour: 21 15 | } 16 | } 17 | } 18 | 19 | module.exports = player -------------------------------------------------------------------------------- /slackbot/commands/index.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../eventBus') 2 | 3 | emitter.on( 'GOOD_MORNING', require('./good-morning') ) 4 | emitter.on( 'HELLO', require('./hello') ) 5 | emitter.on( 'HELP', require('./help') ) 6 | emitter.on( 'MY_SELF', require('./my-self') ) 7 | emitter.on( 'SCORE', require('./score') ) 8 | emitter.on( 'ISSUES', require('./issues') ) 9 | emitter.on( 'LOGIN', require('./login') ) 10 | emitter.on( 'GOAL', require('./goal') ) 11 | emitter.on( 'TOP10', require('./top10') ) 12 | emitter.on( 'PLAYER', require('./player') ) -------------------------------------------------------------------------------- /tests/fixtures/env_test: -------------------------------------------------------------------------------- 1 | { 2 | "login": "fellipe.user", 3 | "pass": "123213", 4 | "domain": "http://127.0.0.1:8080/issues.json", 5 | "goalDSN": "3528", 6 | "goalQLD": "2500", 7 | "pointsHourDSN": "21", 8 | "pointsHourQLD": "16", 9 | "workdays": "21", 10 | "startDate": "2017-08-01", 11 | "endDate": "2017-08-31", 12 | "dsn": [ 13 | "person1", 14 | "person2" 15 | ], 16 | "qld": [ 17 | "person3", 18 | "person4" 19 | ], 20 | "admins": [ 21 | "person1" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jira-issues", 3 | "version": "2.1.0", 4 | "description": "GET a list of issues from Jira scraping the page.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "export ENV_DEV=true && jest --verbose --coverage", 9 | "testw": "export ENV_DEV=true && jest --watchAll --verbose --coverage --runInBand" 10 | }, 11 | "author": "@delete", 12 | "license": "MIT", 13 | "dependencies": { 14 | "axios": "^0.18.1", 15 | "nedb": "^1.8.0", 16 | "@slack/client": "^3.10.0" 17 | }, 18 | "devDependencies": { 19 | "jest": "^20.0.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/url.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // const { config } = require('./configs') 4 | const makeQuery = require('./query') 5 | 6 | const getParams = () => 7 | '&fields=assignee,project,customfield_21711,issuetype,timespent,customfield_17132&maxResults=200' 8 | 9 | const baseUrl = domain => `http://${domain}/rest/api/2/search` 10 | 11 | module.exports = ( domain, startDate, endDate, user ) => { 12 | const query = makeQuery( user, startDate, endDate ) 13 | const queryString = query.slower() 14 | const params = getParams() 15 | const uri = encodeURI( `${queryString}${params}` ) 16 | 17 | return `${baseUrl( domain )}${uri}` 18 | } -------------------------------------------------------------------------------- /tests/src/auth.test.js: -------------------------------------------------------------------------------- 1 | const auth = require('../../src/auth') 2 | 3 | describe('auth method must return login:pass as base64', () => { 4 | test('fellipe.user:123213 must return ZmVsbGlwZS51c2VyOjEyMzIxMw==', () => { 5 | const actual = auth('fellipe.user', '123213') 6 | const expected = 'ZmVsbGlwZS51c2VyOjEyMzIxMw==' 7 | expect( actual ).toBe( expected) 8 | }) 9 | 10 | test(' fellipe.user:33333 must NOT return ZmVsbGlwZS51c2VyOjEyMzIxMw==', () => { 11 | const actual = auth('fellipe.user', '33333') 12 | const expected = 'ZmVsbGlwZS51c2VyOjEyMzIxMw==' 13 | expect( actual ).not.toBe( expected) 14 | }) 15 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jira-score [![Build Status](https://travis-ci.org/delete/jira-score.svg?branch=master)](https://travis-ci.org/delete/jira-score) ## Configuration Before run the script, you must create a file in the root project folder. Create a **.env** file with the following data: ``` { "login": "", "pass": "", "domain": "", "goalDSN": "", "goalQLD": "", "pointsHourDSN": "", "pointsHourQLD": "", "workdays": "", "startDate": "2017-08-01", "endDate": "2017-08-31", "dsn": [ "person1", "person2" ], "qld": [ "person3", "person4" ], "admins": [ "id1", "id2" ] } ``` ## Install `npm install` ## Running `npm start` -------------------------------------------------------------------------------- /slackbot/response/patterns.js: -------------------------------------------------------------------------------- 1 | const GOOD_MORNING = /.*bom.*dia.*/i 2 | const GOOD_AFTERNOON = /good afternoon/ 3 | const HELLO = /.*(eai|olar|oi|colé|coe).*/i 4 | const HELP = /.*(help|ajuda|socorro|socorre).*/i 5 | const MY_SELF = /.*(jira|malcriado).*/ 6 | const POINTS = /pontos|ponto|score/i 7 | const ISSUES = /issues|issue|tarefas/i 8 | const LOGIN = /^(entrar|entar|entra|logar|login|vai) .*(dsn|qld).cir$/i 9 | const GOAL = /^meta/i 10 | const TOP10 = /top10|topten/i 11 | const PLAYER = /^(filhote|player|jogador) .*(dsn|qld).cir$/i 12 | 13 | module.exports = { 14 | GOOD_MORNING, 15 | GOOD_AFTERNOON, 16 | HELLO, 17 | HELP, 18 | MY_SELF, 19 | POINTS, 20 | ISSUES, 21 | LOGIN, 22 | GOAL, 23 | TOP10, 24 | PLAYER 25 | } -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | const Datastore = require('nedb') 2 | const db = new Datastore({ filename: './datafile', autoload: true }); 3 | 4 | const save = ( object, callback ) => db.insert( object, callback ) 5 | const find = ( query, callback ) => db.find( query, callback ) 6 | const findOne = ( query, callback ) => db.findOne( query, callback ) 7 | const remove = ( query, callback ) => db.remove( query, {}, callback ) 8 | const update = ( query, update, options, callback ) => db.update( query, update, options, callback ) 9 | const count = ( query, callback ) => db.count( query, callback ) 10 | 11 | module.exports = { 12 | crud: { 13 | save, 14 | find, 15 | findOne, 16 | remove, 17 | update, 18 | count, 19 | db 20 | } 21 | } -------------------------------------------------------------------------------- /src/query.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const makeQuery = ( user, startDate, endDate ) => { 4 | 5 | const slower = () => 6 | `?jql=category = Cirrus AND issuetype not in (Epic) AND status changed to (Pronto, Finalizado, "Finalizado / Liberado") during (${startDate}, "${endDate} 23:59") AND status was not in (Pronto, Finalizado, "Finalizado / Liberado") before ${startDate} AND assignee in (${user})` 7 | 8 | const faster = () => 9 | `?jql=category = Cirrus AND "Dificuldade de Implementação" is not EMPTY AND resolution = Resolvido AND assignee = ${user} AND resolved >= ${startDate} AND resolved <= "${endDate} 23:59" ORDER BY cf[17132] ASC, resolved DESC` 10 | 11 | return { 12 | faster, 13 | slower 14 | } 15 | } 16 | 17 | module.exports = makeQuery -------------------------------------------------------------------------------- /slackbot/response/callbacks.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../eventBus') 2 | 3 | const goodMorning = ( message ) => emitter.emit( 'GOOD_MORNING' , message ) 4 | const hello = ( message ) => emitter.emit( 'HELLO', message ) 5 | const help = ( message ) => emitter.emit( 'HELP', message ) 6 | const mySelf = ( message ) => emitter.emit( 'MY_SELF', message ) 7 | const loadIssues = ( message, event ) => emitter.emit( 'LOAD_ISSUES', message, event ) 8 | const login = ( message ) => emitter.emit( 'LOGIN', message ) 9 | const top10 = ( message ) => emitter.emit( 'TOP10', message ) 10 | const player = ( message ) => emitter.emit( 'PLAYER', message ) 11 | 12 | module.exports = { 13 | goodMorning, 14 | hello, 15 | help, 16 | mySelf, 17 | loadIssues, 18 | login, 19 | top10, 20 | player 21 | } -------------------------------------------------------------------------------- /slackbot/middlewares/load-issues.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const emitter = require('../eventBus') 3 | const { crud } = require('../../src/db') 4 | 5 | const loginIsNeeded = ( channel ) => emitter.emit('SEND', messages('USER_NEEDED'), channel ) 6 | 7 | const loadIssues = ( message, event ) => { 8 | 9 | const { user , channel } = message 10 | const player = { slackId: user } 11 | const month = 'aug' 12 | 13 | // Must reload the database to get the new values 14 | crud.db.loadDatabase(function (err) { 15 | if ( err ) throw err 16 | 17 | crud.findOne( player, (err, doc) => 18 | doc ? emitter.emit(event, message, doc ) : loginIsNeeded( channel) ) 19 | }); 20 | } 21 | 22 | emitter.on( 'LOAD_ISSUES', loadIssues ) 23 | console.log('middleware LOAD_ISSUES') -------------------------------------------------------------------------------- /src/configs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { loadFileSync } = require('./utils') 4 | const urlProd = require('./url') 5 | 6 | const isDev = process.env.ENV_DEV == 'true' 7 | const filename = isDev ? './tests/fixtures/env_test' : './.env' 8 | const config = JSON.parse( loadFileSync( filename, "utf8") ) 9 | 10 | const urlDev = () => config.domain 11 | 12 | const url = ( startDate, endDate, user=config.login ) => 13 | isDev ? urlDev() : urlProd( config.domain, startDate, endDate, user ) 14 | 15 | const workdays = () => parseInt(config.workdays) 16 | 17 | const dsn = config.dsn 18 | const qld = config.qld 19 | const admins = config.admins 20 | const startDate = () => config.startDate 21 | const endDate = () => config.endDate 22 | 23 | module.exports = { 24 | url, 25 | workdays, 26 | isDev, 27 | startDate, 28 | endDate, 29 | config, 30 | admins 31 | } 32 | -------------------------------------------------------------------------------- /tests/src/url.test.js: -------------------------------------------------------------------------------- 1 | process.env.ENV_DEV = 'true' 2 | 3 | const url = require('../../src/url') 4 | 5 | test('Must return the hole query with the right inputs"', () => { 6 | const user = 'iamanuser' 7 | const startDate = '2017-06-01' 8 | const endDate = '2017-06-30' 9 | const domain = '127.0.0.1:8080' 10 | 11 | const result = url( domain, startDate, endDate, user ) 12 | 13 | const expected = `http://${domain}/rest/api/2/search?jql=category%20=%20Cirrus%20AND%20issuetype%20not%20in%20(Epic)%20AND%20status%20changed%20to%20(Pronto,%20Finalizado,%20%22Finalizado%20/%20Liberado%22)%20during%20(${startDate},%20%22${endDate}%2023:59%22)%20%20AND%20status%20was%20not%20in%20(Pronto,%20Finalizado,%20%22Finalizado%20/%20Liberado%22)%20before%20${startDate}%20AND%20assignee%20in%20(${user})&fields=assignee,project,customfield_21711,issuetype,timespent,customfield_17132&maxResults=200` 14 | 15 | expect(result).toBe(expected) 16 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fellipe Pinheiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/src/configs_dev.test.js: -------------------------------------------------------------------------------- 1 | process.env.ENV_DEV = 'true' 2 | const configs = require('../../src/configs') 3 | 4 | describe('Test with ENV_DEV variable', () => { 5 | test('isDev method must return true', () => { 6 | const actual = configs.isDev 7 | const expected = true 8 | expect( actual ).toBe( expected) 9 | }) 10 | 11 | test('Configs must return an literal object', () => { 12 | expect( typeof configs ).toBe( 'object' ) 13 | }) 14 | 15 | test('url method must return http://127.0.0.1:8080/issues.json to dev environment', () => { 16 | const actual = configs.url() 17 | const expected = "http://127.0.0.1:8080/issues.json" 18 | expect( actual ).toBe( expected) 19 | }) 20 | 21 | test('start date must return 2017-08-01', () => { 22 | const actual = configs.startDate() 23 | const expected = '2017-08-01' 24 | expect( actual ).toBe( expected ) 25 | }) 26 | 27 | test('end date must return 2017-08-31', () => { 28 | const actual = configs.endDate() 29 | const expected = '2017-08-31' 30 | expect( actual ).toBe( expected ) 31 | }) 32 | }) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | .idea/ 61 | 62 | datafile 63 | -------------------------------------------------------------------------------- /slackbot/messages/index.js: -------------------------------------------------------------------------------- 1 | const types = require('./types') 2 | 3 | const random = ( array ) => array[ Math.floor( Math.random() * array.length ) ] 4 | 5 | const messages = ( t ) => { 6 | const ts = { 7 | DONT_GET_IT: () => random( types[t] ), 8 | ERROR_BOT: () => random( types[t] ), 9 | ERROR_JIRA: () => random( types[t] ), 10 | GOOD_MORNING: () => random( types[t] ), 11 | USER_FOUND: () => random( types[t] ), 12 | WELCOME: () => random( types[t] ), 13 | USER_NEEDED: () => random( types[t] ), 14 | ONE_THIRD: () => random( types[t] ), 15 | LESS_HALF: () => random( types[t] ), 16 | MORE_HALF: () => random( types[t] ), 17 | MY_SELF: () => random( types[t] ), 18 | HELLO: () => random( types[t] ), 19 | HELP: () => random( types[t] ), 20 | LOADING: () => random( types[t] ), 21 | COMPLETED: () => random( types[t] ), 22 | NOT_ADMIN: () => random( types[t] ), 23 | USER_NOT_FOUND: () => random( types[t] ), 24 | default: () => new Error('Message not found') 25 | } 26 | return (ts[ t ] || ts[ 'default' ])() 27 | } 28 | 29 | module.exports = messages -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | 5 | const toBase64 = ( string ) => Buffer.from(string).toString('base64') 6 | const loadFile = ( name, callback ) => fs.readFile(name, "utf8", callback) 7 | const loadFileSync = ( name ) => fs.readFileSync(name, "utf8") 8 | 9 | const splitStringAndReturnLast = ( rawString ) => { 10 | const stringSplitted = rawString.split('-') 11 | const lastIndex = stringSplitted.length - 1 12 | return stringSplitted[ lastIndex ].trim() 13 | } 14 | 15 | const isWeekend = ( weekDay ) => ( weekDay === 0 || weekDay === 6 ) 16 | // TODO make this function more functional, using reduce maybe? 17 | const getWorkingDays = ( startDate, endDate ) => { 18 | let result = 0; 19 | 20 | let currentDate = startDate 21 | while ( currentDate <= endDate ) { 22 | const weekDay = currentDate.getDay(); 23 | 24 | if ( !isWeekend( weekDay) ) result++; 25 | 26 | currentDate.setDate( currentDate.getDate() + 1 ); 27 | } 28 | return result; 29 | } 30 | 31 | module.exports = { 32 | toBase64, 33 | loadFile, 34 | loadFileSync, 35 | splitStringAndReturnLast, 36 | getWorkingDays 37 | } -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { dsn, qld } = require('./configs') 4 | const { crud } = require('./db') 5 | 6 | const monthsName = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' ] 7 | const months = {} 8 | 9 | monthsName.forEach( month => 10 | ( 11 | months[ month ] = { 12 | issues: [], // list of issues objects 13 | goal: 0, 14 | workdays: 0, 15 | pointsPerHour: 0 16 | } 17 | ) 18 | ) 19 | 20 | const saveUser = username => { 21 | const player = { 22 | username, // username da empresa 23 | months, 24 | slackId: username, 25 | channel: '', // canal do slack 26 | updated: new Date(), //timestamp da ultima atualização no jira 27 | isAdmin: false // bool 28 | } 29 | 30 | crud.save( player, (err, newDoc) => { 31 | if ( err ) throw err 32 | 33 | console.log(`${newDoc.username} created!`) 34 | } ) 35 | } 36 | 37 | const players = [ ...dsn, ...qld ] 38 | players.map( saveUser ) 39 | 40 | crud.count( {}, (err, count) => count ? console.log(`\n\n${count} people was saved!`) : console.log('not found') ) -------------------------------------------------------------------------------- /src/pontuations.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const getDifficulty = ( difficulty ) => { 4 | const dificulties = { 5 | 'Sem Pontuação': () => ({'points': 0, 'slug': 'NP'}), 6 | 'Não classificado': () => ({'points': 30, 'slug': 'NC'}), 7 | 'Muito simples': () => ({'points': 30, 'slug': 'VS'}), 8 | 'Simples': () => ({'points': 75, 'slug': 'S'}), 9 | 'Média': () => ({'points': 160, 'slug': 'M'}), 10 | 'Difícil': () => ({'points': 320, 'slug': 'H'}), 11 | 'Muito difícil': () => ({'points': 560, 'slug': 'VH'}), 12 | } 13 | return (dificulties[ difficulty ] || dificulties['Não classificado'])() 14 | } 15 | 16 | const isClassified = ( type ) => { 17 | const types = { 18 | 'Programação': () => true, 19 | 'Teste': () => true, 20 | 'Manual / Documentação': () => true, 21 | 'Liberação de Versão Web': () => true, 22 | 'Liberação de Versão': () => true, 23 | 'Tarefa': () => true, 24 | 'Melhoria': () => true, 25 | 'Erro': () => true, 26 | 'default': () => false 27 | } 28 | return (types[ type ] || types[ 'default' ])() 29 | } 30 | 31 | module.exports = { 32 | getDifficulty, 33 | isClassified 34 | } -------------------------------------------------------------------------------- /tests/slackbot/commands/help.test.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../../../slackbot/eventBus') 2 | const { HELP } = require('../../../slackbot/messages/types') 3 | 4 | const help = require('../../../slackbot/commands/help') 5 | 6 | describe('Help command Must emit an event and data object', () => { 7 | 8 | describe('help command', () => { 9 | it('should emit an "SEND"', ( ) => { 10 | const eventSpy = jest.fn() 11 | emitter.on('SEND', eventSpy ) 12 | 13 | help( {} ) 14 | 15 | expect(eventSpy).toBeCalled() 16 | }) 17 | 18 | it('should emit an "SEND" wit some message and data', ( ) => { 19 | const data = { 20 | text: 'help me', 21 | channel: 'aaa', 22 | user: '1234' 23 | } 24 | const eventSpy = jest.fn() 25 | emitter.on('SEND', eventSpy ) 26 | 27 | help( data ) 28 | 29 | expect(eventSpy).toBeCalled() 30 | 31 | // Both params are passing as only one, the first 32 | const [ actualMessage, channel ] = eventSpy.mock.calls[0] 33 | 34 | expect( HELP ).toContain( actualMessage ); 35 | expect( channel ).toBe( data.channel ); 36 | }) 37 | }) 38 | }) -------------------------------------------------------------------------------- /src/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { isClassified } = require('./pontuations') 4 | 5 | const countIssuesByType = ( issues, type ) => 6 | issues.reduce( (total, issue ) => issue.type === type ? total + 1 : total, 0 ) 7 | 8 | const countIssuesByDifficulty = ( issues, difficulty ) => 9 | issues.reduce( (total, issue ) => issue.difficulty === difficulty && isClassified(issue.type) ? total + 1 : total, 0 ) 10 | 11 | const sumPontuation = ( issues ) => 12 | issues.reduce( (total, issue ) => isClassified(issue.type) ? total + issue.pontuation : total, 0 ) 13 | 14 | const sumTime = ( issues, type ) => 15 | issues.reduce( (total, issue ) => issue.type === type ? total + issue.time : total, 0 ) 16 | 17 | const minutesToPoints = ( minutes, pointsPerHour ) => Math.round( ( minutes * (pointsPerHour / 60) ) ) 18 | const pointsPercentage = ( goal, points ) => ( (points * 100) / goal ).toFixed(2); 19 | const hasScore = ( issue ) => issue.pontuation > 0 && isClassified(issue.type) 20 | const scoredIssues = ( issues ) => issues.filter( issue => hasScore( issue ) ) 21 | 22 | module.exports = { 23 | countIssuesByType, 24 | countIssuesByDifficulty, 25 | sumPontuation, 26 | sumTime, 27 | hasScore, 28 | scoredIssues, 29 | minutesToPoints, 30 | pointsPercentage 31 | } -------------------------------------------------------------------------------- /tests/slackbot/commands/good-morning.test.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../../../slackbot/eventBus') 2 | const { GOOD_MORNING } = require('../../../slackbot/messages/types') 3 | 4 | const goodMorning = require('../../../slackbot/commands/good-morning') 5 | 6 | describe('Must emit an event and data object', () => { 7 | 8 | describe('Good morning callback', () => { 9 | it('should emit an "SEND"', ( ) => { 10 | const eventSpy = jest.fn() 11 | emitter.on('SEND', eventSpy ) 12 | 13 | goodMorning( {} ) 14 | 15 | expect(eventSpy).toBeCalled() 16 | }) 17 | 18 | it('should emit an "SEND" wit some message and data', ( ) => { 19 | const data = { 20 | text: 'good morning', 21 | channel: 'aaa' 22 | } 23 | const eventSpy = jest.fn() 24 | emitter.on('SEND', eventSpy ) 25 | 26 | goodMorning( data ) 27 | 28 | expect(eventSpy).toBeCalled() 29 | 30 | // Both params are passing as only one, the first 31 | const [ actualMessage, channel ] = eventSpy.mock.calls[0] 32 | expect( GOOD_MORNING ).toContain( actualMessage ); 33 | expect( channel ).toBe( data.channel ); 34 | }) 35 | }) 36 | }) -------------------------------------------------------------------------------- /tests/slackbot/commands/my-self.test.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../../../slackbot/eventBus') 2 | const { MY_SELF } = require('../../../slackbot/messages/types') 3 | 4 | const mySelf = require('../../../slackbot/commands/my-self') 5 | 6 | describe('MySelf command Must emit an event and data object', () => { 7 | 8 | describe('mySelf command', () => { 9 | it('should emit an "SEND"', ( ) => { 10 | const eventSpy = jest.fn() 11 | emitter.on('SEND', eventSpy ) 12 | 13 | mySelf( {} ) 14 | 15 | expect(eventSpy).toBeCalled() 16 | }) 17 | 18 | it('should emit an "SEND" wit some message and data', ( ) => { 19 | const data = { 20 | text: 'jira', 21 | channel: 'aaa', 22 | user: '1234' 23 | } 24 | const eventSpy = jest.fn() 25 | emitter.on('SEND', eventSpy ) 26 | 27 | mySelf( data ) 28 | 29 | expect(eventSpy).toBeCalled() 30 | 31 | // Both params are passing as only one, the first 32 | const [ actualMessage, channel ] = eventSpy.mock.calls[0] 33 | 34 | expect( MY_SELF ).toContain( actualMessage ); 35 | expect( channel ).toBe( data.channel ); 36 | }) 37 | }) 38 | }) -------------------------------------------------------------------------------- /tests/slackbot/commands/hello.test.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../../../slackbot/eventBus') 2 | const { HELLO } = require('../../../slackbot/messages/types') 3 | 4 | const hello = require('../../../slackbot/commands/hello') 5 | 6 | describe('Hello command Must emit an event and data object', () => { 7 | 8 | describe('Hello command', () => { 9 | it('should emit an "SEND"', ( ) => { 10 | const eventSpy = jest.fn() 11 | emitter.on('SEND', eventSpy ) 12 | 13 | hello( {} ) 14 | 15 | expect(eventSpy).toBeCalled() 16 | }) 17 | 18 | it('should emit an "SEND" wit some message and data', ( ) => { 19 | const data = { 20 | text: 'olar', 21 | channel: 'aaa', 22 | user: '1234' 23 | } 24 | const eventSpy = jest.fn() 25 | emitter.on('SEND', eventSpy ) 26 | 27 | hello( data ) 28 | 29 | expect(eventSpy).toBeCalled() 30 | 31 | // Both params are passing as only one, the first 32 | const [ actualMessage, channel ] = eventSpy.mock.calls[0] 33 | 34 | // Mock mensagens with the same user as data object 35 | const helloMessages = HELLO.map( (h) => h( data.user ) ) 36 | 37 | expect( helloMessages ).toContain( actualMessage ); 38 | expect( channel ).toBe( data.channel ); 39 | }) 40 | }) 41 | }) -------------------------------------------------------------------------------- /tests/src/configs.test.js: -------------------------------------------------------------------------------- 1 | const { loadFile } = require('../../src/utils') 2 | const ENV_FILE_DEV = './tests/fixtures/env_test' 3 | 4 | 5 | describe('Test configs file', () => { 6 | test('File must have the expected data', done => { 7 | 8 | loadFile(ENV_FILE_DEV, (err, data) => { 9 | if (err) throw err 10 | 11 | const dataObj = JSON.parse(data) 12 | const fields = Object.keys(dataObj) 13 | 14 | const expectedValues = { 15 | login: "fellipe.user", 16 | pass: "123213", 17 | domain: "http://127.0.0.1:8080/issues.json", 18 | goalDSN: "3528", 19 | goalQLD: "2500", 20 | pointsHourDSN: "21", 21 | pointsHourQLD: "16", 22 | workdays: "21", 23 | startDate: "2017-08-01", 24 | endDate: "2017-08-31", 25 | dsn: [ 26 | "person1", 27 | "person2" 28 | ], 29 | qld: [ 30 | "person3", 31 | "person4" 32 | ], 33 | admins: [ 34 | "person1" 35 | ] 36 | } 37 | 38 | fields.map( field => expect( dataObj[field] ).toEqual( expectedValues[field] ) ) 39 | 40 | done() 41 | }) 42 | }) 43 | }) 44 | 45 | -------------------------------------------------------------------------------- /tests/fixtures/issues.js: -------------------------------------------------------------------------------- 1 | const issues = [ 2 | { 3 | key: 'TEST-1', 4 | type: 'Programação', 5 | time: 600, 6 | difficulty: 'Muito simples', 7 | pontuation: 30 8 | }, 9 | { 10 | key: 'TEST-2', 11 | type: 'Teste', 12 | time: 300, 13 | difficulty: 'Muito simples', 14 | pontuation: 30 15 | }, 16 | { 17 | key: 'TEST-3', 18 | type: 'Tarefa', 19 | time: 300, 20 | difficulty: 'Não classificado', 21 | pontuation: 0 22 | }, 23 | { 24 | key: 'TEST-4', 25 | type: 'Programação', 26 | time: 360, 27 | difficulty: 'Simples', 28 | pontuation: 75 29 | }, 30 | { 31 | key: 'TEST-5', 32 | type: 'Atendimento', 33 | time: 600, 34 | difficulty: 'Não classificado', 35 | pontuation: 0 36 | }, 37 | { 38 | key: 'TEST-6', 39 | type: 'Liberação de Versão', 40 | time: 0, 41 | difficulty: 'Muito simples', 42 | pontuation: 30 43 | }, 44 | { 45 | key: 'TEST-7', 46 | type: 'Liberação de Versão Web', 47 | time: 0, 48 | difficulty: 'Simples', 49 | pontuation: 75 50 | }, 51 | { 52 | key: 'TEST-8', 53 | type: 'Manual / Documentação', 54 | time: 0, 55 | difficulty: 'Difícil', 56 | pontuation: 320 57 | } 58 | ] 59 | 60 | module.exports = issues -------------------------------------------------------------------------------- /tests/src/query.test.js: -------------------------------------------------------------------------------- 1 | const makeQuery = require('../../src/query') 2 | 3 | describe('Function: faster query', () => { 4 | test('Must return the hole query with the right inputs"', () => { 5 | const user = 'iamanuser' 6 | const startDate = '2017-06-01' 7 | const endDate = '2017-06-30' 8 | 9 | const query = makeQuery( user, startDate, endDate ) 10 | const result = query.faster() 11 | 12 | const expected = '?jql=category = Cirrus AND "Dificuldade de Implementação" is not EMPTY AND resolution = Resolvido AND assignee = iamanuser AND resolved >= 2017-06-01 AND resolved <= "2017-06-30 23:59" ORDER BY cf[17132] ASC, resolved DESC' 13 | 14 | expect(result).toBe(expected) 15 | }) 16 | }) 17 | 18 | describe('Function: slower query', () => { 19 | test('Must return the hole query with the right inputs"', () => { 20 | const user = 'iamanuser' 21 | const startDate = '2017-06-01' 22 | const endDate = '2017-06-30' 23 | 24 | const query = makeQuery( user, startDate, endDate ) 25 | const result = query.slower() 26 | 27 | const expected = `?jql=category = Cirrus AND issuetype not in (Epic) AND status changed to (Pronto, Finalizado, "Finalizado / Liberado") during (${startDate}, "${endDate} 23:59") AND status was not in (Pronto, Finalizado, "Finalizado / Liberado") before ${startDate} AND assignee in (${user})` 28 | 29 | expect(result).toBe(expected) 30 | }) 31 | }) -------------------------------------------------------------------------------- /slackbot/auth.js: -------------------------------------------------------------------------------- 1 | const messages = require('./messages') 2 | const emitter = require('./eventBus') 3 | const { crud } = require('./../src/db') 4 | const { admins } = require('./../src/configs') 5 | 6 | const isAdmin = ( user ) => admins.includes( user ) 7 | const sendMessage = ( message, channel ) => emitter.emit('SEND', message, channel ) 8 | const userFound = ( channel ) => sendMessage( messages('USER_FOUND'), channel) 9 | const welcome = ( channel ) => sendMessage( messages('WELCOME'), channel) 10 | const getUsername = string => string.split(' ')[1] 11 | 12 | const login = ( message ) => { 13 | const { text, channel, user} = message 14 | const username = getUsername( text ) 15 | const playerbyId = { slackId: user } 16 | 17 | crud.findOne( playerbyId, (err, doc) => { 18 | if ( err ) throw err 19 | 20 | if ( doc ) { 21 | userFound( channel ) 22 | return 23 | } 24 | 25 | const player = { username: username } 26 | crud.findOne( player, (err, doc) => { 27 | if ( doc ) { 28 | crud.update(player, { $set: { slackId: user } }, {} ) 29 | crud.update(player, { $set: { channel: channel } }, {} ) 30 | crud.update(player, { $set: { isAdmin: isAdmin( user ) } }, {} ) 31 | welcome( channel ) 32 | console.log(`${doc.username} logged!`) 33 | } 34 | }) 35 | }) 36 | } 37 | 38 | module.exports = { 39 | isAdmin, 40 | login 41 | } -------------------------------------------------------------------------------- /slackbot/commands/player.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const emitter = require('../eventBus') 3 | const { crud } = require('../../src/db') 4 | const { isAdmin } = require('../auth') 5 | 6 | const notAlowed = ( channel ) => emitter.emit('SEND', messages('NOT_ADMIN'), channel ) 7 | const userNotFound = ( channel ) => emitter.emit('SEND', messages('USER_NOT_FOUND'), channel ) 8 | const extractRegexPattern = ( text, pattern ) => text.match( pattern ) 9 | 10 | const userPattern = /\s.*(dsn|qld).cir/i 11 | 12 | const getParam = text => { 13 | const paramText = extractRegexPattern( text, userPattern ) 14 | 15 | return { 16 | player: paramText ? paramText[0].trim() : null 17 | } 18 | } 19 | 20 | const emitters = ( message, player ) => { 21 | emitter.emit( 'SCORE', message, player ) 22 | emitter.emit( 'ISSUES', message, player ) 23 | emitter.emit( 'GOAL', message, player ) 24 | } 25 | 26 | const playerDetails = message => { 27 | const { user , channel, text } = message 28 | const month = 'aug' 29 | 30 | if ( !isAdmin( user ) ) { 31 | notAlowed( channel ) 32 | return 33 | } 34 | 35 | const { player } = getParam( text ) 36 | 37 | // Must reload the database to get the new values 38 | crud.db.loadDatabase(function (err) { 39 | if ( err ) throw err 40 | 41 | crud.findOne( { username: player }, (err, doc) => 42 | doc ?emitters( message, doc ) : userNotFound( channel) ) 43 | }); 44 | } 45 | 46 | console.log('on PLAYER') 47 | module.exports = playerDetails -------------------------------------------------------------------------------- /tests/src/utils.test.js: -------------------------------------------------------------------------------- 1 | const utils = require('../../src/utils') 2 | 3 | describe('Function: toBase64', () => { 4 | test('base4 of admin:pass must be YWRtaW46cGFzcw==', () => { 5 | const actual = 'admin:pass' 6 | const expected = 'YWRtaW46cGFzcw==' 7 | 8 | const result = utils.toBase64(actual) 9 | expect(result).toBe(expected) 10 | }) 11 | }) 12 | 13 | describe('Function: splitStringAndReturnLast', () => { 14 | test('Must return te string after te "-" char', () => { 15 | const actual = '0 - Não Classificado' 16 | const expected = 'Não Classificado' 17 | 18 | const result = utils.splitStringAndReturnLast(actual) 19 | expect(result).toBe(expected) 20 | }) 21 | 22 | test('Must return empty if a empty string is given', () => { 23 | const actual = '' 24 | const expected = '' 25 | 26 | const result = utils.splitStringAndReturnLast(actual) 27 | expect(result).toBe(expected) 28 | }) 29 | }) 30 | 31 | describe('Function: getWorkingDays', () => { 32 | test('Must return 12 days to July 2017', () => { 33 | const july = 7 - 1 // must substract 1 34 | const firstDay = 1 35 | const lastDay = 31 36 | const year = 2017 37 | 38 | const begin = new Date(year, july, firstDay) 39 | const end = new Date(year, july, lastDay) 40 | 41 | const expected = 21 42 | 43 | const result = utils.getWorkingDays( begin, end ) 44 | expect(result).toBe(expected) 45 | }) 46 | }) -------------------------------------------------------------------------------- /tests/slackbot/commands/goal.test.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../../../slackbot/eventBus') 2 | const player = require('../../fixtures/player') 3 | const goal = require('../../../slackbot/commands/goal') 4 | 5 | describe('Score command Must emit an event and data object', () => { 6 | 7 | describe('goal command', () => { 8 | it('should emit an "SEND"', ( ) => { 9 | const eventSpy = jest.fn() 10 | emitter.on('SEND', eventSpy ) 11 | 12 | const message = {} 13 | goal( message, player ) 14 | 15 | expect(eventSpy).toBeCalled() 16 | }) 17 | 18 | it('should emit an "SEND" wit some message and channel', ( ) => { 19 | // Must be improved, passing the month as parameter to goal function 20 | const message = { 21 | text: 'help me', 22 | channel: 'aaa', 23 | user: '1234' 24 | } 25 | const expectedResponse = [ 26 | /.*\*158\/dia.*/, 27 | /.*já com abono de 210 pontos de atendimento.*/ 28 | ] 29 | 30 | const eventSpy = jest.fn() 31 | emitter.on('SEND', eventSpy ) 32 | 33 | goal( message, player ) 34 | 35 | expect(eventSpy).toBeCalled() 36 | 37 | // Both params are passing as only one, the first 38 | const [ actualResponse, channel ] = eventSpy.mock.calls[0] 39 | 40 | expectedResponse.map( (response) => expect( actualResponse ).toMatch( response ) ) 41 | expect( channel ).toBe( message.channel ); 42 | }) 43 | }) 44 | }) -------------------------------------------------------------------------------- /slackbot/commands/goal.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const emitter = require('../eventBus') 3 | 4 | const { getWorkingDays } = require('../../src/utils') 5 | const { sumPontuation, sumTime, minutesToPoints } = require('../../src/filters') 6 | 7 | const myGoal = ( message, player ) => { 8 | // Must be improved, passing the month as parameter to goal function 9 | const monthNum = 8 10 | const month = 'aug' 11 | const { username } = player 12 | const { issues, pointsPerHour , workdays, goal } = player.months[month] 13 | const { channel } = message 14 | 15 | 16 | const costumerServiceTime = sumTime( issues, 'Atendimento' ) 17 | const timeInPoints = minutesToPoints( costumerServiceTime, pointsPerHour ) 18 | 19 | const objectiveMinusCostumerService = ( goal - timeInPoints ) 20 | 21 | const pointsPerDay = ( Math.round( objectiveMinusCostumerService / workdays ) ) 22 | 23 | const currentDate = new Date() 24 | const startDate = new Date(currentDate.getFullYear(), monthNum - 1, 1) // must subsctract 1 25 | const workingDays = getWorkingDays( startDate, currentDate ) 26 | 27 | const issuesPontuation = sumPontuation( issues ) 28 | const mypointsPerDay = Math.round( issuesPontuation / workingDays ) 29 | 30 | const response = [ 31 | `A pontuação diária desejada é de *${pointsPerDay < 0 ? 0 : pointsPerDay}/dia* (já com abono de ${timeInPoints} pontos de atendimento)`, 32 | `Já se passaram *${workingDays}/${workdays} dias* e a sua pontuação diária está sendo de *${mypointsPerDay}/dia*.` 33 | ].join('\n') 34 | 35 | emitter.emit('SEND', response, channel ) 36 | } 37 | 38 | console.log('on GOAL') 39 | module.exports = myGoal -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { getDifficulty, isClassified } = require('./pontuations') 4 | const formatDificultyString = require('./utils').splitStringAndReturnLast 5 | const { canClassify } = require('./filters') 6 | 7 | const getIssueKey = ( element ) => element.key 8 | const getIssueDificulty = ( element ) => element.fields.customfield_17132 ? element.fields.customfield_17132.value : '0 - Sem Pontuação' 9 | const getIssueType = ( element ) => element.fields.customfield_21711 10 | const getCustomerServiceTime = ( element ) => element.fields.timespent 11 | 12 | const getIssueInfo = ( obj ) => { 13 | const customerServiceTime = getCustomerServiceTime( obj ) 14 | const difficulty = getIssueDificulty( obj ) 15 | return { 16 | key: getIssueKey( obj ), 17 | type: getIssueType( obj ), 18 | time: ( customerServiceTime / 60 ), 19 | difficulty: formatDificultyString( difficulty ) 20 | } 21 | } 22 | 23 | const hasLog = time => time > 0 24 | const alreadyHasPoint = difficulty => ( (difficulty !== 'Não classificado') && (difficulty !== 'Sem Pontuação') ) 25 | 26 | module.exports = ( body ) => { 27 | if ( !body.issues ) throw new Error('Request error!') 28 | 29 | return body.issues.map( issue => { 30 | const newIssue = getIssueInfo( issue ) 31 | 32 | if ( alreadyHasPoint(newIssue.difficulty) || ( isClassified(newIssue.type) && hasLog(newIssue.time) ) ) { 33 | const issueScored = getDifficulty( newIssue.difficulty ) 34 | newIssue.pontuation = issueScored.points 35 | } else { 36 | newIssue.difficulty = formatDificultyString( '0 - Sem Pontuação' ) 37 | newIssue.pontuation = 0 38 | } 39 | return newIssue 40 | }) 41 | } -------------------------------------------------------------------------------- /tests/slackbot/commands/issues.test.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../../../slackbot/eventBus') 2 | const player = require('../../fixtures/player') 3 | const issuesCommand = require('../../../slackbot/commands/issues') 4 | 5 | describe('Score command Must emit an event and data object', () => { 6 | 7 | describe('issues command', () => { 8 | it('should emit an "SEND"', ( ) => { 9 | const eventSpy = jest.fn() 10 | emitter.on('SEND', eventSpy ) 11 | 12 | const message = {} 13 | issuesCommand( message, player ) 14 | 15 | expect(eventSpy).toBeCalled() 16 | }) 17 | 18 | it('should emit an "SEND" wit some message and data', ( ) => { 19 | const message = { 20 | text: 'help me', 21 | channel: 'aaa', 22 | user: '123465' 23 | } 24 | const expectedResponse = [ 25 | /.*Muito simples: \*3\*.*/, 26 | /.*Simples: \*2\*.*/, 27 | /.*Muito simples: \*3\*.*/, 28 | /.*Médias: \*0\*.*/, 29 | /.*Difíceis: \*1\*.*/, 30 | /.*Muito díficeis: \*0\*.*/, 31 | /.*Atendimento: \*1\*.*/, 32 | /.*Atendimento: \*600 minutos\* - 210 pontos.*/, 33 | /feitas: \*8\*/ 34 | ] 35 | 36 | const eventSpy = jest.fn() 37 | emitter.on('SEND', eventSpy ) 38 | 39 | issuesCommand( message, player ) 40 | 41 | expect(eventSpy).toBeCalled() 42 | 43 | // Both params are passing as only one, the first 44 | const [ actualResponse, channel ] = eventSpy.mock.calls[0] 45 | 46 | expectedResponse.map( (response) => expect( actualResponse ).toMatch( response ) ) 47 | expect( channel ).toBe( message.channel ); 48 | }) 49 | }) 50 | }) -------------------------------------------------------------------------------- /slackbot/commands/score.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const emitter = require('../eventBus') 3 | 4 | const { 5 | sumPontuation, 6 | sumTime, 7 | minutesToPoints, 8 | pointsPercentage 9 | } = require('../../src/filters') 10 | 11 | const lessThanOneThird = ( value ) => value <= 33 12 | const lessThanHalf = ( value ) => value <= 50 13 | const lessThanHundred = ( value ) => value < 100 14 | const trollMessage = ( percentage ) => 15 | lessThanOneThird( percentage ) 16 | ? messages('ONE_THIRD') 17 | : lessThanHalf( percentage ) 18 | ? messages('LESS_HALF') 19 | : lessThanHundred( percentage) 20 | ? messages('MORE_HALF') 21 | : messages('COMPLETED') 22 | 23 | const score = ( message, player ) => { 24 | const month = 'aug' 25 | const { username } = player 26 | const { issues, pointsPerHour, goal } = player.months[month] 27 | 28 | const issuesPontuation = sumPontuation( issues ) 29 | const costumerServiceTime = sumTime( issues, 'Atendimento' ) 30 | 31 | const timeInPoints = minutesToPoints( costumerServiceTime, pointsPerHour ) 32 | const actualPercentage = pointsPercentage( goal, issuesPontuation ) 33 | const restPercentage = ( 100 - actualPercentage ).toFixed( 2 ) 34 | 35 | const rest = ( goal - issuesPontuation ) 36 | const restToGoal = rest < 0 ? 0 : rest 37 | 38 | const response = [ 39 | `Você fez *${costumerServiceTime} minutos* de atendimento, o que da *${timeInPoints}* pontos`, 40 | `Você tem *${issuesPontuation}* pontos e completou *${actualPercentage}%* da meta *${goal}* !`, 41 | `Faltam *${restToGoal}* pontos, *${restPercentage < 0 ? 0 : restPercentage}%* para bater a meta!`, 42 | `\n${trollMessage( actualPercentage )}` 43 | ].join('\n') 44 | 45 | emitter.emit( 'SEND', response, message.channel ) 46 | } 47 | console.log( 'on SCORE' ) 48 | module.exports = score -------------------------------------------------------------------------------- /slackbot/index.js: -------------------------------------------------------------------------------- 1 | const RtmClient = require('@slack/client').RtmClient 2 | const RTM_EVENTS = require('@slack/client').RTM_EVENTS 3 | const emitter = require('./eventBus') 4 | const subscribe = require('./subscribe') 5 | const messages = require('./messages') 6 | const { 7 | GOOD_MORNING, 8 | HELLO, 9 | HELP, 10 | MY_SELF, 11 | POINTS, 12 | ISSUES, 13 | LOGIN, 14 | GOAL, 15 | TOP10, 16 | PLAYER 17 | } = require('./response').patterns 18 | const { 19 | goodMorning, 20 | hello, 21 | help, 22 | mySelf, 23 | loadIssues, 24 | login, 25 | top10, 26 | player 27 | } = require('./response').callbacks 28 | // Load listeners 29 | require('./commands') 30 | require('./middlewares') 31 | 32 | const bot_token = process.env.SLACK_BOT_TOKEN || '' 33 | const rtm = new RtmClient(bot_token) 34 | 35 | const sender = ( response, channel ) => rtm.sendMessage( response, channel ) 36 | emitter.on( 'SEND', sender ) 37 | 38 | rtm.on(RTM_EVENTS.MESSAGE, message => emitter.emit( 'PRINT_COMMAND', message ) ) 39 | 40 | // General commands 41 | rtm.on(RTM_EVENTS.MESSAGE, message => { 42 | const runOn = subscribe({ message }) 43 | 44 | runOn( GOOD_MORNING, goodMorning ) 45 | runOn( HELLO, hello ) 46 | runOn( HELP, help ) 47 | runOn( MY_SELF, mySelf ) 48 | runOn( LOGIN, login ) 49 | runOn( TOP10, top10 ) 50 | runOn( PLAYER, player ) 51 | }) 52 | 53 | rtm.on(RTM_EVENTS.MESSAGE, message => { 54 | const event = 'SCORE' 55 | const runOn = subscribe({ message, event }) 56 | runOn( POINTS, loadIssues ) 57 | }) 58 | 59 | rtm.on(RTM_EVENTS.MESSAGE, message => { 60 | const event = 'ISSUES' 61 | const runOn = subscribe({ message, event }) 62 | runOn( ISSUES, loadIssues ) 63 | }) 64 | 65 | rtm.on(RTM_EVENTS.MESSAGE, message => { 66 | const event = 'GOAL' 67 | const runOn = subscribe({ message, event }) 68 | runOn( GOAL, loadIssues ) 69 | }) 70 | 71 | rtm.start() 72 | -------------------------------------------------------------------------------- /src/spider.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const get = require('./request') 3 | const { url, config, startDate, endDate, workdays } = require('./configs') 4 | const auth = require('./auth') 5 | const parser = require('./parser') 6 | const { crud } = require('./db') 7 | 8 | const isDSN = ( user ) => /.*dsn.*/.test(user) 9 | const goal = ( user='someone.dsn.cir' ) => 10 | isDSN(user) ? parseInt(config.goalDSN) : parseInt(config.goalQLD) 11 | 12 | const pointsPerHour = ( user='someone.dsn.cir' ) => 13 | isDSN(user) ? (parseInt(config.pointsHourDSN)) : (parseInt(config.pointsHourQLD)) 14 | 15 | const saveData = ( data, username ) => { 16 | const issues = parser( data ) 17 | const player = { username: username } 18 | const month = 'aug' 19 | const userGoal = goal( username ) 20 | const userPointsHour = pointsPerHour( username ) 21 | console.log(`${username} -> goal: ${userGoal} -> points: ${userPointsHour} -> issues: ${issues.length}`) 22 | 23 | // Clear issues before update the list 24 | crud.update(player, { $push: { [`months.${month}.issues`]: { $slice: 0 } } }, {}, () => { 25 | crud.update(player, { $set: { [`months.${month}.issues`]: issues } }, {} ) 26 | }) 27 | crud.update(player, { $set: { [`months.${month}.goal`]: userGoal } }, {} ) 28 | crud.update(player, { $set: { [`months.${month}.pointsPerHour`]: userPointsHour } }, {} ) 29 | crud.update(player, { $set: { [`months.${month}.workdays`]: workdays() } }, {} ) 30 | } 31 | 32 | const saveUserData = username => { 33 | const filterUrl = url( startDate(), endDate(), username ) 34 | const headers = { 'Authorization': `Basic ${auth( config.login, config.pass )}` } 35 | const options = { headers } 36 | 37 | get(filterUrl, options) 38 | .then( response => saveData(response.data, username) ) 39 | .catch( response => console.log( `Error: ${response.message}` ) ) 40 | } 41 | 42 | 43 | const players = [ ...config.dsn, ...config.qld ] 44 | players.map( saveUserData ) -------------------------------------------------------------------------------- /slackbot/commands/issues.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const emitter = require('../eventBus') 3 | 4 | const { 5 | countIssuesByType, 6 | countIssuesByDifficulty, 7 | sumTime, 8 | minutesToPoints, 9 | } = require('../../src/filters') 10 | 11 | const printIssue = issue => `${issue.key} -> ${issue.difficulty} -> ${issue.pontuation}` 12 | 13 | const issues = ( message, player ) => { 14 | const month = 'aug' 15 | const { username } = player 16 | const { issues, pointsPerHour } = player.months[month] 17 | const { channel } = message 18 | 19 | const nc = countIssuesByDifficulty( issues, 'Não classificado') 20 | const s = countIssuesByDifficulty( issues, 'Simples') 21 | const vs = countIssuesByDifficulty( issues, 'Muito simples') 22 | const m = countIssuesByDifficulty( issues, 'Média') 23 | const h = countIssuesByDifficulty( issues, 'Difícil') 24 | const vh = countIssuesByDifficulty( issues, 'Muito difícil') 25 | 26 | const cs = countIssuesByType( issues, 'Atendimento') 27 | const cst = sumTime( issues, 'Atendimento' ) 28 | 29 | const cstp = minutesToPoints( cst, pointsPerHour ) 30 | 31 | const tasks = countIssuesByType( issues, 'Tarefa') 32 | 33 | const response = [ 34 | `Issues Não Classificadas: *${nc}*`, 35 | `Issues Muito simples: *${vs}*`, 36 | `Issues Simples: *${s}*`, 37 | `Issues Médias: *${m}*`, 38 | `Issues Difíceis: *${h}*`, 39 | `Issues Muito díficeis: *${vh}*`, 40 | `\nIssues de Atendimento: *${cs}*`, 41 | `Total de tempo em issues de Atendimento: *${cst} minutos* - ${cstp} pontos`, 42 | `\nIssues do tipo Tarefa: *${tasks}*`, 43 | `\n\nTotal de issues feitas: *${issues.length}*`, 44 | ].join('\n') 45 | 46 | emitter.emit('SEND', response, channel ) 47 | 48 | const allIssuesResponse = issues.map( printIssue ).join('\n') 49 | emitter.emit('SEND', allIssuesResponse, channel ) 50 | } 51 | 52 | console.log('on ISSUES') 53 | module.exports = issues -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const get = require('./src/request') 4 | const { loadFile } = require('./src/utils') 5 | const { url, goal, pointsMinute, startDate, endDate, config } = require('./src/configs') 6 | const auth = require('./src/auth') 7 | const parser = require('./src/parser') 8 | const { 9 | countIssuesByType, 10 | countIssuesByDifficulty, 11 | sumPontuation, 12 | sumTime, 13 | scoredIssues, 14 | minutesToPoints 15 | } = require('./src/filters') 16 | 17 | const printIssue = issue => console.log( `${issue.key} -> ${issue.difficulty} -> ${issue.pontuation}` ) 18 | 19 | const USER = '' 20 | 21 | const print = data => { 22 | const issues = parser(data) 23 | 24 | issues.map( printIssue ) 25 | 26 | const nc = countIssuesByDifficulty( issues, 'Não classificado') 27 | const s = countIssuesByDifficulty( issues, 'Simples') 28 | const vs = countIssuesByDifficulty( issues, 'Muito simples') 29 | const m = countIssuesByDifficulty( issues, 'Média') 30 | const h = countIssuesByDifficulty( issues, 'Difícil') 31 | const vh = countIssuesByDifficulty( issues, 'Muito difícil') 32 | 33 | const cs = countIssuesByType( issues, 'Atendimento') 34 | const cst = sumTime( issues, 'Atendimento' ) 35 | 36 | const tasks = countIssuesByType( issues, 'Tarefa') 37 | const taskstime = sumTime( issues, 'Tarefa' ) 38 | 39 | const pontuation = sumPontuation( issues ) 40 | const scored = scoredIssues( issues ) 41 | 42 | const percentage = (pontuation * 100) / goal( USER ); 43 | 44 | console.log(`\n\nTotal Not Classified Issues: ${nc}`) 45 | console.log(`Total Very Simple Issues: ${vs}`) 46 | console.log(`Total Simple Issues: ${s}`) 47 | console.log(`Total Medium Issues: ${m}`) 48 | console.log(`Total Hard Issues: ${h}`) 49 | console.log(`Total Very Hard Issues: ${vh}`) 50 | console.log(`\nTotal Customer Service: ${cs}`) 51 | console.log(`Total Customer Service Time: ${cst} minutes`) 52 | console.log(`\nTotal Tasks: ${tasks}`) 53 | console.log(`Total Tasks Time: ${taskstime} minutes -> ${minutesToPoints( taskstime, pointsMinute( USER ) )}`) 54 | console.log(`\nTotal issues: ${issues.length}`) 55 | console.log(`Scored issues: ${scored.length}`) 56 | console.log(`Total pontuation: ${pontuation} -> ${percentage.toFixed(2)}`) 57 | console.log(`Total to complete the goal: ${goal( USER ) - pontuation} -> ${100 - percentage.toFixed(2)}`) 58 | console.log(`Goal: ${goal( USER )}`) 59 | } 60 | 61 | const filterUrl = url( startDate() endDate(), USER ) 62 | const headers = { 'Authorization': `Basic ${auth( config.login, config.pass )}` } 63 | const options = { headers } 64 | 65 | get(filterUrl, options) 66 | .then( response => print(response.data) ) 67 | .catch( response => console.log( `Error: ${response.message}` ) ) 68 | -------------------------------------------------------------------------------- /tests/src/parser.test.js: -------------------------------------------------------------------------------- 1 | const parser = require('../../src/parser') 2 | const { loadFile } = require('../../src/utils') 3 | 4 | const MOCK_FILE = './tests/fixtures/issues.json' 5 | 6 | test('Test error on get issues list', () => { 7 | const dataObj = { issues: null } 8 | const runPartser = () => parser( dataObj ) 9 | expect( runPartser ).toThrowError( 'Request error!' ) 10 | }) 11 | 12 | 13 | test('Total of issues before parser method must be 12', done => { 14 | 15 | loadFile(MOCK_FILE, (err, data) => { 16 | if (err) throw err 17 | 18 | const dataObj = JSON.parse(data) 19 | const issues = dataObj.issues 20 | 21 | const actual = issues.length 22 | const expected = 12 23 | 24 | expect(actual).toBe(expected) 25 | 26 | done() 27 | }) 28 | }) 29 | 30 | test('Total of issues after paser method must be 12', done => { 31 | 32 | loadFile(MOCK_FILE, (err, data) => { 33 | if (err) throw err 34 | 35 | const dataObj = JSON.parse(data) 36 | const issues = parser(dataObj) 37 | 38 | const actual = issues.length 39 | const expected = 12 40 | 41 | expect(actual).toBe(expected) 42 | 43 | done() 44 | }) 45 | }) 46 | 47 | test('The chosen issue must have all the right fields before parser method', done => { 48 | 49 | loadFile(MOCK_FILE, (err, data) => { 50 | if (err) throw err 51 | 52 | const dataObj = JSON.parse(data) 53 | const issues = dataObj.issues 54 | 55 | const actual = issues[6] 56 | const expected = { 57 | key: 'TEST-113', 58 | customfield_17132: { 59 | value: '0 - Não classificado' 60 | }, 61 | timespent: 3600, 62 | customfield_21711: 'Atendimento' 63 | } 64 | 65 | expect(actual.key).toBe(expected.key) 66 | expect(actual.fields.customfield_17132.value).toBe(expected.customfield_17132.value) 67 | expect(actual.fields.timespent).toBe(expected.timespent) 68 | expect(actual.fields.customfield_21711).toBe(expected.customfield_21711) 69 | 70 | done() 71 | }) 72 | }) 73 | 74 | 75 | test('The chosen issue must have all the right fields after parser method', done => { 76 | 77 | loadFile(MOCK_FILE, (err, data) => { 78 | if (err) throw err 79 | 80 | const dataObj = JSON.parse(data) 81 | const issues = parser(dataObj) 82 | 83 | const actual = issues[6] 84 | const expected = { 85 | key: 'TEST-113', 86 | difficulty: 'Sem Pontuação', 87 | pontuation: 0, 88 | time: 60, 89 | type: 'Atendimento' 90 | } 91 | 92 | expect(actual).toMatchObject(expected) 93 | 94 | done() 95 | }) 96 | }) -------------------------------------------------------------------------------- /tests/slackbot/subscribe.test.js: -------------------------------------------------------------------------------- 1 | const subscribe = require('../../slackbot/subscribe') 2 | const patterns = require('../../slackbot/response/patterns') 3 | 4 | describe('Regex patterns: goodMorning', () => { 5 | test('Must run a callback when a right pattern string is sent"', () => { 6 | const message = { 7 | text: 'Olá bom dia.', 8 | channel: 'a1a1a1' 9 | } 10 | const pattern = patterns.GOOD_MORNING 11 | 12 | const callbackMock = jest.fn() 13 | callbackMock.mockReturnValueOnce('Bom dia') 14 | 15 | const runOn = subscribe( {message} ) 16 | 17 | const expected = 'Bom dia' 18 | 19 | runOn( pattern, callbackMock ) 20 | 21 | expect(callbackMock).toBeCalled(); 22 | expect(callbackMock).toBeCalledWith( message, null ); 23 | }) 24 | 25 | test('Must NOT run a callback when a wrong pattern string is sent"', () => { 26 | const message = { 27 | text: 'Olá bom noite.', 28 | channel: 'a1a1a1' 29 | } 30 | const pattern = patterns.GOOD_MORNING 31 | 32 | const callbackMock = jest.fn() 33 | callbackMock.mockReturnValueOnce('Bom dia') 34 | 35 | const runOn = subscribe( {message} ) 36 | 37 | const expected = 'Bom dia' 38 | 39 | runOn( pattern, callbackMock ) 40 | 41 | expect(callbackMock).not.toBeCalled(); 42 | expect(callbackMock).not.toBeCalledWith( message ); 43 | }) 44 | }) 45 | 46 | describe('String pattern: teste', () => { 47 | test('Must run a callback when a right string is sent"', () => { 48 | const message = { 49 | text: 'teste', 50 | channel: 'a1a1a1' 51 | } 52 | const pattern = 'teste' 53 | 54 | const callbackMock = jest.fn() 55 | callbackMock.mockReturnValueOnce('This was a test!') 56 | 57 | const runOn = subscribe( {message} ) 58 | 59 | const expected = 'This was a test!' 60 | 61 | runOn( pattern, callbackMock ) 62 | 63 | expect(callbackMock).toBeCalled(); 64 | expect(callbackMock).toBeCalledWith( message, null ); 65 | }) 66 | 67 | test('Must NOT run a callback when a wrong string is sent"', () => { 68 | const message = { 69 | text: 'testEE', 70 | channel: 'a1a1a1' 71 | } 72 | const pattern = 'teste' 73 | 74 | const callbackMock = jest.fn() 75 | callbackMock.mockReturnValueOnce('This was a test!') 76 | 77 | const runOn = subscribe( {message} ) 78 | 79 | const expected = 'This was a test!' 80 | 81 | runOn( pattern, callbackMock ) 82 | 83 | expect( callbackMock ).not.toBeCalled(); 84 | expect( callbackMock ).not.toBeCalledWith( message ); 85 | }) 86 | }) -------------------------------------------------------------------------------- /slackbot/messages/types.js: -------------------------------------------------------------------------------- 1 | const DONT_GET_IT = [ 2 | 'Não entendi, fala pra fora.', 3 | 'Tira o ovo pra falar.' 4 | ] 5 | 6 | const GOOD_MORNING = [ 7 | 'Só se for pra você!', 8 | 'Bom dia pra quem vai bater a meta de hoje!' 9 | ] 10 | 11 | const WELCOME = [ 12 | 'Ai sim!', 13 | 'Uhul, agora você também é um malcriado!' 14 | ] 15 | 16 | const USER_FOUND = [ 17 | 'Cê já ta logado nem. É só se divertir agora.', 18 | 'Só é preciso logar uma vez, filhote.' 19 | ] 20 | 21 | const USER_NOT_FOUND = [ 22 | 'Não achei esse usuário chefe, escreve direito ae!' 23 | ] 24 | 25 | const USER_NEEDED = [ 26 | 'Ou! Preciso do seu login antes!', 27 | 'Cara crachá, cara crachá. Cade o login.' 28 | ] 29 | 30 | const NOT_ADMIN = [ 31 | 'https://media0.giphy.com/media/njYrp176NQsHS/giphy-downsized.gif' 32 | ] 33 | 34 | const MY_SELF = [ 35 | 'Eu!', 36 | 'Meu nome!', 37 | 'Diga...' 38 | ] 39 | 40 | const HELLO = [ 41 | ( user ) => `Eai <@${user}>!`, 42 | ( user ) => `Colé <@${user}>!` 43 | ] 44 | 45 | const HELP = [ 46 | (() => [ 47 | '*entrar USERNAME* para fazer login', 48 | '*pontos* para pegar seus pontos', 49 | '*issues* para listar as quantidades.', 50 | '*top10* ranking dos colaboradores.', 51 | ].join('\n'))() 52 | ] 53 | 54 | const LOADING = [ 55 | 'To pensando, pera ae!!', 56 | 'Já vai...', 57 | 'Ta com pressa? Passa por cima!', 58 | 'Cê é chato, hein?', 59 | 'Já pensou em deixar isso anotado?' 60 | ] 61 | 62 | const ERROR_BOT = [ 63 | 'Tô a fim de responder, não! Tente novamente...', 64 | 'Me obrigue...' 65 | ] 66 | 67 | const ERROR_JIRA = [ 68 | 'Maẽẽẽẽẽ, foi o Jira!' 69 | ] 70 | 71 | // SCORE MESSAGES 72 | const ONE_THIRD = [ 73 | 'Trabalha não?! É bom começar.', 74 | 'Que que ta pegando? Vamos lá, vocẽ consegue!' 75 | ] 76 | 77 | const LESS_HALF = [ 78 | 'Anda logo com isso ae, ta quase na metade!', 79 | 'Já está quase na metade. BORA TIMEEE!!!' 80 | ] 81 | 82 | const MORE_HALF = [ 83 | 'Tu ta o bichão memo, em?!', 84 | 'Mais um pouco e sai do chão! Ta voando...', 85 | 'Mais um pouco e breja vai ta garantida!' 86 | ] 87 | 88 | const COMPLETED = [ 89 | 'Agora que chegou na meta, vamos dobrar ela.', 90 | 'A meta já acabou mas o trabalho ainda não.', 91 | 'Agora já pode ir ajudar oszamiguinhos.', 92 | 'A breja ta garantida!', 93 | 'Agora pode pagar o sorvetinho pros coleguinhas,' 94 | ] 95 | 96 | module.exports = { 97 | DONT_GET_IT, 98 | ERROR_BOT, 99 | ERROR_JIRA, 100 | GOOD_MORNING, 101 | USER_FOUND, 102 | WELCOME, 103 | USER_NEEDED, 104 | ONE_THIRD, 105 | LESS_HALF, 106 | MORE_HALF, 107 | MY_SELF, 108 | HELLO, 109 | HELP, 110 | LOADING, 111 | COMPLETED, 112 | NOT_ADMIN, 113 | USER_NOT_FOUND 114 | } -------------------------------------------------------------------------------- /slackbot/commands/top10.js: -------------------------------------------------------------------------------- 1 | const messages = require('../messages') 2 | const emitter = require('../eventBus') 3 | const { sumPontuation, minutesToPoints, sumTime } = require('../../src/filters') 4 | const { crud } = require('../../src/db') 5 | const { admins } = require('../../src/configs') 6 | 7 | const isAdmin = ( user ) => admins.includes( user ) 8 | const reachedGoal = ( points, goal ) => points > goal ? ':sunglasses:' : '' 9 | const sortByPoints = ( people ) => [...people].sort( (a,b) => b.points - a.points ); 10 | const printPerson = ( person, index ) => `${index}º - ${person.username} ${reachedGoal(person.points, person.goal)}` 11 | const printPersonAsAdmin = ( person, index ) => `${index + 1}º - ${person.username} => ${person.points} - ${person.issues.length} issues ${reachedGoal(person.points, person.goal)}` 12 | const sendResponse = ( response, channel ) => emitter.emit('SEND', response, channel ) 13 | 14 | const mountResponse = ( user, players, month ) => { 15 | const playerWithPoints = players.map( player => { 16 | const { username } = player 17 | const { issues, pointsPerHour, goal } = player.months[month] 18 | const issuesPoints = sumPontuation( issues ) 19 | const costumerServicePoints = minutesToPoints( sumTime( issues, 'Atendimento' ), pointsPerHour ) 20 | const points = issuesPoints + costumerServicePoints 21 | 22 | return { username, points, issues, goal } 23 | }) 24 | const playersTop = sortByPoints( playerWithPoints) 25 | const topTen = playersTop 26 | .map( isAdmin(user) ? printPersonAsAdmin : printPerson ) 27 | .slice(0, 10) 28 | .join('\n') 29 | 30 | return topTen 31 | } 32 | 33 | const mountTop = ( user, month, channel, topMessage ) => players => { 34 | const response = mountResponse( user, players, month ) 35 | sendResponse( topMessage, channel ) 36 | sendResponse( response, channel ) 37 | } 38 | 39 | const top10 = ( message ) => { 40 | const { channel, user } = message 41 | const month = 'aug' 42 | const dsnPlayers = {username: /.*dsn.*/ } 43 | const qldPlayers = {username: /.*qld.*/ } 44 | 45 | const mountTopDsnWith = mountTop( user, month, channel, '*TOP 10 do desenvolvimento:*' ) 46 | const mountTopToQldWith = mountTop( user, month, channel, '*TOP 10 da qualidade:* ') 47 | 48 | // Must reload the database to get the new values 49 | crud.db.loadDatabase(function (err) { 50 | if ( err ) throw err 51 | 52 | // Get all DSN 53 | crud.find( dsnPlayers, (err, docs) => { 54 | if ( err ) throw err 55 | 56 | if ( docs.length ) mountTopDsnWith( docs ) 57 | }) 58 | 59 | // Get all QLD 60 | crud.find( qldPlayers, (err, docs) => { 61 | if ( err ) throw err 62 | 63 | if ( docs.length ) mountTopToQldWith( docs ) 64 | }) 65 | 66 | }); 67 | } 68 | 69 | console.log('on TOP10') 70 | module.exports = top10 -------------------------------------------------------------------------------- /tests/slackbot/commands/score.test.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../../../slackbot/eventBus') 2 | const player = require('../../fixtures/player') 3 | const { ONE_THIRD, LESS_HALF, MORE_HALF, COMPLETED } = require('../../../slackbot/messages/types') 4 | const score = require('../../../slackbot/commands/score') 5 | 6 | 7 | describe('Score command Must emit an event and data object', () => { 8 | 9 | describe('score command', () => { 10 | it('should emit an "SEND"', ( ) => { 11 | const eventSpy = jest.fn() 12 | emitter.on('SEND', eventSpy ) 13 | 14 | const message = {} 15 | score( message, player ) 16 | 17 | expect(eventSpy).toBeCalled() 18 | }) 19 | 20 | it('should emit an "SEND" wit some message and channel', ( ) => { 21 | const message = { 22 | text: 'help me', 23 | channel: 'aaa', 24 | user: '123465' 25 | } 26 | const expectedResponse = [ 27 | /.*\*600 minutos\* de atendimento.*/, 28 | /.*\*210\* pontos.*/, 29 | /.*\*560\* pontos/, 30 | /.*completou \*15.87%\*/, 31 | /.*meta \*3528\* !.*/, 32 | /.*Faltam \*2968\* pontos.*/, 33 | /.*\*84.13%\* para bater a meta.*/, 34 | ] 35 | 36 | const eventSpy = jest.fn() 37 | emitter.on('SEND', eventSpy ) 38 | 39 | score( message, player ) 40 | 41 | expect(eventSpy).toBeCalled() 42 | 43 | // Both params are passing as only one, the first 44 | const [ actualResponse, channel ] = eventSpy.mock.calls[0] 45 | 46 | expectedResponse.map( (response) => expect( actualResponse ).toMatch( response ) ) 47 | expect( channel ).toBe( message.channel ); 48 | }) 49 | }) 50 | 51 | describe('score messages', () => { 52 | // Goal is 35xx 53 | it('Must return one third kind of message', ( ) => { 54 | const issues = [ 55 | { 56 | key: 'TEST-1', 57 | type: 'Programação', 58 | time: 600, 59 | difficulty: 'Muito simples', 60 | pontuation: 30 61 | } 62 | ] 63 | testScoreMessage( ONE_THIRD, issues ) 64 | }) 65 | 66 | it('Must return less than half kind of message', ( ) => { 67 | const issues = [ 68 | { 69 | key: 'TEST-1', 70 | type: 'Programação', 71 | time: 600, 72 | difficulty: 'Muito simples', 73 | pontuation: 1500 74 | } 75 | ] 76 | testScoreMessage( LESS_HALF, issues) 77 | }) 78 | 79 | it('Must return more than half kind of message', ( ) => { 80 | const issues = [ 81 | { 82 | key: 'TEST-1', 83 | type: 'Programação', 84 | time: 600, 85 | difficulty: 'Muito simples', 86 | pontuation: 2500 87 | } 88 | ] 89 | testScoreMessage( MORE_HALF, issues) 90 | }) 91 | 92 | it('Must return completed kind of message', ( ) => { 93 | const issues = [ 94 | { 95 | key: 'TEST-1', 96 | type: 'Programação', 97 | time: 600, 98 | difficulty: 'Muito simples', 99 | pontuation: 3600 100 | } 101 | ] 102 | testScoreMessage( COMPLETED, issues) 103 | }) 104 | }) 105 | }) 106 | 107 | 108 | const testScoreMessage = ( messages, issues ) => { 109 | const message = {} 110 | const eventSpy = jest.fn() 111 | emitter.on('SEND', eventSpy ) 112 | 113 | player.months.aug.issues = issues 114 | score( message, player ) 115 | 116 | const [ actualResponse, channel ] = eventSpy.mock.calls[0] 117 | const responseLines = actualResponse.split('\n') 118 | const lastLine = responseLines[ responseLines.length - 1 ] 119 | expect( messages ).toContain( lastLine ); 120 | } -------------------------------------------------------------------------------- /tests/slackbot/messages.test.js: -------------------------------------------------------------------------------- 1 | const types = require('../../slackbot/messages/types') 2 | const messages = require('../../slackbot/messages') 3 | 4 | describe('Must return a message containing in the list', () => { 5 | 6 | describe('DONT_GET_IT message', () => { 7 | it('should return an message from the list', ( ) => { 8 | const actual = messages( 'DONT_GET_IT' ) 9 | 10 | expect( types.DONT_GET_IT ).toContain( actual ); 11 | }) 12 | }) 13 | 14 | describe('ERROR_BOT message', () => { 15 | it('should return an message from the list', ( ) => { 16 | const actual = messages( 'ERROR_BOT' ) 17 | 18 | expect( types.ERROR_BOT ).toContain( actual ); 19 | }) 20 | }) 21 | 22 | describe('ERROR_JIRA message', () => { 23 | it('should return an message from the list', ( ) => { 24 | const actual = messages( 'ERROR_JIRA' ) 25 | 26 | expect( types.ERROR_JIRA ).toContain( actual ); 27 | }) 28 | }) 29 | 30 | describe('GOOD_MORNING message', () => { 31 | it('should return an message from the list', ( ) => { 32 | const actual = messages( 'GOOD_MORNING' ) 33 | 34 | expect( types.GOOD_MORNING ).toContain( actual ); 35 | }) 36 | }) 37 | 38 | describe('USER_FOUND message', () => { 39 | it('should return an message from the list', ( ) => { 40 | const actual = messages( 'USER_FOUND' ) 41 | 42 | expect( types.USER_FOUND ).toContain( actual ); 43 | }) 44 | }) 45 | 46 | describe('WELCOME message', () => { 47 | it('should return an message from the list', ( ) => { 48 | const actual = messages( 'WELCOME' ) 49 | 50 | expect( types.WELCOME ).toContain( actual ); 51 | }) 52 | }) 53 | 54 | describe('USER_NEEDED message', () => { 55 | it('should return an message from the list', ( ) => { 56 | const actual = messages( 'USER_NEEDED' ) 57 | 58 | expect( types.USER_NEEDED ).toContain( actual ); 59 | }) 60 | }) 61 | 62 | describe('ONE_THIRD message', () => { 63 | it('should return an message from the list', ( ) => { 64 | const actual = messages( 'ONE_THIRD' ) 65 | 66 | expect( types.ONE_THIRD ).toContain( actual ); 67 | }) 68 | }) 69 | 70 | describe('LESS_HALF message', () => { 71 | it('should return an message from the list', ( ) => { 72 | const actual = messages( 'LESS_HALF' ) 73 | 74 | expect( types.LESS_HALF ).toContain( actual ); 75 | }) 76 | }) 77 | 78 | describe('MORE_HALF message', () => { 79 | it('should return an message from the list', ( ) => { 80 | const actual = messages( 'MORE_HALF' ) 81 | 82 | expect( types.MORE_HALF ).toContain( actual ); 83 | }) 84 | }) 85 | 86 | describe('MY_SELF message', () => { 87 | it('should return an message from the list', ( ) => { 88 | const actual = messages( 'MY_SELF' ) 89 | 90 | expect( types.MY_SELF ).toContain( actual ); 91 | }) 92 | }) 93 | 94 | describe('HELLO message', () => { 95 | it('should return an message from the list', ( ) => { 96 | const actual = messages( 'HELLO' ) 97 | 98 | expect( types.HELLO ).toContain( actual ); 99 | }) 100 | }) 101 | 102 | describe('HELP message', () => { 103 | it('should return an message from the list', ( ) => { 104 | const actual = messages( 'HELP' ) 105 | 106 | expect( types.HELP ).toContain( actual ); 107 | }) 108 | }) 109 | 110 | describe('LOADING message', () => { 111 | it('should return an message from the list', ( ) => { 112 | const actual = messages( 'LOADING' ) 113 | 114 | expect( types.LOADING ).toContain( actual ); 115 | }) 116 | }) 117 | 118 | describe('COMPLETED message', () => { 119 | it('should return an message from the list', ( ) => { 120 | const actual = messages( 'COMPLETED' ) 121 | 122 | expect( types.COMPLETED ).toContain( actual ); 123 | }) 124 | }) 125 | 126 | describe('NOT_ADMIN message', () => { 127 | it('should return an message from the list', ( ) => { 128 | const actual = messages( 'NOT_ADMIN' ) 129 | 130 | expect( types.NOT_ADMIN ).toContain( actual ); 131 | }) 132 | }) 133 | 134 | describe('USER_NOT_FOUND message', () => { 135 | it('should return an message from the list', ( ) => { 136 | const actual = messages( 'USER_NOT_FOUND' ) 137 | 138 | expect( types.USER_NOT_FOUND ).toContain( actual ); 139 | }) 140 | }) 141 | 142 | describe('default message', () => { 143 | it('should return an errow if the options does not exist', ( ) => { 144 | const expectedError = new Error('Message not found') 145 | expect( messages( 'ANYTHING' ) ).toMatchObject( expectedError ); 146 | }) 147 | }) 148 | }) -------------------------------------------------------------------------------- /tests/slackbot/callbacks.test.js: -------------------------------------------------------------------------------- 1 | const emitter = require('../../slackbot/eventBus') 2 | 3 | const { callbacks } = require('../../slackbot/response') 4 | 5 | describe('Must emit an event and data object', () => { 6 | 7 | describe('Good morning callback', () => { 8 | it('should emit an "GOOD_MORNING"', ( ) => { 9 | const eventSpy = jest.fn() 10 | emitter.on('GOOD_MORNING', eventSpy ) 11 | 12 | callbacks.goodMorning( {} ) 13 | 14 | expect(eventSpy).toBeCalled() 15 | }) 16 | 17 | it('should emit an "GOOD_MORNING"', ( ) => { 18 | const message = { 19 | text: 'good morning', 20 | channel: 'aaa' 21 | } 22 | const eventSpy = jest.fn() 23 | emitter.on('GOOD_MORNING', eventSpy ) 24 | 25 | callbacks.goodMorning( message ) 26 | 27 | expect(eventSpy).toBeCalled() 28 | expect(eventSpy).toBeCalledWith( message ) 29 | }) 30 | }) 31 | 32 | describe('Hello callback', () => { 33 | it('should emit an "HELLO"', ( ) => { 34 | const eventSpy = jest.fn() 35 | emitter.on('HELLO', eventSpy ) 36 | 37 | callbacks.hello( {} ) 38 | 39 | expect(eventSpy).toBeCalled() 40 | }) 41 | 42 | it('should emit an "HELLO"', ( ) => { 43 | const message = { 44 | text: 'hello', 45 | channel: 'aaa' 46 | } 47 | const eventSpy = jest.fn() 48 | emitter.on('HELLO', eventSpy ) 49 | 50 | callbacks.hello( message ) 51 | 52 | expect(eventSpy).toBeCalled() 53 | expect(eventSpy).toBeCalledWith( message ) 54 | }) 55 | }) 56 | 57 | describe('Help callback', () => { 58 | it('should emit an "HELP"', ( ) => { 59 | const eventSpy = jest.fn() 60 | emitter.on('HELP', eventSpy ) 61 | 62 | callbacks.help( {} ) 63 | 64 | expect(eventSpy).toBeCalled() 65 | }) 66 | 67 | it('should emit an "HELP"', ( ) => { 68 | const message = { 69 | text: 'help', 70 | channel: 'aaa' 71 | } 72 | const eventSpy = jest.fn() 73 | emitter.on('HELP', eventSpy ) 74 | 75 | callbacks.help( message ) 76 | 77 | expect(eventSpy).toBeCalled() 78 | expect(eventSpy).toBeCalledWith( message ) 79 | }) 80 | }) 81 | 82 | describe('My self callback', () => { 83 | it('should emit an "MY_SELF"', ( ) => { 84 | const eventSpy = jest.fn() 85 | emitter.on('MY_SELF', eventSpy ) 86 | 87 | callbacks.mySelf( {} ) 88 | 89 | expect(eventSpy).toBeCalled() 90 | }) 91 | 92 | it('should emit an "MY_SELF"', ( ) => { 93 | const message = { 94 | text: 'my self', 95 | channel: 'aaa' 96 | } 97 | const eventSpy = jest.fn() 98 | emitter.on('MY_SELF', eventSpy ) 99 | 100 | callbacks.mySelf( message ) 101 | 102 | expect(eventSpy).toBeCalled() 103 | expect(eventSpy).toBeCalledWith( message ) 104 | }) 105 | }) 106 | 107 | describe('Load issues callback', () => { 108 | it('should emit an "LOAD_ISSUES"', ( ) => { 109 | const eventSpy = jest.fn() 110 | emitter.on('LOAD_ISSUES', eventSpy ) 111 | 112 | callbacks.loadIssues( {} ) 113 | 114 | expect(eventSpy).toBeCalled() 115 | }) 116 | 117 | it('should emit an "LOAD_ISSUES"', ( ) => { 118 | const message = { 119 | text: 'my score', 120 | channel: 'aaa' 121 | } 122 | const event = 'score' 123 | 124 | const eventSpy = jest.fn() 125 | emitter.on('LOAD_ISSUES', eventSpy ) 126 | 127 | callbacks.loadIssues( message, event ) 128 | 129 | expect(eventSpy).toBeCalled() 130 | expect(eventSpy).toBeCalledWith( message, event ) 131 | }) 132 | }) 133 | 134 | describe('Login callback', () => { 135 | it('should emit an "LOGIN"', ( ) => { 136 | const eventSpy = jest.fn() 137 | emitter.on('LOGIN', eventSpy ) 138 | 139 | callbacks.login( {} ) 140 | 141 | expect(eventSpy).toBeCalled() 142 | }) 143 | 144 | it('should emit an "LOGIN"', ( ) => { 145 | const message = { 146 | text: 'log me in', 147 | channel: 'aaa' 148 | } 149 | 150 | const eventSpy = jest.fn() 151 | emitter.on('LOGIN', eventSpy ) 152 | 153 | callbacks.login( message ) 154 | 155 | expect(eventSpy).toBeCalled() 156 | expect(eventSpy).toBeCalledWith( message ) 157 | }) 158 | }) 159 | 160 | describe('Player callback', () => { 161 | it('should emit an "PLAYER"', ( ) => { 162 | const eventSpy = jest.fn() 163 | emitter.on('PLAYER', eventSpy ) 164 | 165 | callbacks.player( {} ) 166 | 167 | expect(eventSpy).toBeCalled() 168 | }) 169 | 170 | it('should emit an "PLAYER"', ( ) => { 171 | const message = { 172 | text: 'player fellipe', 173 | channel: 'aaa' 174 | } 175 | 176 | const eventSpy = jest.fn() 177 | emitter.on('PLAYER', eventSpy ) 178 | 179 | callbacks.player( message ) 180 | 181 | expect(eventSpy).toBeCalled() 182 | expect(eventSpy).toBeCalledWith( message ) 183 | }) 184 | }) 185 | }) -------------------------------------------------------------------------------- /tests/src/parser_filter.test.js: -------------------------------------------------------------------------------- 1 | const parser = require('../../src/parser') 2 | const { loadFile } = require('../../src/utils') 3 | const { 4 | countIssuesByType, 5 | countIssuesByDifficulty, 6 | sumPontuation, 7 | sumTime, 8 | hasScore, 9 | scoredIssues 10 | } = require('../../src/filters') 11 | 12 | const MOCK_FILE = './tests/fixtures/issues.json' 13 | 14 | test('Scored issues must be 10', done => { 15 | 16 | loadFile(MOCK_FILE, (err, data) => { 17 | if (err) throw err 18 | 19 | const dataObj = JSON.parse(data) 20 | const issues = parser(dataObj) 21 | const scored = scoredIssues( issues ) 22 | 23 | const actual = scored.length 24 | const expected = 10 25 | 26 | expect(actual).toBe(expected) 27 | 28 | done() 29 | }) 30 | 31 | }) 32 | 33 | test('Pontuation must be 1295', done => { 34 | // Não Classificada 60 35 | // 4 Muito simples (4x30) 36 | // 1 Simples 75 37 | // 1 Média 160 38 | // 1 Difícil 320 39 | // 1 Muito difícil 560 40 | loadFile(MOCK_FILE, (err, data) => { 41 | if (err) throw err 42 | 43 | const dataObj = JSON.parse(data) 44 | const issues = parser(dataObj) 45 | 46 | const actual = sumPontuation( issues ) 47 | const expected = 1295 48 | 49 | expect(actual).toBe(expected) 50 | 51 | done() 52 | }) 53 | 54 | }) 55 | 56 | test('Issues must have all fields right', done => { 57 | 58 | loadFile(MOCK_FILE, (err, data) => { 59 | if (err) throw err 60 | 61 | const dataObj = JSON.parse(data) 62 | const issues = parser(dataObj) 63 | 64 | const actual = issues[6] 65 | const expected = { 66 | key: 'TEST-113', 67 | difficulty: 'Sem Pontuação', 68 | pontuation: 0, 69 | time: 60, 70 | type: 'Atendimento' 71 | } 72 | 73 | expect(actual).toMatchObject(expected) 74 | 75 | done() 76 | }) 77 | 78 | }) 79 | 80 | test('Total customer service issues must be 2', done => { 81 | 82 | loadFile(MOCK_FILE, (err, data) => { 83 | if (err) throw err 84 | 85 | const dataObj = JSON.parse(data) 86 | const issues = parser(dataObj) 87 | 88 | const actual = countIssuesByType( issues, 'Atendimento') 89 | const expected = 2 90 | 91 | expect(actual).toBe(expected) 92 | 93 | done() 94 | }) 95 | 96 | }) 97 | 98 | test('Total customer service issues time must be 180 minutes', done => { 99 | 100 | loadFile(MOCK_FILE, (err, data) => { 101 | if (err) throw err 102 | 103 | const dataObj = JSON.parse(data) 104 | const issues = parser(dataObj) 105 | 106 | const actual = sumTime( issues, 'Atendimento' ) 107 | const expected = 180 108 | 109 | expect(actual).toBe(expected) 110 | 111 | done() 112 | }) 113 | 114 | }) 115 | 116 | test('Total tasks issues must be 1', done => { 117 | 118 | loadFile(MOCK_FILE, (err, data) => { 119 | if (err) throw err 120 | 121 | const dataObj = JSON.parse(data) 122 | const issues = parser(dataObj) 123 | 124 | const actual = countIssuesByType( issues, 'Tarefa') 125 | const expected = 1 126 | 127 | expect(actual).toBe(expected) 128 | 129 | done() 130 | }) 131 | 132 | }) 133 | 134 | test('Total tasks issues time must be 120 minutes', done => { 135 | 136 | loadFile(MOCK_FILE, (err, data) => { 137 | if (err) throw err 138 | 139 | const dataObj = JSON.parse(data) 140 | const issues = parser(dataObj) 141 | 142 | const actual = sumTime( issues, 'Tarefa' ) 143 | const expected = 120 144 | 145 | expect(actual).toBe(expected) 146 | 147 | done() 148 | }) 149 | 150 | }) 151 | 152 | test('Total not classified issues must be 2', done => { 153 | // Issues with type equals to 'Programação', 'Teste' and 'Tarefa' 154 | loadFile(MOCK_FILE, (err, data) => { 155 | if (err) throw err 156 | 157 | const dataObj = JSON.parse(data) 158 | const issues = parser(dataObj) 159 | 160 | const actual = countIssuesByDifficulty( issues, 'Não classificado') 161 | const expected = 2 162 | 163 | expect(actual).toBe(expected) 164 | 165 | done() 166 | }) 167 | 168 | }) 169 | 170 | test('Total very simple issues must be 4', done => { 171 | 172 | loadFile(MOCK_FILE, (err, data) => { 173 | if (err) throw err 174 | 175 | const dataObj = JSON.parse(data) 176 | const issues = parser(dataObj) 177 | 178 | const actual = countIssuesByDifficulty( issues, 'Muito simples') 179 | const expected = 4 180 | 181 | expect(actual).toBe(expected) 182 | 183 | done() 184 | }) 185 | 186 | }) 187 | 188 | test('Total simple issues must be 1', done => { 189 | 190 | loadFile(MOCK_FILE, (err, data) => { 191 | if (err) throw err 192 | 193 | const dataObj = JSON.parse(data) 194 | const issues = parser(dataObj) 195 | 196 | const actual = countIssuesByDifficulty( issues, 'Simples') 197 | const expected = 1 198 | 199 | expect(actual).toBe(expected) 200 | 201 | done() 202 | }) 203 | 204 | }) 205 | 206 | test('Total medium issues must be 1', done => { 207 | 208 | loadFile(MOCK_FILE, (err, data) => { 209 | if (err) throw err 210 | 211 | const dataObj = JSON.parse(data) 212 | const issues = parser(dataObj) 213 | 214 | const actual = countIssuesByDifficulty( issues, 'Média') 215 | const expected = 1 216 | 217 | expect(actual).toBe(expected) 218 | 219 | done() 220 | }) 221 | 222 | }) 223 | 224 | test('Total hard issues must be 1', done => { 225 | 226 | loadFile(MOCK_FILE, (err, data) => { 227 | if (err) throw err 228 | 229 | const dataObj = JSON.parse(data) 230 | const issues = parser(dataObj) 231 | 232 | const actual = countIssuesByDifficulty( issues, 'Difícil') 233 | const expected = 1 234 | 235 | expect(actual).toBe(expected) 236 | 237 | done() 238 | }) 239 | 240 | }) 241 | 242 | test('Total very hard issues must be 1', done => { 243 | 244 | loadFile(MOCK_FILE, (err, data) => { 245 | if (err) throw err 246 | 247 | const dataObj = JSON.parse(data) 248 | const issues = parser(dataObj) 249 | 250 | const actual = countIssuesByDifficulty( issues, 'Muito difícil') 251 | const expected = 1 252 | 253 | expect(actual).toBe(expected) 254 | 255 | done() 256 | }) 257 | 258 | }) 259 | -------------------------------------------------------------------------------- /tests/src/filters.test.js: -------------------------------------------------------------------------------- 1 | const filters = require('../../src/filters') 2 | 3 | const issues = [ 4 | { 5 | key: 'TEST-1', 6 | type: 'Programação', 7 | time: 600, 8 | difficulty: 'Muito simples', 9 | pontuation: 30 10 | }, 11 | { 12 | key: 'TEST-2', 13 | type: 'Teste', 14 | time: 300, 15 | difficulty: 'Muito simples', 16 | pontuation: 30 17 | }, 18 | { 19 | key: 'TEST-3', 20 | type: 'Tarefa', 21 | time: 300, 22 | difficulty: 'Não classificado', 23 | pontuation: 0 24 | }, 25 | { 26 | key: 'TEST-4', 27 | type: 'Programação', 28 | time: 360, 29 | difficulty: 'Simples', 30 | pontuation: 75 31 | }, 32 | { 33 | key: 'TEST-5', 34 | type: 'Atendimento', 35 | time: 600, 36 | difficulty: 'Não classificado', 37 | pontuation: 0 38 | }, 39 | { 40 | key: 'TEST-6', 41 | type: 'Liberação de Versão', 42 | time: 0, 43 | difficulty: 'Muito simples', 44 | pontuation: 30 45 | }, 46 | { 47 | key: 'TEST-7', 48 | type: 'Liberação de Versão Web', 49 | time: 0, 50 | difficulty: 'Simples', 51 | pontuation: 75 52 | }, 53 | { 54 | key: 'TEST-8', 55 | type: 'Manual / Documentação', 56 | time: 0, 57 | difficulty: 'Difícil', 58 | pontuation: 320 59 | } 60 | 61 | ] 62 | 63 | describe('Function: countIssueByType', () => { 64 | test('Must return 2 issues of "Programação"', () => { 65 | const expected = 2 66 | 67 | const result = filters.countIssuesByType( issues, 'Programação' ) 68 | expect(result).toBe(expected) 69 | }) 70 | 71 | test('Must return 1 issues of "Tarefa"', () => { 72 | const expected = 1 73 | 74 | const result = filters.countIssuesByType( issues, 'Tarefa' ) 75 | expect(result).toBe(expected) 76 | }) 77 | 78 | test('Must return 0 issues of "Erro"', () => { 79 | const expected = 0 80 | 81 | const result = filters.countIssuesByType( issues, 'Erro' ) 82 | expect(result).toBe(expected) 83 | }) 84 | 85 | test('Must return 1 issues of "Liberação de Versão"', () => { 86 | const expected = 1 87 | 88 | const result = filters.countIssuesByType( issues, 'Liberação de Versão' ) 89 | expect(result).toBe(expected) 90 | }) 91 | }) 92 | 93 | describe('Function: countIssueByDifficulty', () => { 94 | test('Must return 3 issues "Muito simples"', () => { 95 | const expected = 3 96 | 97 | const result = filters.countIssuesByDifficulty( issues, 'Muito simples' ) 98 | expect(result).toBe(expected) 99 | }) 100 | 101 | test('Must return 2 issues "Simples"', () => { 102 | const expected = 2 103 | 104 | const result = filters.countIssuesByDifficulty( issues, 'Simples' ) 105 | expect(result).toBe(expected) 106 | }) 107 | 108 | test('Must return 0 issues "Média"', () => { 109 | const expected = 0 110 | 111 | const result = filters.countIssuesByDifficulty( issues, 'Média' ) 112 | expect(result).toBe(expected) 113 | }) 114 | 115 | test('Must return 1 issues "Difícil"', () => { 116 | const expected = 1 117 | 118 | const result = filters.countIssuesByDifficulty( issues, 'Difícil' ) 119 | expect(result).toBe(expected) 120 | }) 121 | 122 | test('Must return 0 issues "Muito difícil"', () => { 123 | const expected = 0 124 | 125 | const result = filters.countIssuesByDifficulty( issues, 'Muito difícil' ) 126 | expect(result).toBe(expected) 127 | }) 128 | }) 129 | 130 | describe('Function: sumPontuation', () => { 131 | test('Must return 560 points', () => { 132 | const expected = 560 133 | 134 | const result = filters.sumPontuation( issues ) 135 | expect(result).toBe(expected) 136 | }) 137 | }) 138 | 139 | describe('Function: sumTime', () => { 140 | // divided by 60, we have 5 minutes 141 | test('Must return 300 for "Tarefa" issues', () => { 142 | const expected = 300 143 | 144 | const result = filters.sumTime( issues, 'Tarefa' ) 145 | expect(result).toBe(expected) 146 | }) 147 | 148 | test('Must return 600 for "Atendimento" issues', () => { 149 | const expected = 600 150 | 151 | const result = filters.sumTime( issues, 'Atendimento' ) 152 | expect(result).toBe(expected) 153 | }) 154 | 155 | // divided by 60, we have 16 minutes 156 | test('Must return 960 for "Programação" issues', () => { 157 | const expected = 960 158 | 159 | const result = filters.sumTime( issues, 'Programação' ) 160 | expect(result).toBe(expected) 161 | }) 162 | }) 163 | 164 | describe('Function: hasScore', () => { 165 | test('Must return true for "Programação" issues', () => { 166 | const expected = true 167 | const firstIssue = issues[0] 168 | const result = filters.hasScore( firstIssue ) 169 | expect(result).toBe(expected) 170 | }) 171 | 172 | test('Must return true for "Teste" issues', () => { 173 | const expected = true 174 | const firstIssue = issues[1] 175 | const result = filters.hasScore( firstIssue ) 176 | expect(result).toBe(expected) 177 | }) 178 | 179 | test('Must return false for "Tarefa" issues', () => { 180 | const expected = false 181 | const firstIssue = issues[2] 182 | const result = filters.hasScore( firstIssue ) 183 | expect(result).toBe(expected) 184 | }) 185 | 186 | test('Must return false for "Atendimento" issues', () => { 187 | const expected = false 188 | const firstIssue = issues[4] 189 | const result = filters.hasScore( firstIssue ) 190 | expect(result).toBe(expected) 191 | }) 192 | }) 193 | 194 | describe('Function: scoredIssues', () => { 195 | test('Must return 6 issues', () => { 196 | const expected = 6 197 | const scored = filters.scoredIssues( issues ) 198 | const result = scored.length 199 | expect(result).toBe(expected) 200 | }) 201 | }) 202 | 203 | describe('Function: minutesToPoints', () => { 204 | test('Must return 42 points for 120 minutes, 21/h ', () => { 205 | const pointsPerHour = 21 206 | const minutes = 120 207 | 208 | const expected = 42 209 | const result = filters.minutesToPoints( minutes, pointsPerHour ) 210 | expect(result).toBe(expected) 211 | }) 212 | }) 213 | 214 | describe('Function: pointsPercentage', () => { 215 | test('Must return 37.30 percent for 1316 points, with 3528 goal ', () => { 216 | const goal = 3528 217 | const points = 1316 218 | 219 | const expected = '37.30' 220 | const result = filters.pointsPercentage( goal, points ) 221 | expect(result).toBe(expected) 222 | }) 223 | }) -------------------------------------------------------------------------------- /tests/src/pontuations.test.js: -------------------------------------------------------------------------------- 1 | const { getDifficulty, isClassified } = require('../../src/pontuations') 2 | 3 | describe('Function: getDifficulty', () => { 4 | describe('Issue: Very simple', () => { 5 | test('"Muito simples" must have "VS" as slug', () => { 6 | const payload = 'Muito simples' 7 | const expected = 'VS' 8 | 9 | const result = getDifficulty(payload).slug 10 | expect(result).toBe(expected) 11 | }); 12 | 13 | test('"Muito simples" must return 30', () => { 14 | const payload = 'Muito simples' 15 | const expected = 30 16 | 17 | const result = getDifficulty(payload).points 18 | expect(result).toBe(expected) 19 | }); 20 | }) 21 | 22 | 23 | describe('Issue: Simple', () => { 24 | test('"Simples" must have "S" as slug', () => { 25 | const payload = 'Simples' 26 | const expected = 'S' 27 | 28 | const result = getDifficulty(payload).slug 29 | expect(result).toBe(expected) 30 | }) 31 | 32 | test('"Simples" must return 75', () => { 33 | const payload = 'Simples' 34 | const expected = 75 35 | 36 | const result = getDifficulty(payload).points 37 | expect(result).toBe(expected) 38 | }) 39 | }) 40 | 41 | describe('Issue: Média', () => { 42 | test('"Média" must have "M" as slug', () => { 43 | const payload = 'Média' 44 | const expected = 'M' 45 | 46 | const result = getDifficulty(payload).slug 47 | expect(result).toBe(expected) 48 | }) 49 | 50 | test('"Média" must return 160', () => { 51 | const payload = 'Média' 52 | const expected = 160 53 | 54 | const result = getDifficulty(payload).points 55 | expect(result).toBe(expected) 56 | }) 57 | }) 58 | 59 | describe('Issue: Difícil', () => { 60 | test('"Difícil" must have "D" as slug', () => { 61 | const payload = 'Difícil' 62 | const expected = 'H' 63 | 64 | const result = getDifficulty(payload).slug 65 | expect(result).toBe(expected) 66 | }) 67 | 68 | test('"Difícil" must return 320', () => { 69 | const payload = 'Difícil' 70 | const expected = 320 71 | 72 | const result = getDifficulty(payload).points 73 | expect(result).toBe(expected) 74 | }) 75 | }) 76 | 77 | describe('Issue: Muito Difícil', () => { 78 | test('"Muito Difícil" must have "D" as slug', () => { 79 | const payload = 'Muito difícil' 80 | const expected = 'VH' 81 | 82 | const result = getDifficulty(payload).slug 83 | expect(result).toBe(expected) 84 | }) 85 | 86 | test('"Muito difícil" must return 560', () => { 87 | const payload = 'Muito difícil' 88 | const expected = 560 89 | 90 | const result = getDifficulty(payload).points 91 | expect(result).toBe(expected) 92 | }) 93 | }) 94 | 95 | describe('Issue: Não classificado', () => { 96 | test('"Não classificado" must have "NC" as slug', () => { 97 | const payload = 'Não classificado' 98 | const expected = 'NC' 99 | 100 | const result = getDifficulty(payload).slug 101 | expect(result).toBe(expected) 102 | }) 103 | 104 | test('"Não classificado" must return 30', () => { 105 | const payload = 'Não classificado' 106 | const expected = 30 107 | 108 | const result = getDifficulty(payload).points 109 | expect(result).toBe(expected) 110 | }) 111 | }) 112 | 113 | describe('Issue: Sem Pontuação', () => { 114 | test('"Sem Pontuação" must have "NP" as slug', () => { 115 | const payload = 'Sem Pontuação' 116 | const expected = 'NP' 117 | 118 | const result = getDifficulty(payload).slug 119 | expect(result).toBe(expected) 120 | }) 121 | 122 | test('"Sem Pontuação" must return 0', () => { 123 | const payload = 'Sem Pontuação' 124 | const expected = 0 125 | 126 | const result = getDifficulty(payload).points 127 | expect(result).toBe(expected) 128 | }) 129 | }) 130 | 131 | test('"Anything" must return as "Não classificado"', () => { 132 | const payload = 'Anything' 133 | const expectedPoints = 30 134 | const expectedSlug = 'NC' 135 | 136 | const actualPoints = getDifficulty(payload).points 137 | expect(actualPoints).toBe(expectedPoints) 138 | 139 | const actualSlug = getDifficulty(payload).slug 140 | expect(actualSlug).toBe(expectedSlug) 141 | 142 | }) 143 | }) 144 | 145 | describe('Function: isClassified', () => { 146 | test('Must return true for "Programação" issues', () => { 147 | const payload = 'Programação' 148 | const expected = true 149 | const result = isClassified( payload ) 150 | expect(result).toBe(expected) 151 | }) 152 | 153 | test('Must return true for "Teste" issues', () => { 154 | const payload = 'Teste' 155 | const expected = true 156 | const result = isClassified( payload ) 157 | expect(result).toBe(expected) 158 | }) 159 | 160 | test('Must return true for "Manual / Documentação" issues', () => { 161 | const payload = 'Manual / Documentação' 162 | const expected = true 163 | const result = isClassified( payload ) 164 | expect(result).toBe(expected) 165 | }) 166 | 167 | test('Must return true for "Liberação de Versão Web" issues', () => { 168 | const payload = 'Liberação de Versão Web' 169 | const expected = true 170 | const result = isClassified( payload ) 171 | expect(result).toBe(expected) 172 | }) 173 | 174 | test('Must return true for "Liberação de Versão" issues', () => { 175 | const payload = 'Liberação de Versão' 176 | const expected = true 177 | const result = isClassified( payload ) 178 | expect(result).toBe(expected) 179 | }) 180 | 181 | test('Must return true for "Tarefa" issues', () => { 182 | const payload = 'Tarefa' 183 | const expected = true 184 | const result = isClassified( payload ) 185 | expect(result).toBe(expected) 186 | }) 187 | 188 | test('Must return false for "Melhoria" issues', () => { 189 | const payload = 'Melhoria' 190 | const expected = true 191 | const result = isClassified( payload ) 192 | expect(result).toBe(expected) 193 | }) 194 | 195 | test('Must return false for "Erro" issues', () => { 196 | const payload = 'Erro' 197 | const expected = true 198 | const result = isClassified( payload ) 199 | expect(result).toBe(expected) 200 | }) 201 | 202 | test('Must return false for "Atendimento" issues', () => { 203 | const payload = 'Atendimento' 204 | const expected = false 205 | const result = isClassified( payload ) 206 | expect(result).toBe(expected) 207 | }) 208 | }) -------------------------------------------------------------------------------- /tests/fixtures/issues.json: -------------------------------------------------------------------------------- 1 | { 2 | "expand": "schema,names", 3 | "startAt": 0, 4 | "maxResults": 200, 5 | "total": 10, 6 | "issues": [{ 7 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 8 | "id": "690215", 9 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/690215", 10 | "key": "TEST-117", 11 | "fields": { 12 | "issuetype": { 13 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/7", 14 | "id": "7", 15 | "description": "Tarefa de programação da pendência", 16 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22860&avatarType=issuetype", 17 | "name": "Programação", 18 | "subtask": true, 19 | "avatarId": 22860 20 | }, 21 | "timespent": 900, 22 | "customfield_17132": { 23 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 24 | "value": "1 - Muito simples", 25 | "id": "15804" 26 | }, 27 | "project": { 28 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 29 | "id": "17274", 30 | "key": "TEST", 31 | "name": "Project", 32 | "avatarUrls": { 33 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 34 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 35 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 36 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 37 | }, 38 | "projectCategory": { 39 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 40 | "id": "10002", 41 | "description": "Projetos do setor de novas tecnologias", 42 | "name": "Group" 43 | } 44 | }, 45 | "assignee": { 46 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 47 | "name": "myuser", 48 | "key": "user.group", 49 | "emailAddress": "myuser@mycompany.com.br", 50 | "avatarUrls": { 51 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 52 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 53 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 54 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 55 | }, 56 | "displayName": "myuser", 57 | "active": true, 58 | "timeZone": "America/Sao_Paulo" 59 | }, 60 | "customfield_21711": "Programação" 61 | } 62 | }, { 63 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 64 | "id": "690214", 65 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/690214", 66 | "key": "TEST-116", 67 | "fields": { 68 | "issuetype": { 69 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/7", 70 | "id": "7", 71 | "description": "Tarefa de programação da pendência", 72 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22860&avatarType=issuetype", 73 | "name": "Programação", 74 | "subtask": true, 75 | "avatarId": 22860 76 | }, 77 | "timespent": 600, 78 | "customfield_17132": { 79 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 80 | "value": "2 - Simples", 81 | "id": "15804" 82 | }, 83 | "project": { 84 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 85 | "id": "17274", 86 | "key": "TEST", 87 | "name": "Project", 88 | "avatarUrls": { 89 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 90 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 91 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 92 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 93 | }, 94 | "projectCategory": { 95 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 96 | "id": "10002", 97 | "description": "Projetos do setor de novas tecnologias", 98 | "name": "Group" 99 | } 100 | }, 101 | "assignee": { 102 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 103 | "name": "myuser", 104 | "key": "user.group", 105 | "emailAddress": "myuser@mycompany.com.br", 106 | "avatarUrls": { 107 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 108 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 109 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 110 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 111 | }, 112 | "displayName": "myuser", 113 | "active": true, 114 | "timeZone": "America/Sao_Paulo" 115 | }, 116 | "customfield_21711": "Programação" 117 | } 118 | }, { 119 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 120 | "id": "690212", 121 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/690212", 122 | "key": "TEST-115", 123 | "fields": { 124 | "issuetype": { 125 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/7", 126 | "id": "7", 127 | "description": "Tarefa de programação da pendência", 128 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22860&avatarType=issuetype", 129 | "name": "Programação", 130 | "subtask": true, 131 | "avatarId": 22860 132 | }, 133 | "timespent": 600, 134 | "customfield_17132": { 135 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 136 | "value": "3 - Média", 137 | "id": "15804" 138 | }, 139 | "project": { 140 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 141 | "id": "17274", 142 | "key": "TEST", 143 | "name": "Project", 144 | "avatarUrls": { 145 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 146 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 147 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 148 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 149 | }, 150 | "projectCategory": { 151 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 152 | "id": "10002", 153 | "description": "Projetos do setor de novas tecnologias", 154 | "name": "Group" 155 | } 156 | }, 157 | "assignee": { 158 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 159 | "name": "myuser", 160 | "key": "user.group", 161 | "emailAddress": "myuser@mycompany.com.br", 162 | "avatarUrls": { 163 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 164 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 165 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 166 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 167 | }, 168 | "displayName": "myuser", 169 | "active": true, 170 | "timeZone": "America/Sao_Paulo" 171 | }, 172 | "customfield_21711": "Programação" 173 | } 174 | }, { 175 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 176 | "id": "689869", 177 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/689869", 178 | "key": "TEST-112", 179 | "fields": { 180 | "issuetype": { 181 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/7", 182 | "id": "7", 183 | "description": "Tarefa de programação da pendência", 184 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22860&avatarType=issuetype", 185 | "name": "Programação", 186 | "subtask": true, 187 | "avatarId": 22860 188 | }, 189 | "timespent": 780, 190 | "customfield_17132": { 191 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 192 | "value": "4 - Difícil", 193 | "id": "15804" 194 | }, 195 | "project": { 196 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 197 | "id": "17274", 198 | "key": "TEST", 199 | "name": "Project", 200 | "avatarUrls": { 201 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 202 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 203 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 204 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 205 | }, 206 | "projectCategory": { 207 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 208 | "id": "10002", 209 | "description": "Projetos do setor de novas tecnologias", 210 | "name": "Group" 211 | } 212 | }, 213 | "assignee": { 214 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 215 | "name": "myuser", 216 | "key": "user.group", 217 | "emailAddress": "myuser@mycompany.com.br", 218 | "avatarUrls": { 219 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 220 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 221 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 222 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 223 | }, 224 | "displayName": "myuser", 225 | "active": true, 226 | "timeZone": "America/Sao_Paulo" 227 | }, 228 | "customfield_21711": "Programação" 229 | } 230 | }, { 231 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 232 | "id": "689865", 233 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/689865", 234 | "key": "TEST-110", 235 | "fields": { 236 | "issuetype": { 237 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/7", 238 | "id": "7", 239 | "description": "Tarefa de programação da pendência", 240 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22860&avatarType=issuetype", 241 | "name": "Programação", 242 | "subtask": true, 243 | "avatarId": 22860 244 | }, 245 | "timespent": 720, 246 | "customfield_17132": { 247 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 248 | "value": "5 - Muito difícil", 249 | "id": "15804" 250 | }, 251 | "project": { 252 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 253 | "id": "17274", 254 | "key": "TEST", 255 | "name": "Project", 256 | "avatarUrls": { 257 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 258 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 259 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 260 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 261 | }, 262 | "projectCategory": { 263 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 264 | "id": "10002", 265 | "description": "Projetos do setor de novas tecnologias", 266 | "name": "Group" 267 | } 268 | }, 269 | "assignee": { 270 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 271 | "name": "myuser", 272 | "key": "user.group", 273 | "emailAddress": "myuser@mycompany.com.br", 274 | "avatarUrls": { 275 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 276 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 277 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 278 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 279 | }, 280 | "displayName": "myuser", 281 | "active": true, 282 | "timeZone": "America/Sao_Paulo" 283 | }, 284 | "customfield_21711": "Programação" 285 | } 286 | }, { 287 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 288 | "id": "689866", 289 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/689866", 290 | "key": "TEST-111", 291 | "fields": { 292 | "issuetype": { 293 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/7", 294 | "id": "7", 295 | "description": "Tarefa de programação da pendência", 296 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22860&avatarType=issuetype", 297 | "name": "Programação", 298 | "subtask": true, 299 | "avatarId": 22860 300 | }, 301 | "timespent": 720, 302 | "customfield_17132": { 303 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 304 | "value": "1 - Muito simples", 305 | "id": "15804" 306 | }, 307 | "project": { 308 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 309 | "id": "17274", 310 | "key": "TEST", 311 | "name": "Project", 312 | "avatarUrls": { 313 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 314 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 315 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 316 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 317 | }, 318 | "projectCategory": { 319 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 320 | "id": "10002", 321 | "description": "Projetos do setor de novas tecnologias", 322 | "name": "Group" 323 | } 324 | }, 325 | "assignee": { 326 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 327 | "name": "myuser", 328 | "key": "user.group", 329 | "emailAddress": "myuser@mycompany.com.br", 330 | "avatarUrls": { 331 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 332 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 333 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 334 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 335 | }, 336 | "displayName": "myuser", 337 | "active": true, 338 | "timeZone": "America/Sao_Paulo" 339 | }, 340 | "customfield_21711": "Programação" 341 | } 342 | }, { 343 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 344 | "id": "690161", 345 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/690161", 346 | "key": "TEST-113", 347 | "fields": { 348 | "issuetype": { 349 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/23", 350 | "id": "23", 351 | "description": "Uma tarefa que precisa ser feita.", 352 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22878&avatarType=issuetype", 353 | "name": "Atendimento", 354 | "subtask": false, 355 | "avatarId": 22878 356 | }, 357 | "timespent": 3600, 358 | "customfield_17132": { 359 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 360 | "value": "0 - Não classificado", 361 | "id": "15804" 362 | }, 363 | "project": { 364 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 365 | "id": "17274", 366 | "key": "TEST", 367 | "name": "Project", 368 | "avatarUrls": { 369 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 370 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 371 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 372 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 373 | }, 374 | "projectCategory": { 375 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 376 | "id": "10002", 377 | "description": "Projetos do setor de novas tecnologias", 378 | "name": "Group" 379 | } 380 | }, 381 | "assignee": { 382 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 383 | "name": "myuser", 384 | "key": "user.group", 385 | "emailAddress": "myuser@mycompany.com.br", 386 | "avatarUrls": { 387 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 388 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 389 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 390 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 391 | }, 392 | "displayName": "myuser", 393 | "active": true, 394 | "timeZone": "America/Sao_Paulo" 395 | }, 396 | "customfield_21711": "Atendimento" 397 | } 398 | }, { 399 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 400 | "id": "689864", 401 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/689864", 402 | "key": "TEST-814", 403 | "fields": { 404 | "issuetype": { 405 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/7", 406 | "id": "7", 407 | "description": "Tarefa de programação da pendência", 408 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22860&avatarType=issuetype", 409 | "name": "Programação", 410 | "subtask": true, 411 | "avatarId": 22860 412 | }, 413 | "timespent": 2460, 414 | "customfield_17132": { 415 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 416 | "value": "0 - Não classificado", 417 | "id": "15804" 418 | }, 419 | "project": { 420 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 421 | "id": "17274", 422 | "key": "TEST", 423 | "name": "Project", 424 | "avatarUrls": { 425 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 426 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 427 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 428 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 429 | }, 430 | "projectCategory": { 431 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 432 | "id": "10002", 433 | "description": "Projetos do setor de novas tecnologias", 434 | "name": "Group" 435 | } 436 | }, 437 | "assignee": { 438 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 439 | "name": "myuser", 440 | "key": "user.group", 441 | "emailAddress": "myuser@mycompany.com.br", 442 | "avatarUrls": { 443 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 444 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 445 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 446 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 447 | }, 448 | "displayName": "myuser", 449 | "active": true, 450 | "timeZone": "America/Sao_Paulo" 451 | }, 452 | "customfield_21711": "Programação" 453 | } 454 | }, { 455 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 456 | "id": "689395", 457 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/689395", 458 | "key": "TEST-107", 459 | "fields": { 460 | "issuetype": { 461 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/7", 462 | "id": "7", 463 | "description": "Tarefa de programação da pendência", 464 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22860&avatarType=issuetype", 465 | "name": "Programação", 466 | "subtask": true, 467 | "avatarId": 22860 468 | }, 469 | "timespent": 5820, 470 | "customfield_17132": { 471 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 472 | "value": "1 - Muito simples", 473 | "id": "15804" 474 | }, 475 | "project": { 476 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 477 | "id": "17274", 478 | "key": "TEST", 479 | "name": "Project", 480 | "avatarUrls": { 481 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 482 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 483 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 484 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 485 | }, 486 | "projectCategory": { 487 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 488 | "id": "10002", 489 | "description": "Projetos do setor de novas tecnologias", 490 | "name": "Group" 491 | } 492 | }, 493 | "assignee": { 494 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 495 | "name": "myuser", 496 | "key": "user.group", 497 | "emailAddress": "myuser@mycompany.com.br", 498 | "avatarUrls": { 499 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 500 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 501 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 502 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 503 | }, 504 | "displayName": "myuser", 505 | "active": true, 506 | "timeZone": "America/Sao_Paulo" 507 | }, 508 | "customfield_21711": "Programação" 509 | } 510 | }, { 511 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 512 | "id": "689013", 513 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/689013", 514 | "key": "TEST-104", 515 | "fields": { 516 | "issuetype": { 517 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/7", 518 | "id": "7", 519 | "description": "Tarefa de programação da pendência", 520 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22860&avatarType=issuetype", 521 | "name": "Teste", 522 | "subtask": true, 523 | "avatarId": 22860 524 | }, 525 | "timespent": 19740, 526 | "customfield_17132": { 527 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 528 | "value": "1 - Muito simples", 529 | "id": "15804" 530 | }, 531 | "project": { 532 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 533 | "id": "17274", 534 | "key": "TEST", 535 | "name": "Project", 536 | "avatarUrls": { 537 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 538 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 539 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 540 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 541 | }, 542 | "projectCategory": { 543 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 544 | "id": "10002", 545 | "description": "Projetos do setor de novas tecnologias", 546 | "name": "Group" 547 | } 548 | }, 549 | "assignee": { 550 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 551 | "name": "myuser", 552 | "key": "user.group", 553 | "emailAddress": "myuser@mycompany.com.br", 554 | "avatarUrls": { 555 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 556 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 557 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 558 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 559 | }, 560 | "displayName": "myuser", 561 | "active": true, 562 | "timeZone": "America/Sao_Paulo" 563 | }, 564 | "customfield_21711": "Teste" 565 | } 566 | }, { 567 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 568 | "id": "690161", 569 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/690161", 570 | "key": "TEST-113", 571 | "fields": { 572 | "issuetype": { 573 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/23", 574 | "id": "23", 575 | "description": "Uma tarefa que precisa ser feita.", 576 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22878&avatarType=issuetype", 577 | "name": "Tarefa", 578 | "subtask": false, 579 | "avatarId": 22878 580 | }, 581 | "timespent": 7200, 582 | "customfield_17132": { 583 | "self": "http://MYDOMAIN:8080/rest/api/2/customFieldOption/15804", 584 | "value": "0 - Não classificado", 585 | "id": "15804" 586 | }, 587 | "project": { 588 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 589 | "id": "17274", 590 | "key": "TEST", 591 | "name": "Project", 592 | "avatarUrls": { 593 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 594 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 595 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 596 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 597 | }, 598 | "projectCategory": { 599 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 600 | "id": "10002", 601 | "description": "Projetos do setor de novas tecnologias", 602 | "name": "Group" 603 | } 604 | }, 605 | "assignee": { 606 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 607 | "name": "myuser", 608 | "key": "user.group", 609 | "emailAddress": "myuser@mycompany.com.br", 610 | "avatarUrls": { 611 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 612 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 613 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 614 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 615 | }, 616 | "displayName": "myuser", 617 | "active": true, 618 | "timeZone": "America/Sao_Paulo" 619 | }, 620 | "customfield_21711": "Tarefa" 621 | } 622 | }, { 623 | "expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields", 624 | "id": "690161", 625 | "self": "http://MYDOMAIN:8080/rest/api/2/issue/690161", 626 | "key": "TEST-114", 627 | "fields": { 628 | "issuetype": { 629 | "self": "http://MYDOMAIN:8080/rest/api/2/issuetype/23", 630 | "id": "23", 631 | "description": "Um Atendimento feito.", 632 | "iconUrl": "http://MYDOMAIN:8080/secure/viewavatar?size=xsmall&avatarId=22878&avatarType=issuetype", 633 | "name": "Atendimento", 634 | "subtask": false, 635 | "avatarId": 22878 636 | }, 637 | "timespent": 7200, 638 | "customfield_17132": null, 639 | "project": { 640 | "self": "http://MYDOMAIN:8080/rest/api/2/project/17274", 641 | "id": "17274", 642 | "key": "TEST", 643 | "name": "Project", 644 | "avatarUrls": { 645 | "48x48": "http://MYDOMAIN:8080/secure/projectavatar?pid=17274&avatarId=22964", 646 | "24x24": "http://MYDOMAIN:8080/secure/projectavatar?size=small&pid=17274&avatarId=22964", 647 | "16x16": "http://MYDOMAIN:8080/secure/projectavatar?size=xsmall&pid=17274&avatarId=22964", 648 | "32x32": "http://MYDOMAIN:8080/secure/projectavatar?size=medium&pid=17274&avatarId=22964" 649 | }, 650 | "projectCategory": { 651 | "self": "http://MYDOMAIN:8080/rest/api/2/projectCategory/10002", 652 | "id": "10002", 653 | "description": "Projetos do setor de novas tecnologias", 654 | "name": "Group" 655 | } 656 | }, 657 | "assignee": { 658 | "self": "http://MYDOMAIN:8080/rest/api/2/user?username=myuser", 659 | "name": "myuser", 660 | "key": "user.group", 661 | "emailAddress": "myuser@mycompany.com.br", 662 | "avatarUrls": { 663 | "48x48": "http://MYDOMAIN:8080/secure/useravatar?ownerId=user.group&avatarId=23764", 664 | "24x24": "http://MYDOMAIN:8080/secure/useravatar?size=small&ownerId=user.group&avatarId=23764", 665 | "16x16": "http://MYDOMAIN:8080/secure/useravatar?size=xsmall&ownerId=user.group&avatarId=23764", 666 | "32x32": "http://MYDOMAIN:8080/secure/useravatar?size=medium&ownerId=user.group&avatarId=23764" 667 | }, 668 | "displayName": "myuser", 669 | "active": true, 670 | "timeZone": "America/Sao_Paulo" 671 | }, 672 | "customfield_21711": "Atendimento" 673 | } 674 | }] 675 | } --------------------------------------------------------------------------------