├── .cfignore ├── .gitignore ├── webhookHandlers ├── index.js ├── intake │ ├── move-to-iaa-go │ │ ├── create-checklists-on-intake-card.js │ │ ├── create-atc-card.js │ │ └── create-bpa-components.js │ ├── move-to-iaa-go.js │ └── move-to-iaa-completed.js ├── intake.js └── event-types.js ├── .eslintrc ├── logger.js ├── manifest.yml ├── .codeclimate.yml ├── actions ├── create-bpa-order-board.js ├── index.js ├── create-atc-card.js ├── create-bpa-order-card.js └── add-intake-checklist.js ├── test ├── coverage.js ├── webhookHandlers │ ├── move-to-iaa-go │ │ ├── create-checklists-on-intake-card.js │ │ ├── create-atc-card.js │ │ └── create-bpa-components.js │ ├── intake.js │ ├── event-types.js │ ├── move-to-iaa-go.js │ └── move-to-iaa-completed.js ├── actions │ ├── create-bpa-order-board.js │ ├── create-atc-card.js │ ├── create-bpa-order-card.js │ └── add-intake-checklist.js └── trello.js ├── env.js ├── trello.js ├── CONTRIBUTING.md ├── package.json ├── LICENSE.md ├── .travis.yml ├── main.js └── README.md /.cfignore: -------------------------------------------------------------------------------- 1 | .gitignore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .env 4 | .cf-ups.json 5 | .nyc_output 6 | coverage 7 | -------------------------------------------------------------------------------- /webhookHandlers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const intake = require('./intake'); 4 | 5 | module.exports = { 6 | intake 7 | }; 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "comma-dangle": [ 1, "never" ], 5 | "max-len": 0, 6 | "strict": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /webhookHandlers/intake/move-to-iaa-go/create-checklists-on-intake-card.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const actions = require('../../../actions'); 4 | 5 | module.exports = function createChecklists(e) { 6 | return actions.addIntakeChecklist(e.action.data.card.id); 7 | }; 8 | -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Logger = require('@erdc-itl/simple-logger'); 4 | Logger.setOptions({ 5 | level: (process.env.LOG_LEVEL || 10), 6 | console: true 7 | }); 8 | 9 | module.exports = function getLogger(name) { 10 | return new Logger(name); 11 | }; 12 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: acq-trello-listener 4 | buildpack: https://github.com/cloudfoundry/nodejs-buildpack.git#v1.5.14 5 | memory: 512M 6 | disk_quota: 256M 7 | instances: 1 8 | host: acq-trello-listener 9 | services: 10 | - acq-trello-cups 11 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - javascript 8 | eslint: 9 | enabled: true 10 | fixme: 11 | enabled: true 12 | ratings: 13 | paths: 14 | - "**.js" 15 | exclude_paths: 16 | - test/ 17 | - bin/ 18 | - demo/ 19 | -------------------------------------------------------------------------------- /actions/create-bpa-order-board.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const trello = require('../trello'); 4 | 5 | module.exports = function createBoard(name) { 6 | const meta = { 7 | name, 8 | defaultLists: false, 9 | prefs_permissionLevel: 'private' 10 | }; 11 | 12 | return trello.post('/1/boards', meta); 13 | }; 14 | -------------------------------------------------------------------------------- /test/coverage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // require() all the things, so they get included 4 | // in the coverage reports. 5 | 6 | process.env.TRELLO_API_KEY = 'trello-api-key'; 7 | process.env.TRELLO_API_TOK = 'trello-api-tok'; 8 | 9 | require('../actions'); 10 | require('../webhookHandlers'); 11 | 12 | require('tap').pass('Coverage initialized'); 13 | -------------------------------------------------------------------------------- /actions/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const addIntakeChecklist = require('./add-intake-checklist'); 4 | const createBPAOrderBoard = require('./create-bpa-order-board'); 5 | const createBPAOrderCard = require('./create-bpa-order-card'); 6 | const createATCCard = require('./create-atc-card'); 7 | 8 | module.exports = { 9 | addIntakeChecklist, 10 | createATCCard, 11 | createBPAOrderBoard, 12 | createBPAOrderCard 13 | }; 14 | -------------------------------------------------------------------------------- /actions/create-atc-card.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const moment = require('moment'); 4 | const trello = require('../trello'); 5 | 6 | module.exports = function createATCCard(cardName, intakeCardURL) { 7 | if (process.env.TRELLO_ATC_PREFLIGHT_LIST_ID) { 8 | return trello.post('/1/cards/', { 9 | name: cardName, 10 | idList: process.env.TRELLO_ATC_PREFLIGHT_LIST_ID, 11 | desc: `\n\n---\n* [Intake](${intakeCardURL})` 12 | }); 13 | } else { 14 | return Promise.reject(new Error('ATC Preflight list ID not ready')); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /webhookHandlers/intake.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const iaaGo = require('./intake/move-to-iaa-go') 4 | const iaaCompleted = require('./intake/move-to-iaa-completed') 5 | 6 | module.exports = function executeIntakeHandlers(e) { 7 | // Wraps functions in a 0-argument shell and captures 8 | // the event variable in a closure. This way the 9 | // wrapped functions can be passed directly into 10 | // then/catch. 11 | const func = fn => (() => fn(e)); 12 | 13 | const iaaCompletedPromise = func(iaaCompleted); 14 | 15 | return iaaGo(e) 16 | .then(iaaCompletedPromise, iaaCompletedPromise) 17 | .catch(() => null); 18 | } 19 | -------------------------------------------------------------------------------- /actions/create-bpa-order-card.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const moment = require('moment'); 4 | const trello = require('../trello'); 5 | 6 | module.exports = function createBPAOrderCard(cardName, agency, subagency, managementBoardURL) { 7 | if (process.env.TRELLO_BPA_IAA_LIST_ID) { 8 | return trello.post('/1/cards/', { 9 | name: cardName, 10 | desc: `* Project: \n* Agency: ${agency}\n* SubAgency: ${subagency}\n* Trello Board: ${managementBoardURL}\n* Open date: ${moment(new Date()).format('M/D/YY')}`, 11 | idList: process.env.TRELLO_BPA_IAA_LIST_ID 12 | }); 13 | } else { 14 | return Promise.reject('BPA IAA list ID not ready'); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /webhookHandlers/event-types.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CARD_MOVED_TYPE = 'CardMoved'; 4 | const LABEL_ADDED_TYPE = 'LabelAdded'; 5 | 6 | module.exports = function getEventType(trelloEvent) { 7 | let eventType = null; 8 | try { 9 | if (trelloEvent.action.type === 'updateCard' && trelloEvent.action.data.listAfter && trelloEvent.action.data.listBefore) { 10 | eventType = CARD_MOVED_TYPE; 11 | } 12 | else if(trelloEvent.action.type === 'addLabelToCard') { 13 | eventType = LABEL_ADDED_TYPE; 14 | } 15 | } catch (e) { eventType = null; } 16 | return eventType; 17 | }; 18 | 19 | module.exports.CardMoved = CARD_MOVED_TYPE; 20 | module.exports.LabelAdded = LABEL_ADDED_TYPE; 21 | -------------------------------------------------------------------------------- /env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('dotenv').config(); 4 | const cfenv = require('cfenv'); 5 | const appEnv = cfenv.getAppEnv(); 6 | 7 | const knownEnvs = [ 8 | 'TRELLO_INTAKE_BOARD_ID', 9 | 'TRELLO_BPA_BOARD_ID', 10 | 'TRELLO_ATC_BOARD_ID', 11 | 'TRELLO_API_KEY', 12 | 'TRELLO_API_TOK', 13 | 'TRELLO_CLIENT_SECRET', 14 | 'LOG_LEVEL' 15 | ]; 16 | 17 | if (appEnv.getServices() && Object.keys(appEnv.getServices()).length) { 18 | // If running on Cloud Foundry 19 | for (const env of knownEnvs) { 20 | process.env[env] = appEnv.getServiceCreds('acq-trello-cups')[env]; 21 | } 22 | process.env.HOST = appEnv.url; 23 | 24 | if (!process.env.TRELLO_WEBHOOK_HOST) { 25 | process.env.TRELLO_WEBHOOK_HOST = process.env.HOST; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /trello.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const NodeTrello = require('node-trello'); 4 | const trello = new NodeTrello(process.env.TRELLO_API_KEY, process.env.TRELLO_API_TOK); 5 | 6 | function promisify(fn) { 7 | const ofn = fn.bind(trello); 8 | return function(...args) { 9 | if(args.length && typeof args[args.length - 1] === 'function') { 10 | ofn(...args); 11 | } else { 12 | return new Promise((resolve, reject) => { 13 | ofn(...args, (err, ...data) => { 14 | if(err) { 15 | return reject(err); 16 | } 17 | return resolve(...data); 18 | }); 19 | }); 20 | } 21 | } 22 | } 23 | 24 | module.exports = { 25 | get: promisify(trello.get), 26 | put: promisify(trello.put), 27 | post: promisify(trello.post) 28 | }; 29 | -------------------------------------------------------------------------------- /actions/add-intake-checklist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const trello = require('../trello'); 4 | 5 | const checklistName = 'Intake Forms'; 6 | 7 | module.exports = function addIntakeChecklist(cardID) { 8 | let checklistID; 9 | return trello.get(`/1/cards/${cardID}/checklists`) 10 | .then(checklists => { 11 | if (checklists.some(list => list.name === checklistName)) { 12 | throw new Error('Intake card already has an intake forms checklist'); 13 | } 14 | 15 | return trello.post('/1/checklists', { 16 | idCard: cardID, 17 | name: checklistName 18 | }); 19 | }).then(checklist => { 20 | checklistID = checklist.id; 21 | return trello.post(`/1/checklists/${checklistID}/checkItems`, { 22 | name: '7600 SOW' 23 | }); 24 | }) 25 | .then(() => trello.post(`/1/checklists/${checklistID}/checkItems`, { name: 'Budget Estimate' })); 26 | }; 27 | -------------------------------------------------------------------------------- /webhookHandlers/intake/move-to-iaa-go.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const trello = require('../../trello'); 4 | const actions = require('../../actions'); 5 | const eventTypes = require('../event-types'); 6 | const log = require('../../logger')('intake handler: IAA Go'); 7 | 8 | const createChecklists = require('./move-to-iaa-go/create-checklists-on-intake-card'); 9 | const createATCCard = require('./move-to-iaa-go/create-atc-card'); 10 | const createBPAComponents = require('./move-to-iaa-go/create-bpa-components'); 11 | 12 | module.exports = function handleIntakeWebhookEvent(e) { 13 | if (eventTypes(e) === eventTypes.CardMoved && e.action.data.listAfter.name.startsWith('IAA Go')) { 14 | return createChecklists(e) 15 | .then(() => createATCCard(e)) 16 | .then(() => createBPAComponents(e)) 17 | } else { 18 | return Promise.reject(new Error('Not a move to IAA Go')); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /webhookHandlers/intake/move-to-iaa-go/create-atc-card.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const trello = require('../../../trello'); 4 | const actions = require('../../../actions'); 5 | 6 | module.exports = function createATCCard(e) { 7 | let intakeCard; 8 | return trello.get(`/1/cards/${e.action.data.card.id}`) 9 | .then(card => { 10 | intakeCard = card; 11 | if(card.desc.match(/(^|\n)---\n\n\* \[Air Traffic Control\]/)) { 12 | throw new Error('Intake card already has a link to ATC card'); 13 | } 14 | return actions.createATCCard(card.name, card.url); 15 | }) 16 | .then(atcCard => { 17 | let newDesc = intakeCard.desc; 18 | if (newDesc) { 19 | newDesc += '\n\n'; 20 | } 21 | newDesc += `---\n\n* [Air Traffic Control](${atcCard.url})`; 22 | 23 | return trello.put(`/1/cards/${intakeCard.id}/desc`, { value: newDesc }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Welcome! 2 | 3 | We're so glad you're thinking about contributing to an 18F open source project! If you're unsure about anything, just ask -- or submit the issue or pull request anyway. The worst that can happen is you'll be politely asked to change something. We love all friendly contributions. 4 | 5 | We want to ensure a welcoming environment for all of our projects. Our staff follow the [18F Code of Conduct](https://github.com/18F/code-of-conduct/blob/master/code-of-conduct.md) and all contributors should do the same. 6 | 7 | We encourage you to read this project's CONTRIBUTING policy (you are here), its [LICENSE](LICENSE.md), and its [README](README.md). 8 | 9 | If you have any questions or want to read more, check out the [18F Open Source Policy GitHub repository]( https://github.com/18f/open-source-policy), or just [shoot us an email](mailto:18f@gsa.gov). 10 | 11 | ## Public domain 12 | 13 | This project is in the public domain within the United States, and 14 | copyright and related rights in the work worldwide are waived through 15 | the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 16 | 17 | All contributions to this project will be released under the CC0 18 | dedication. By submitting a pull request, you are agreeing to comply 19 | with this waiver of copyright interest. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "acq-trello-listener", 3 | "version": "1.1.0", 4 | "description": "AcqStack Trello listener server", 5 | "main": "main.js", 6 | "keywords": [ 7 | "" 8 | ], 9 | "author": "Greg Walker ", 10 | "license": "CC0-1.0", 11 | "scripts": { 12 | "start": "node main.js", 13 | "cf-ups-dev": "node -e 'console.log(\"'\"'\"'\" + JSON.stringify(require(\"./.cf-ups.json\").dev) + \"'\"'\"'\")'", 14 | "cf-ups-prod": "node -e 'console.log(\"'\"'\"'\" + JSON.stringify(require(\"./.cf-ups.json\").prod) + \"'\"'\"'\")'", 15 | "lint": "eslint --fix **/*.js", 16 | "test": "tap -Rspec --cov test/coverage.js 'test/**/*.js'" 17 | }, 18 | "dependencies": { 19 | "@18f/trello-webhook-server": "^3.1.0", 20 | "@erdc-itl/simple-logger": "^1.1.0", 21 | "cfenv": "^1.0.3", 22 | "dotenv": "^2.0.0", 23 | "moment": "^2.13.0", 24 | "node-trello": "^1.1.2" 25 | }, 26 | "engines": { 27 | "node": "^6.1.0", 28 | "npm": "^3.8.6" 29 | }, 30 | "devDependencies": { 31 | "eslint": "^2.9.0", 32 | "eslint-config-airbnb": "^9.0.1", 33 | "eslint-plugin-import": "^1.7.0", 34 | "eslint-plugin-jsx-a11y": "^1.2.0", 35 | "eslint-plugin-react": "^5.0.1", 36 | "mock-require": "^1.3.0", 37 | "sinon": "^1.17.4", 38 | "sinon-as-promised": "^4.0.0", 39 | "tap": "^5.7.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | As a work of the United States Government, this project is in the 2 | public domain within the United States. 3 | 4 | Additionally, we waive copyright and related rights in the work 5 | worldwide through the CC0 1.0 Universal public domain dedication. 6 | 7 | ## CC0 1.0 Universal Summary 8 | 9 | This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 10 | 11 | ### No Copyright 12 | 13 | The person who associated a work with this deed has dedicated the work to 14 | the public domain by waiving all of his or her rights to the work worldwide 15 | under copyright law, including all related and neighboring rights, to the 16 | extent allowed by law. 17 | 18 | You can copy, modify, distribute and perform the work, even for commercial 19 | purposes, all without asking permission. 20 | 21 | ### Other Information 22 | 23 | In no way are the patent or trademark rights of any person affected by CC0, 24 | nor are the rights that other persons may have in the work or in how the 25 | work is used, such as publicity or privacy rights. 26 | 27 | Unless expressly stated otherwise, the person who associated a work with 28 | this deed makes no warranties about the work, and disclaims liability for 29 | all uses of the work, to the fullest extent permitted by applicable law. 30 | When using or citing the work, you should not imply endorsement by the 31 | author or the affirmer. 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6.1' 4 | - '6.2' 5 | env: 6 | global: 7 | - CF_USERNAME=18f-acq_deployer 8 | - secure: Yir8usMJgp0JYw3CHhn8ueaPTwq/uBA4AKTwjauOtVKrWJvciPfolcGg3mUTZvAUVMI5KatkDaqHdLSk1uCyThrg7vaanv/mJ7GDJLGPstsCgRexw7bJG3a8wX3gi7PP8z1+iSb/MjJdKdBZSrj6pGt0vwTtNU5GotQqJGgMLfMVtYl2zIUiayDi47P/MPeEGYWRy/16RMklMobVraEFl5e0QIZbF8QLiO20xqAq/mdPbMAODePDr+qQJhoEg8DoABfl1UOqVb3fRAyo244Wx4enFUrU1jirD7scY3M2ENXjDCVjRmkI/0Poi4O1G3O3cyh0XsQVkpkE6kCBBH8hkL2KZMgzbGqxtA6yK1eJ9BskR1h3SbwwRdC8+AXYRBMnY8ZaikphwgkcBPauitznFG6j4k1lVLQFtJPNgTYCZybh8KHeVRPjCe3k4qpEYp8CjQoDenDI6VQK2jhrdNzRn6ev8RPszW+ZoGzBsCIy6fRc10Zuw4lQsq10XCEPuFoQTGRwVF4z1U09TCDlibJQPC4c56awXe9T4GpHFWOf781tp2Jmy9oBAvBKJR/f7nCOX6Nk01A4p4RtFV/kPWw2UtU+9WASnXGStXXP9rn3jKJO7Th8/J9fbbXdm8mfA1eUZtTlaJpadiDc4KWbyut/PPHqiBmSgrPJndI+m8JXYsU= 9 | cache: 10 | directories: 11 | - "$HOME/node_modules" 12 | before_deploy: 13 | - export PATH=$HOME:$PATH 14 | - travis_retry curl -L -o $HOME/cf.tgz "https://cli.run.pivotal.io/stable?release=linux64-binary&version=6.15.0" 15 | - tar xzvf $HOME/cf.tgz -C $HOME 16 | - mkdir -p ${HOME}/Godeps/_workspace 17 | - export GOPATH=${HOME}/Godeps/_workspace 18 | - go get github.com/concourse/autopilot 19 | - cf install-plugin -f $GOPATH/bin/autopilot 20 | - travis_retry curl -L -o $HOME/18f.zip "https://github.com/18F/18f-cli/archive/release.zip" 21 | - unzip $HOME/18f.zip -d $HOME/18f-cli 22 | deploy: 23 | - provider: script 24 | script: bash $HOME/18f-cli/18f-cli-release/bin/deploy -o 18f-acq -s tools acq-trello-listener 25 | skip_cleanup: true 26 | on: 27 | branch: master 28 | -------------------------------------------------------------------------------- /webhookHandlers/intake/move-to-iaa-go/create-bpa-components.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const trello = require('../../../trello'); 4 | const actions = require('../../../actions'); 5 | 6 | module.exports = function createBPAComponents(e) { 7 | if (e.action.data.card.name.startsWith('Agile BPA')) { 8 | let card; 9 | let boardURL; 10 | return trello.get(`/1/cards/${e.action.data.card.id}`) 11 | .then(intakeCard => { 12 | // It's possible this card was moved back into the 13 | // IAA Go list from a later list and already has 14 | // an associated BPA card and board. So, we need 15 | // to check the description. 16 | if (intakeCard.desc.match(/(^|\n\n)---\n\n### Agile BPA Links\n\n/)) { 17 | throw new Error('Intake card already has links to BPA dashboard and BPA project management board'); 18 | } 19 | card = intakeCard; 20 | return actions.createBPAOrderBoard(card.name); 21 | }) 22 | .then(board => { 23 | boardURL = board.url; 24 | const bits = card.name.match(/Agile BPA\s?(\/|-)([^\/-]*)(\/|-)(.*)/); 25 | const agency = bits[2].trim(); 26 | const project = bits[4].trim(); 27 | 28 | // And then create a BPA dashboard card. 29 | return actions.createBPAOrderCard(project, agency, '', boardURL); 30 | }) 31 | .then(bpaCard => { 32 | let newDesc = card.desc; 33 | if (newDesc) { 34 | newDesc += '\n\n'; 35 | } 36 | newDesc += `---\n\n### Agile BPA Links\n\n* [Management Board](${boardURL})\n* [BPA Dashboard](${bpaCard.url})`; 37 | 38 | return trello.put(`/1/cards/${card.id}/desc`, { value: newDesc }); 39 | }); 40 | } else { 41 | return Promise.reject('Not an Agile BPA card'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('./env'); 3 | const TrelloWebhookServer = require('@18f/trello-webhook-server'); 4 | const log = require('./logger')('main'); 5 | const webhookHandlers = require('./webhookHandlers'); 6 | const trello = require('./trello'); 7 | 8 | const httpServer = require('http').createServer(); 9 | 10 | const intakeListener = new TrelloWebhookServer({ 11 | server: httpServer, 12 | hostURL: process.env.TRELLO_WEBHOOK_HOST, 13 | apiKey: process.env.TRELLO_API_KEY, 14 | apiToken: process.env.TRELLO_API_TOK, 15 | clientSecret: process.env.TRELLO_CLIENT_SECRET 16 | }); 17 | 18 | httpServer.listen(process.env.PORT, () => { 19 | intakeListener.start(process.env.TRELLO_INTAKE_BOARD_ID) 20 | .then(webhookID => { 21 | log.info(`Intake webhook ID: ${webhookID}`); 22 | intakeListener.on('data', webhookHandlers.intake); 23 | }) 24 | .catch(e => { 25 | log.error(`Error setting up intake webhook listener:`); 26 | log.error(e); 27 | }); 28 | }); 29 | 30 | trello.get(`/1/boards/${process.env.TRELLO_BPA_BOARD_ID}/lists`) 31 | .then(lists => { 32 | const sortedLists = lists.sort((a, b) => a.pos - b.pos); 33 | const iaaList = sortedLists.filter(a => a.name.startsWith('IAA'))[0]; 34 | const workshopPrepList = sortedLists.filter(a => a.name.startsWith('Workshop Prep'))[0]; 35 | process.env.TRELLO_BPA_IAA_LIST_ID = iaaList.id; 36 | process.env.TRELLO_BPA_WORKSHOP_PREP_LIST_ID = workshopPrepList.id; 37 | }); 38 | 39 | trello.get(`/1/boards/${process.env.TRELLO_ATC_BOARD_ID}/lists`) 40 | .then(lists => { 41 | const sortedLists = lists.sort((a, b) => a.pos - b.pos); 42 | const preflightList = sortedLists.filter(a => a.name.startsWith('Preflight'))[0]; 43 | process.env.TRELLO_ATC_PREFLIGHT_LIST_ID = preflightList.id; 44 | }); 45 | -------------------------------------------------------------------------------- /webhookHandlers/intake/move-to-iaa-completed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const trello = require('../../trello'); 4 | const actions = require('../../actions'); 5 | const eventTypes = require('../event-types'); 6 | const log = require('../../logger')('intake handler: IAA Completed'); 7 | 8 | const bpaStartsWith = 'Agile BPA'; 9 | const iaaCompleteStartsWith = 'IAA Completed Work Begin'; 10 | 11 | const bpaURLRegex = /(^|\n)### Agile BPA Links\n[\s\S]*\n\* \[BPA Dashboard\]\((http.*?)\)(\n|$)/; 12 | const bpaCardIDRegex = /https:\/\/trello\.com\/c\/([^\/]+)\/.+/; 13 | 14 | module.exports = function handleIntakeWebhookEvent(e) { 15 | if(!process.env.TRELLO_BPA_WORKSHOP_PREP_LIST_ID) { 16 | log.warn('BPA Workshop Prep list ID not ready'); 17 | return Promise.reject(new Error('BPA Workshop Prep list ID not ready')); 18 | } 19 | 20 | if(eventTypes(e) !== eventTypes.CardMoved) { 21 | log.verbose('Not a card move'); 22 | return Promise.reject(new Error('Not a card move')); 23 | } 24 | 25 | if(!e.action.data.listAfter.name.startsWith(iaaCompleteStartsWith)) { 26 | log.verbose(`Not a move into ${iaaCompleteStartsWith}`); 27 | return Promise.reject(new Error(`Not a move into ${iaaCompleteStartsWith}`)); 28 | } 29 | 30 | if (!e.action.data.card.name.startsWith(bpaStartsWith)) { 31 | log.verbose(`Not an Agile BPA card`); 32 | return Promise.reject(new Error(`Not an Agile BPA card`)); 33 | } 34 | 35 | return trello.get(`/1/cards/${e.action.data.card.id}`) //, (err, card) => { 36 | .then(card => { 37 | // Make sure this intake card has an associated BPA Dashboard card 38 | if (!card.desc.match(bpaURLRegex)) { 39 | log.info('Intake BPA does not have an associated BPA Dashboard card'); 40 | throw new Error('Intake BPA does not have an associated BPA Dashboard card'); 41 | } 42 | 43 | const bpaCardURL = card.desc.match(bpaURLRegex)[2]; 44 | const bpaCardID = bpaCardURL.match(bpaCardIDRegex)[1]; 45 | 46 | log.info(`Intake card '${card.name}' moved to '${iaaCompleteStartsWith}'. Moving associated BPA Dashboard card.`); 47 | return trello.put(`/1/cards/${bpaCardID}/idList`, { value: process.env.TRELLO_BPA_WORKSHOP_PREP_LIST_ID }); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /test/webhookHandlers/move-to-iaa-go/create-checklists-on-intake-card.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | const sinon = require('sinon'); 5 | require('sinon-as-promised'); 6 | const mockRequire = require('mock-require'); 7 | 8 | const sandbox = sinon.sandbox.create(); 9 | const addChecklistAction = sandbox.stub(); 10 | 11 | mockRequire('../../../actions', { 12 | addIntakeChecklist: addChecklistAction 13 | }); 14 | 15 | // Disable logging to the console. 16 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 17 | 18 | const createChecklistHandler = require('../../../webhookHandlers/intake/move-to-iaa-go/create-checklists-on-intake-card'); 19 | const trelloEvent = { 20 | action: { data: { card: { id: 'intake-card-id' }}} 21 | }; 22 | 23 | tap.test('webhook handlers - intake: move to IAA Go > create checklist on intake card', t1 => { 24 | tap.beforeEach(done => { 25 | sandbox.reset(); 26 | done(); 27 | }); 28 | tap.teardown(() => { 29 | sandbox.restore(); 30 | }); 31 | 32 | t1.test('add intake checklist action rejects', t2 => { 33 | const err = new Error('Test error'); 34 | addChecklistAction.rejects(err); 35 | 36 | createChecklistHandler(trelloEvent) 37 | .then(() => { 38 | t2.fail('rejects'); 39 | }) 40 | .catch(e => { 41 | t2.pass('rejects'); 42 | t2.equal(addChecklistAction.callCount, 1, 'calls add intake checklist action one time'); 43 | t2.equal(addChecklistAction.args[0][0], trelloEvent.action.data.card.id, 'passes the card ID'); 44 | t2.equal(e, err, 'returns the expected error'); 45 | }) 46 | .then(t2.done); 47 | }); 48 | 49 | t1.test('add intake checklist action resolves', t2 => { 50 | addChecklistAction.resolves(); 51 | 52 | createChecklistHandler(trelloEvent) 53 | .then(() => { 54 | t2.pass('resolves'); 55 | t2.equal(addChecklistAction.callCount, 1, 'calls add intake checklist action one time'); 56 | t2.equal(addChecklistAction.args[0][0], trelloEvent.action.data.card.id, 'passes the card ID'); 57 | }) 58 | .catch(e => { 59 | t2.fail('resolves'); 60 | }) 61 | .then(t2.done); 62 | }); 63 | 64 | t1.done(); 65 | }); 66 | -------------------------------------------------------------------------------- /test/webhookHandlers/intake.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | const sinon = require('sinon'); 5 | require('sinon-as-promised'); 6 | const mockRequire = require('mock-require'); 7 | 8 | const sandbox = sinon.sandbox.create(); 9 | const iaaGo = sandbox.mock(); 10 | const iaaCompleted = sandbox.mock(); 11 | 12 | mockRequire('../../webhookHandlers/intake/move-to-iaa-go', iaaGo); 13 | mockRequire('../../webhookHandlers/intake/move-to-iaa-completed', iaaCompleted); 14 | 15 | // Disable logging to the console. 16 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 17 | 18 | const intake = require('../../webhookHandlers/intake'); 19 | 20 | tap.beforeEach(done => { 21 | sandbox.reset(); 22 | done(); 23 | }); 24 | tap.teardown(() => { 25 | sandbox.restore(); 26 | mockRequire.stopAll(); 27 | }); 28 | 29 | tap.test('webhook handlers - intake', t1 => { 30 | const err = new Error('Test error'); 31 | const eventObj = { }; 32 | 33 | const actualTest = test => { 34 | intake(eventObj) 35 | .then(() => { 36 | test.pass('resolves'); 37 | test.equal(iaaGo.callCount, 1, 'calls IAA Go handler one time'); 38 | test.equal(iaaGo.args[0][0], eventObj, 'passes the event to IAA Go handler'); 39 | test.equal(iaaCompleted.callCount, 1, 'calls IAA Completed handler one time'); 40 | test.equal(iaaCompleted.args[0][0], eventObj, 'passes the event to IAA Completed handler'); 41 | }) 42 | .catch(() => { 43 | test.fail('resolves'); 44 | }) 45 | .then(test.done); 46 | }; 47 | 48 | t1.test('IAA Go and IAA Completed handlers reject', t2 => { 49 | iaaGo.rejects(err); 50 | iaaCompleted.rejects(err); 51 | actualTest(t2); 52 | }); 53 | 54 | t1.test('IAA Go handler rejects and IAA Completed handler resolves', t2 => { 55 | iaaGo.rejects(err); 56 | iaaCompleted.resolves(); 57 | actualTest(t2); 58 | }); 59 | 60 | t1.test('IAA Go handler resolves and IAA Completed handler rejects', t2 => { 61 | iaaGo.resolves(); 62 | iaaCompleted.rejects(err); 63 | actualTest(t2); 64 | }); 65 | 66 | t1.test('IAA Go and IAA Completed handlers resolve', t2 => { 67 | iaaGo.resolves(); 68 | iaaCompleted.resolves(); 69 | actualTest(t2); 70 | }); 71 | 72 | t1.done(); 73 | }); 74 | -------------------------------------------------------------------------------- /test/actions/create-bpa-order-board.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | const sinon = require('sinon'); 5 | const trello = require('node-trello'); 6 | 7 | process.env.TRELLO_API_KEY = 'trello-api-key'; 8 | process.env.TRELLO_API_TOK = 'trello-api-tok'; 9 | 10 | const sandbox = sinon.sandbox.create(); 11 | const trelloPost = sandbox.stub(trello.prototype, 'post'); 12 | 13 | const createBPAOrderBoard = require('../../actions/create-bpa-order-board'); 14 | 15 | // Disable logging to the console. 16 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 17 | 18 | tap.test('actions - create BPA order board', t1 => { 19 | const boardName = 'test-board-name'; 20 | 21 | t1.beforeEach(done => { 22 | sandbox.reset(); 23 | done(); 24 | }); 25 | 26 | t1.teardown(() => { 27 | sandbox.restore(); 28 | }); 29 | 30 | t1.test('Trello post returns an error', t2 => { 31 | const err = new Error('Test error'); 32 | trelloPost.yields(err, null); 33 | createBPAOrderBoard(boardName) 34 | .then(() => { 35 | t2.fail('rejects'); 36 | }) 37 | .catch(e => { 38 | t2.pass('rejects'); 39 | t2.equal(trelloPost.callCount, 1, 'calls Trello post one time'); 40 | t2.equal(trelloPost.args[0][0], '/1/boards', 'calls the correct URL'); 41 | t2.same(trelloPost.args[0][1], { name: boardName, defaultLists: false, prefs_permissionLevel: 'private' }, 'sends the correct board metadata'); 42 | t2.equal(e, err, 'returns the expected error'); 43 | }) 44 | .then(t2.done); 45 | }); 46 | 47 | t1.test('Trello post returns successfully', t2 => { 48 | const boardURL = 'https://some.board.url/asdf'; 49 | trelloPost.yields(null, { url: boardURL }); 50 | createBPAOrderBoard(boardName) 51 | .then(board => { 52 | t2.pass('resolves'); 53 | t2.equal(trelloPost.callCount, 1, 'calls Trello post one time'); 54 | t2.equal(trelloPost.args[0][0], '/1/boards', 'calls the correct URL'); 55 | t2.same(trelloPost.args[0][1], { name: boardName, defaultLists: false, prefs_permissionLevel: 'private' }, 'sends the correct board metadata'); 56 | t2.equal(typeof board, 'object', 'resolves a board object'); 57 | t2.equal(board.url, boardURL, 'resolved board contains the correct board URL'); 58 | }) 59 | .catch(() => { 60 | t2.fail('resolves'); 61 | }) 62 | .then(t2.done); 63 | }); 64 | 65 | t1.done(); 66 | }); 67 | -------------------------------------------------------------------------------- /test/webhookHandlers/event-types.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | const getEventType = require('../../webhookHandlers/event-types'); 5 | 6 | // Disable logging to the console. 7 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 8 | 9 | tap.test('webhook handlers - event types', t1 => { 10 | t1.test('With no data', t2 => { 11 | const eventType = getEventType(); 12 | t2.equal(eventType, null, 'event type should be null'); 13 | t2.done(); 14 | }); 15 | 16 | t1.test('With non-Trello data', t2 => { 17 | const eventType = getEventType({ some: 'fake', data: 'goes', here: true }); 18 | t2.equal(eventType, null, 'event type should be null'); 19 | t2.done(); 20 | }); 21 | 22 | t1.test('With Trello data', t2 => { 23 | t2.test('With unknown action type', t3 => { 24 | const eventType = getEventType({ action: { type: 'Unknown' }}); 25 | t3.equal(eventType, null, 'event type should be null'); 26 | t3.done(); 27 | }); 28 | 29 | t2.test('With updateCard action type', t3 => { 30 | const trelloEvent = { 31 | action: { type: 'updateCard' } 32 | }; 33 | 34 | t3.test('Without listAfter or listBefore', t4 => { 35 | const eventType = getEventType({ action: { type: 'updateCard' }}); 36 | t4.equal(eventType, null, 'event type should be null'); 37 | t4.done(); 38 | }); 39 | 40 | t3.test('Without listAfter', t4 => { 41 | const eventType = getEventType({ action: { type: 'updateCard', data: { listBefore: 'list-before-id' }}}); 42 | t4.equal(eventType, null, 'event type should be null'); 43 | t4.done(); 44 | }); 45 | 46 | t3.test('Without listBefore', t4 => { 47 | const eventType = getEventType({ action: { type: 'updateCard', data: { listAfter: 'list-after-id' }}}); 48 | t4.equal(eventType, null, 'event type should be null'); 49 | t4.done(); 50 | }); 51 | 52 | t3.test('With listAfter and listBefore', t4 => { 53 | const eventType = getEventType({ action: { type: 'updateCard', data: { listAfter: 'list-after-id', listBefore: 'list-before-id' }}}); 54 | t4.equal(eventType, getEventType.CardMoved, 'event type should be CARD_MOVED_TYPE'); 55 | t4.done(); 56 | }); 57 | 58 | t3.done(); 59 | }); 60 | 61 | t2.test('With addLabelToCard type', t3 => { 62 | const eventType = getEventType({ action: { type: 'addLabelToCard' }}); 63 | t3.equal(eventType, getEventType.LabelAdded, 'event type should be LABEL_ADDED_TYPE'); 64 | t3.done(); 65 | }) 66 | 67 | t2.done(); 68 | }); 69 | 70 | t1.done(); 71 | }); 72 | -------------------------------------------------------------------------------- /test/actions/create-atc-card.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | const sinon = require('sinon'); 5 | require('sinon-as-promised'); 6 | 7 | process.env.TRELLO_API_KEY = 'trello-api-key'; 8 | process.env.TRELLO_API_TOK = 'trello-api-tok'; 9 | 10 | const trello = require('../../trello'); 11 | const sandbox = sinon.sandbox.create(); 12 | const trelloPost = sandbox.stub(trello, 'post'); 13 | 14 | const createATCCard = require('../../actions/create-atc-card'); 15 | 16 | // Disable logging to the console. 17 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 18 | 19 | const atcCardName = 'atc-card-name'; 20 | const intakeCardURL = 'https://intake-card.url'; 21 | 22 | tap.test('actions - create BPA order card', t1 => { 23 | t1.beforeEach(done => { 24 | sandbox.reset(); 25 | done(); 26 | }); 27 | 28 | t1.teardown(() => { 29 | sandbox.restore(); 30 | }); 31 | 32 | t1.test('without preflight list ID environment variable', t2 => { 33 | delete process.env.TRELLO_ATC_PREFLIGHT_LIST_ID; 34 | createATCCard(atcCardName, intakeCardURL) 35 | .then(() => { 36 | t2.fail('rejects'); 37 | }) 38 | .catch(e => { 39 | t2.pass('rejects'); 40 | t2.equal(trelloPost.callCount, 0, 'does not call Trello post'); 41 | t2.equal(e.message, 'ATC Preflight list ID not ready', 'returns the expected error'); 42 | }) 43 | .then(t2.done); 44 | }); 45 | 46 | t1.test('with IAA list ID environment variable set', t2 => { 47 | process.env.TRELLO_ATC_PREFLIGHT_LIST_ID = 'atc-preflight-list-id'; 48 | const expectedPost = { 49 | name: atcCardName, 50 | idList: process.env.TRELLO_ATC_PREFLIGHT_LIST_ID, 51 | desc: `\n\n---\n* [Intake](${intakeCardURL})` 52 | }; 53 | 54 | t2.test('trello post rejects', t3 => { 55 | const err = new Error('Test error'); 56 | trelloPost.rejects(err); 57 | 58 | createATCCard(atcCardName, intakeCardURL) 59 | .then(() => { 60 | t3.fail('rejects'); 61 | }) 62 | .catch(e => { 63 | t3.pass('rejects'); 64 | t3.equal(trelloPost.callCount, 1, 'trello post is called one time'); 65 | t3.equal(trelloPost.args[0][0], `/1/cards/`, 'posts to the right URL'); 66 | t3.same(trelloPost.args[0][1], expectedPost, 'posts the expected object'); 67 | t3.equal(e, err, 'returns the expected error'); 68 | }) 69 | .then(t3.done); 70 | }); 71 | 72 | t2.test('trello post resolves', t3 => { 73 | trelloPost.resolves(); 74 | 75 | createATCCard(atcCardName, intakeCardURL) 76 | .then(() => { 77 | t3.pass('resolves'); 78 | t3.equal(trelloPost.callCount, 1, 'trello post is called one time'); 79 | t3.equal(trelloPost.args[0][0], `/1/cards/`, 'posts to the right URL'); 80 | t3.same(trelloPost.args[0][1], expectedPost, 'posts the expected object'); 81 | }) 82 | .catch(e => { 83 | t3.fail('resolves'); 84 | }) 85 | .then(t3.done); 86 | }); 87 | 88 | t2.done(); 89 | }); 90 | 91 | t1.done(); 92 | }); 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # acq-trello-listener 2 | 3 | [![Build Status](https://travis-ci.org/18F/acq-trello-listener.svg?branch=develop)](https://travis-ci.org/18F/acq-trello-listener) [![Code Climate](https://codeclimate.com/github/18F/acq-trello-listener/badges/gpa.svg)](https://codeclimate.com/github/18F/acq-trello-listener) [![Test Coverage](https://codecov.io/gh/18F/acq-trello-listener/branch/develop/graph/badge.svg)](https://codecov.io/gh/18F/acq-trello-listener) [![Dependency Status](https://david-dm.org/18F/acq-trello-listener.svg)](https://david-dm.org/18F/acq-trello-listener) 4 | 5 | AcqStack Trello listener server. Listens to Trello webhook events on the AcqStack intake, BPA, and ATC boards. 6 | 7 | ## Running 8 | 9 | Clone it, run `npm install`. `npm start` kicks it off. Needs some environment variables: 10 | 11 | Name | Description 12 | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 13 | PORT | **REQUIRED** Port to listen for Trello webhook events on. 14 | TRELLO_API_KEY | **REQUIRED** Obtained from [Trello](https://trello.com/app-key). Located near the top of that page. 15 | TRELLO_API_TOK | **REQUIRED** Obtained from [Trello](https://trello.com/app-key). Located near the bottom of that page. 16 | TRELLO_CLIENT_SECRET | **REQUIRED** Obtained from [Trello](https://trello.com/app-key). There's a link to generate the key at the end of the first paragraph headed "Token." This is used to verify that webhook requests are actually from Trello (see the "Webhook Signatures" section on the [Trello webhook API documentation](https://developers.trello.com/apis/webhooks)). 17 | TRELLO_INTAKE_BOARD_ID | **REQUIRED** Board ID for the Intake board 18 | TRELLO_BPA_BOARD_ID | **REQUIRED** Board ID for the BPA Dashboard 19 | TRELLO_ATC_BOARD_ID | **REQUIRED** Board ID for the Air Traffic Control board 20 | LOG_LEVEL | Log level. See [simple-logger documentation](https://www.npmjs.com/package/@erdc-itl/simple-logger). 21 | HOST, TRELLO_WEBHOOK_HOST | Host where this server is running. One or the other is required. If `TRELLO_WEBHOOK_HOST` is set, it will be used. Otherwise, `HOST` will be used. 22 | 23 | ## Deploying 24 | 25 | For deployment on cloud.gov, expects a custom user-provided service called `acq-trello-cups` in the same org/space. This service is already up on `18F-acq/tools` with the necessary environment variables. 26 | 27 | ## Testing 28 | 29 | `npm test` 30 | 31 | ## Public domain 32 | 33 | This project is in the worldwide [public domain](LICENSE.md). As stated in [CONTRIBUTING](CONTRIBUTING.md): 34 | 35 | > This project is in the public domain within the United States, and copyright and related rights in the work worldwide are waived through the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 36 | 37 | > All contributions to this project will be released under the CC0 dedication. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest. 38 | -------------------------------------------------------------------------------- /test/trello.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.TRELLO_API_KEY = 'trello-api-key'; 4 | process.env.TRELLO_API_TOK = 'trello-api-tok'; 5 | 6 | const tap = require('tap'); 7 | const sinon = require('sinon'); 8 | 9 | const nodeTrello = require('node-trello'); 10 | 11 | const sandbox = sinon.sandbox.create(); 12 | 13 | const methods = [ 'get', 'put', 'post' ]; 14 | const stubs = { }; 15 | methods.forEach(method => { 16 | stubs[method] = sandbox.stub(nodeTrello.prototype, method); 17 | }); 18 | 19 | const trello = require('../trello'); 20 | 21 | tap.beforeEach(done => { 22 | sandbox.reset(); 23 | done(); 24 | }); 25 | tap.teardown(() => { 26 | sandbox.restore(); 27 | }) 28 | 29 | tap.test('trello promise wrapper', t1 => { 30 | methods.forEach(method => { 31 | t1.test(`${method} method`, t2 => { 32 | const trelloMethod = trello[method]; 33 | 34 | t2.test('without callback argument', withoutCallback => { 35 | withoutCallback.test('node-trello method returns an error', t3 => { 36 | const stub = stubs[method]; 37 | const err = new Error('Test error'); 38 | stub.yields(err, null); 39 | 40 | trelloMethod() 41 | .then(() => { 42 | t3.fail('rejects'); 43 | }) 44 | .catch(e => { 45 | t3.pass('rejects'); 46 | t3.equal(stub.callCount, 1, 'node-trello method is called once'); 47 | t3.equal(e, err, 'returns the expected error'); 48 | }) 49 | .then(t3.done); 50 | }); 51 | 52 | withoutCallback.test('node trello returns successfully', t3 => { 53 | const stub = stubs[method]; 54 | const obj = { returned: 'object' }; 55 | stub.yields(null, obj); 56 | 57 | trelloMethod() 58 | .then(o => { 59 | t3.pass('resolves'); 60 | t3.equal(stub.callCount, 1, 'node-trello method is called once'); 61 | t3.equal(o, obj, 'returns the expected object'); 62 | }) 63 | .catch(() => { 64 | t3.fail('resolves'); 65 | }) 66 | .then(t3.done); 67 | }); 68 | 69 | withoutCallback.done(); 70 | }); 71 | 72 | t2.test('with callback argument', withCallback => { 73 | withCallback.test('node-trello method returns an error', t3 => { 74 | const stub = stubs[method]; 75 | const err = new Error('Test error'); 76 | stub.yields(err, null); 77 | 78 | trelloMethod(function(e, data) { 79 | t3.pass('callback called'); 80 | t3.equal(e, err, 'returns the expected error'); 81 | t3.equal(data, null, 'returns no data'); 82 | t3.done(); 83 | }); 84 | }); 85 | 86 | withCallback.test('node trello returns successfully', t3 => { 87 | const stub = stubs[method]; 88 | const obj = { returned: 'object' }; 89 | stub.yields(null, obj); 90 | 91 | trelloMethod(function(e, data) { 92 | t3.pass('callback called'); 93 | t3.equal(e, null, 'returns no error'); 94 | t3.equal(data, obj, 'returns the expected data'); 95 | t3.done(); 96 | }); 97 | }); 98 | 99 | withCallback.done(); 100 | }); 101 | 102 | t2.done(); 103 | }); 104 | }); 105 | 106 | t1.done(); 107 | }); 108 | -------------------------------------------------------------------------------- /test/actions/create-bpa-order-card.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | const sinon = require('sinon'); 5 | const trello = require('node-trello'); 6 | 7 | process.env.TRELLO_API_KEY = 'trello-api-key'; 8 | process.env.TRELLO_API_TOK = 'trello-api-tok'; 9 | 10 | const sandbox = sinon.sandbox.create(); 11 | const trelloPost = sandbox.stub(trello.prototype, 'post'); 12 | 13 | const createBPAOrderCard = require('../../actions/create-bpa-order-card'); 14 | 15 | // Disable logging to the console. 16 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 17 | 18 | const cardName = 'card-name'; 19 | const agency = 'agency-name'; 20 | const subagency = 'subagency-name'; 21 | const boardURL = 'https://some.board.url/asdf'; 22 | 23 | tap.test('actions - create BPA order card', t1 => { 24 | t1.beforeEach(done => { 25 | sandbox.reset(); 26 | done(); 27 | }); 28 | 29 | t1.teardown(() => { 30 | sandbox.restore(); 31 | }); 32 | 33 | t1.test('without IAA list ID environment variable', t2 => { 34 | delete process.env.TRELLO_BPA_IAA_LIST_ID; 35 | trelloPost.yields(null, ''); 36 | createBPAOrderCard(cardName, agency, subagency, boardURL) 37 | .then(() => { 38 | t2.fail('rejects'); 39 | }) 40 | .catch(() => { 41 | t2.pass('rejects'); 42 | t2.equal(trelloPost.callCount, 0, 'does not call Trello post'); 43 | }) 44 | .then(t2.done); 45 | }); 46 | 47 | t1.test('with IAA list ID environment variable set', t2 => { 48 | process.env.TRELLO_BPA_IAA_LIST_ID = 'bpa-iaa-list-id'; 49 | 50 | const expectedCardObj = { 51 | name: cardName, 52 | desc: `* Project: \n* Agency: ${agency}\n* SubAgency: ${subagency}\n* Trello Board: ${boardURL}\n* Open date: ${`${new Date().getMonth() + 1}/${new Date().getDate()}/${`${new Date().getFullYear()}`.substr(2)}`}`, 53 | idList: process.env.TRELLO_BPA_IAA_LIST_ID 54 | }; 55 | 56 | t2.test('Trello post returns an error', t3 => { 57 | const err = new Error('Test error'); 58 | trelloPost.yields(err, null); 59 | 60 | createBPAOrderCard(cardName, agency, subagency, boardURL) 61 | .then(() => { 62 | t3.fail('rejects'); 63 | }) 64 | .catch(e => { 65 | t3.pass('rejects'); 66 | t3.equal(trelloPost.callCount, 1, 'calls trello post one time'); 67 | t3.equal(trelloPost.args[0][0], '/1/cards/', 'posts to the correct URL'); 68 | t3.same(trelloPost.args[0][1], expectedCardObj, 'passes in the correct card info'); 69 | t3.equal(e, err, 'returns the expected error'); 70 | }) 71 | .then(t3.done); 72 | }); 73 | 74 | t2.test('Trello post returns successfully', t3 => { 75 | const cardFromTrello = { 76 | name: 'from-trello' 77 | }; 78 | trelloPost.yields(null, cardFromTrello); 79 | 80 | createBPAOrderCard(cardName, agency, subagency, boardURL) 81 | .then(card => { 82 | t3.pass('resolves'); 83 | t3.equal(trelloPost.callCount, 1, 'calls trello post one time'); 84 | t3.equal(trelloPost.args[0][0], '/1/cards/', 'posts to the correct URL'); 85 | t3.same(trelloPost.args[0][1], expectedCardObj, 'passes in the correct card info'); 86 | t3.equal(card, cardFromTrello, 'resolves the card from Trello'); 87 | }) 88 | .catch(() => { 89 | t3.fail('resolves'); 90 | }) 91 | .then(t3.done); 92 | }); 93 | 94 | t2.done(); 95 | }) 96 | t1.done(); 97 | }); 98 | -------------------------------------------------------------------------------- /test/webhookHandlers/move-to-iaa-go.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | const sinon = require('sinon'); 5 | const mockRequire = require('mock-require'); 6 | require('sinon-as-promised'); 7 | 8 | process.env.TRELLO_API_KEY = 'trello-api-key'; 9 | process.env.TRELLO_API_TOK = 'trello-api-tok'; 10 | 11 | const sandbox = sinon.sandbox.create(); 12 | const addIntakeChecklist = sandbox.stub(); 13 | const createATCCard = sandbox.stub(); 14 | const createBPAComponents = sandbox.stub(); 15 | 16 | mockRequire('../../webhookHandlers/intake/move-to-iaa-go/create-checklists-on-intake-card', addIntakeChecklist); 17 | mockRequire('../../webhookHandlers/intake/move-to-iaa-go/create-atc-card', createATCCard); 18 | mockRequire('../../webhookHandlers/intake/move-to-iaa-go/create-bpa-components', createBPAComponents); 19 | 20 | const moveToiaaGo = require('../../webhookHandlers/intake/move-to-iaa-go'); 21 | 22 | // Disable logging to the console. 23 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 24 | 25 | tap.beforeEach(done => { 26 | sandbox.reset(); 27 | done(); 28 | }); 29 | tap.teardown(() => { 30 | sandbox.restore(); 31 | mockRequire.stopAll(); 32 | }); 33 | 34 | tap.test('webhook handlers - intake: move to IAA Go', t1 => { 35 | t1.test('event is not a card move', t2 => { 36 | const trelloEvent = { 37 | action: { 38 | type: 'Something' 39 | } 40 | }; 41 | moveToiaaGo(trelloEvent) 42 | .then(() => { 43 | t2.fail('rejects'); 44 | }) 45 | .catch(e => { 46 | t2.pass('rejects'); 47 | t2.equal(e.message, 'Not a move to IAA Go', 'returns the expected error'); 48 | }) 49 | .then(t2.done); 50 | }); 51 | 52 | t1.test('event is a card move', t2 => { 53 | t2.test('move is not into IAA Go list', t3 => { 54 | const trelloEvent = { 55 | action: { 56 | type: 'updateCard', 57 | data: { 58 | card: { 59 | id: 'card-id', 60 | name: 'test card' 61 | }, 62 | listBefore: { 63 | name: 'list-before' 64 | }, 65 | listAfter: { 66 | name: 'list-after' 67 | } 68 | } 69 | } 70 | }; 71 | moveToiaaGo(trelloEvent) 72 | .then(() => { 73 | t3.fail('rejects'); 74 | }) 75 | .catch(e => { 76 | t3.pass('rejects'); 77 | t3.equal(e.message, 'Not a move to IAA Go', 'returns the expected error'); 78 | }) 79 | .then(t3.done); 80 | }); 81 | 82 | t2.test('move is into IAA Go list', t3 => { 83 | const trelloEvent = { 84 | action: { 85 | type: 'updateCard', 86 | data: { 87 | card: { 88 | id: 'card-id', 89 | name: `card-name` 90 | }, 91 | listBefore: { 92 | name: 'list-before' 93 | }, 94 | listAfter: { 95 | name: 'IAA Go' 96 | } 97 | } 98 | } 99 | }; 100 | 101 | t3.test('addIntakeChecklist rejects', t4 => { 102 | const err = new Error('Test error'); 103 | addIntakeChecklist.rejects(err); 104 | 105 | moveToiaaGo(trelloEvent) 106 | .then(() => { 107 | t4.fail('rejects'); 108 | }) 109 | .catch(e => { 110 | t4.pass('rejects'); 111 | t4.equal(e, err, 'returns the expected error'); 112 | }) 113 | .then(t4.done); 114 | }); 115 | 116 | t3.test('addIntakeChecklist resolves', t4 => { 117 | t4.test('createATCCard rejects', t5 => { 118 | const err = new Error('Test error'); 119 | addIntakeChecklist.resolves(); 120 | createATCCard.rejects(err); 121 | 122 | moveToiaaGo(trelloEvent) 123 | .then(() => { 124 | t5.fail('rejects'); 125 | }) 126 | .catch(e => { 127 | t5.pass('rejects'); 128 | t5.equal(e, err, 'returns the expected error'); 129 | }) 130 | .then(t5.done); 131 | }); 132 | 133 | t4.test('createATCCard resolves', t5 => { 134 | t5.test('createBPAComponents rejects', t6 => { 135 | const err = new Error('Test error'); 136 | addIntakeChecklist.resolves(); 137 | createATCCard.resolves(); 138 | createBPAComponents.rejects(err); 139 | 140 | moveToiaaGo(trelloEvent) 141 | .then(() => { 142 | t6.fail('rejects'); 143 | }) 144 | .catch(e => { 145 | t6.pass('rejects'); 146 | t6.equal(e, err, 'returns the expected error'); 147 | }) 148 | .then(t6.done); 149 | }); 150 | 151 | t5.test('createBPAComponents resolves', t6 => { 152 | addIntakeChecklist.resolves(); 153 | createATCCard.resolves(); 154 | createBPAComponents.resolves(); 155 | 156 | moveToiaaGo(trelloEvent) 157 | .then(() => { 158 | t6.pass('resolves'); 159 | }) 160 | .catch(() => { 161 | t6.fail('resolves'); 162 | }) 163 | .then(t6.done); 164 | }); 165 | 166 | t5.done(); 167 | }); 168 | 169 | t4.done(); 170 | }); 171 | 172 | t3.done(); 173 | }); 174 | 175 | t2.done(); 176 | }); 177 | 178 | t1.done(); 179 | }); 180 | -------------------------------------------------------------------------------- /test/webhookHandlers/move-to-iaa-go/create-atc-card.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | const sinon = require('sinon'); 3 | require('sinon-as-promised'); 4 | 5 | process.env.TRELLO_API_KEY = 'trello-api-key'; 6 | process.env.TRELLO_API_TOK = 'trello-api-tok'; 7 | 8 | const trello = require('../../../trello'); 9 | const actions = require('../../../actions'); 10 | const createATCCard = require('../../../webhookHandlers/intake/move-to-iaa-go/create-atc-card'); 11 | 12 | const sandbox = sinon.sandbox.create(); 13 | const trelloGet = sandbox.stub(trello, 'get'); 14 | const trelloPut = sandbox.stub(trello, 'put'); 15 | const createATCCardAction = sandbox.stub(actions, 'createATCCard'); 16 | 17 | // Disable logging to the console. 18 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 19 | 20 | tap.test('webhook handlers - intake: move to IAA Go > create ATC components', t1 => { 21 | t1.beforeEach(done => { 22 | sandbox.reset(); 23 | done(); 24 | }); 25 | t1.teardown(() => { 26 | sandbox.restore(); 27 | }); 28 | 29 | const trelloEvent = { 30 | action: { data: { card: { id: 'intake-card-id' }}} 31 | }; 32 | 33 | t1.test('trello get returns an error', t2 => { 34 | const err = new Error('Test error'); 35 | trelloGet.rejects(err); 36 | 37 | createATCCard(trelloEvent) 38 | .then(() => { 39 | t2.fail('rejects'); 40 | }) 41 | .catch(e => { 42 | t2.pass('rejects'); 43 | t2.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 44 | t2.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 45 | t2.equal(e, err, 'returns the expected error'); 46 | }) 47 | .then(t2.done); 48 | }); 49 | 50 | t1.test('trello returns a card that already has an ATC link', t2 => { 51 | const intakeCard = { 52 | desc: '---\n\n* [Air Traffic Control](https://atc)' 53 | }; 54 | trelloGet.resolves(intakeCard); 55 | 56 | createATCCard(trelloEvent) 57 | .then(() => { 58 | t2.fail('rejects') 59 | }) 60 | .catch(e => { 61 | t2.pass('rejects'); 62 | t2.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 63 | t2.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 64 | t2.equal(e.message, 'Intake card already has a link to ATC card', 'returns the expected error'); 65 | }) 66 | .then(t2.done); 67 | }); 68 | 69 | t1.test('trello returns a card that does not already have an ATC link', t2 => { 70 | const intakeCard = { 71 | id: 'intake-card-id', 72 | desc: '', 73 | url: 'https://intake-card.url' 74 | }; 75 | 76 | t2.test('createATCCard rejects', t3 => { 77 | const err = new Error('Test error'); 78 | trelloGet.resolves(intakeCard); 79 | createATCCardAction.rejects(err); 80 | 81 | createATCCard(trelloEvent) 82 | .then(() => { 83 | t3.fail('rejects'); 84 | }) 85 | .catch(e => { 86 | t3.pass('rejects'); 87 | t3.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 88 | t3.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 89 | t3.equal(createATCCardAction.callCount, 1, 'calls createATCCard action one time'); 90 | t3.equal(createATCCardAction.args[0][0], intakeCard.name, 'uses the intake card name'); 91 | t3.equal(createATCCardAction.args[0][0], intakeCard.name, 'uses the intake card URL'); 92 | t3.equal(e, err, 'returns the expected error'); 93 | }) 94 | .then(t3.done); 95 | }); 96 | 97 | t2.test('createATCCard resolves', t3 => { 98 | const atcCard = { 99 | url: 'https://atc-card.url' 100 | }; 101 | 102 | t3.test('trello put returns an error', t4 => { 103 | const err = new Error('Test error'); 104 | trelloGet.resolves(intakeCard); 105 | createATCCardAction.resolves(atcCard); 106 | trelloPut.rejects(err); 107 | 108 | createATCCard(trelloEvent) 109 | .then(() => { 110 | t4.fail('rejects'); 111 | }) 112 | .catch(e => { 113 | t4.pass('rejects'); 114 | t4.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 115 | t4.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 116 | t4.equal(createATCCardAction.callCount, 1, 'calls createATCCard action one time'); 117 | t4.equal(createATCCardAction.args[0][0], intakeCard.name, 'uses the intake card name'); 118 | t4.equal(createATCCardAction.args[0][0], intakeCard.name, 'uses the intake card URL'); 119 | t4.equal(trelloPut.callCount, 1, 'calls Trello put one time'); 120 | t4.equal(trelloPut.args[0][0], `/1/cards/${intakeCard.id}/desc`, 'updates the correct card'); 121 | t4.same(trelloPut.args[0][1], { value: `---\n\n* [Air Traffic Control](${atcCard.url})`}, 'updates with the correct ATC card link'); 122 | t4.equal(e, err, 'returns the expected error'); 123 | }) 124 | .then(t4.done); 125 | }); 126 | 127 | t3.test('trello put returns okay', t4 => { 128 | t4.test('ATC card does not have a description', t5 => { 129 | trelloGet.resolves(intakeCard); 130 | createATCCardAction.resolves(atcCard); 131 | trelloPut.resolves(); 132 | 133 | createATCCard(trelloEvent) 134 | .then(() => { 135 | t5.pass('resolves'); 136 | t5.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 137 | t5.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 138 | t5.equal(createATCCardAction.callCount, 1, 'calls createATCCard action one time'); 139 | t5.equal(createATCCardAction.args[0][0], intakeCard.name, 'uses the intake card name'); 140 | t5.equal(createATCCardAction.args[0][0], intakeCard.name, 'uses the intake card URL'); 141 | t5.equal(trelloPut.callCount, 1, 'calls Trello put one time'); 142 | t5.equal(trelloPut.args[0][0], `/1/cards/${intakeCard.id}/desc`, 'updates the correct card'); 143 | t5.same(trelloPut.args[0][1], { value: `---\n\n* [Air Traffic Control](${atcCard.url})`}, 'updates with the correct ATC card link'); 144 | }) 145 | .catch(e => { 146 | t5.fail('resolves'); 147 | }) 148 | .then(t5.done); 149 | }); 150 | 151 | t4.test('ATC card does already have a description', t5 => { 152 | const intakeCardWithDesc = JSON.parse(JSON.stringify(intakeCard)); 153 | intakeCardWithDesc.desc = 'Existing description'; 154 | trelloGet.resolves(intakeCardWithDesc); 155 | createATCCardAction.resolves(atcCard); 156 | trelloPut.resolves(); 157 | 158 | createATCCard(trelloEvent) 159 | .then(() => { 160 | t5.pass('resolves'); 161 | t5.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 162 | t5.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 163 | t5.equal(createATCCardAction.callCount, 1, 'calls createATCCard action one time'); 164 | t5.equal(createATCCardAction.args[0][0], intakeCard.name, 'uses the intake card name'); 165 | t5.equal(createATCCardAction.args[0][0], intakeCard.name, 'uses the intake card URL'); 166 | t5.equal(trelloPut.callCount, 1, 'calls Trello put one time'); 167 | t5.equal(trelloPut.args[0][0], `/1/cards/${intakeCard.id}/desc`, 'updates the correct card'); 168 | t5.same(trelloPut.args[0][1], { value: `${intakeCardWithDesc.desc}\n\n---\n\n* [Air Traffic Control](${atcCard.url})`}, 'updates with the correct ATC card link'); 169 | }) 170 | .catch(e => { 171 | t5.fail('resolves'); 172 | }) 173 | .then(t5.done); 174 | }); 175 | 176 | t4.done(); 177 | }); 178 | 179 | t3.done(); 180 | }); 181 | 182 | t2.done(); 183 | }); 184 | 185 | t1.done(); 186 | }); 187 | -------------------------------------------------------------------------------- /test/actions/add-intake-checklist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | const sinon = require('sinon'); 5 | require('sinon-as-promised'); 6 | 7 | process.env.TRELLO_API_KEY = 'trello-api-key'; 8 | process.env.TRELLO_API_TOK = 'trello-api-tok'; 9 | 10 | const trello = require('../../trello'); 11 | const sandbox = sinon.sandbox.create(); 12 | const trelloGet = sandbox.stub(trello, 'get'); 13 | const trelloPost = sandbox.stub(trello, 'post'); 14 | 15 | const addIntakeChecklist = require('../../actions/add-intake-checklist'); 16 | const intakeCardID = 'intake-card-id'; 17 | 18 | // Disable logging to the console. 19 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 20 | 21 | tap.beforeEach(done => { 22 | sandbox.reset(); 23 | done(); 24 | }); 25 | 26 | tap.teardown(() => { 27 | sandbox.restore(); 28 | }); 29 | 30 | tap.test('actions - add intake checklist', trelloGetTest => { 31 | trelloGetTest.test('trello get throws error (fetching existing checklists)', test => { 32 | const err = new Error('Test error'); 33 | trelloGet.rejects(err); 34 | 35 | addIntakeChecklist(intakeCardID) 36 | .then(() => { 37 | test.fail('rejects'); 38 | }) 39 | .catch(e => { 40 | test.pass('rejects'); 41 | test.equals(trelloGet.callCount, 1, 'trello get called one time'); 42 | test.equals(trelloGet.args[0][0], `/1/cards/${intakeCardID}/checklists`, 'calls the expected URL'); 43 | test.equals(trelloPost.callCount, 0, 'trello post called 0 times'); 44 | test.equals(e, err, 'returns the expected error'); 45 | }) 46 | .then(test.done); 47 | }); 48 | 49 | trelloGetTest.test('trello get returns a list of checklists include intake forms', test => { 50 | trelloGet.resolves([{ name: 'Intake Forms' }]); 51 | 52 | addIntakeChecklist(intakeCardID) 53 | .then(() => { 54 | test.fail('rejects'); 55 | }) 56 | .catch(e => { 57 | test.pass('rejects'); 58 | test.equals(trelloGet.callCount, 1, 'trello get called one time'); 59 | test.equals(trelloGet.args[0][0], `/1/cards/${intakeCardID}/checklists`, 'calls the expected URL'); 60 | test.equals(trelloPost.callCount, 0, 'trello post called 0 times'); 61 | test.equals(e.message, 'Intake card already has an intake forms checklist', 'returns the expected error'); 62 | }) 63 | .then(test.done); 64 | }); 65 | 66 | const getPassValues = [ 67 | { name: 'empty list', value: [ ] }, 68 | { name: 'list without intake checklist', value: [{ name: 'Not intake' }] } 69 | ]; 70 | 71 | getPassValues.forEach(getPass => { 72 | trelloGetTest.test(`trello get returns ${getPass.name}`, trelloPostChecklistTest => { 73 | const expectedChecklistReq = { 74 | idCard: intakeCardID, 75 | name: 'Intake Forms' 76 | }; 77 | 78 | trelloPostChecklistTest.test('trello post (create checklist) rejects', test => { 79 | const err = new Error('Test error'); 80 | trelloGet.resolves(getPass.value); 81 | trelloPost.onCall(0).rejects(err); 82 | 83 | addIntakeChecklist(intakeCardID) 84 | .then(() => { 85 | test.fail('rejects'); 86 | }) 87 | .catch(e => { 88 | test.pass('rejects'); 89 | test.equals(trelloGet.callCount, 1, 'trello get called one time'); 90 | test.equals(trelloGet.args[0][0], `/1/cards/${intakeCardID}/checklists`, 'calls the expected URL'); 91 | test.equals(trelloPost.callCount, 1, 'trello post called one time'); 92 | test.equals(trelloPost.args[0][0], `/1/checklists`, 'calls the expected URL'); 93 | test.same(trelloPost.args[0][1], expectedChecklistReq, 'sends the expected arguments'); 94 | test.equals(e, err, 'returns the expected error'); 95 | }) 96 | .then(test.done); 97 | }); 98 | 99 | trelloPostChecklistTest.test('trello post (create checklist) resolves', trelloPostFirstCheckItemTest => { 100 | const checklistID = 'checklist-id'; 101 | 102 | trelloPostFirstCheckItemTest.test('trello post (add first item) rejects', test => { 103 | const err = new Error('Test error'); 104 | trelloGet.resolves(getPass.value); 105 | trelloPost.onCall(0).resolves({ id: checklistID }); 106 | trelloPost.onCall(1).rejects(err); 107 | 108 | addIntakeChecklist(intakeCardID) 109 | .then(() => { 110 | test.fail('rejects'); 111 | }) 112 | .catch(e => { 113 | test.pass('rejects'); 114 | test.equals(trelloGet.callCount, 1, 'trello get called one time'); 115 | test.equals(trelloGet.args[0][0], `/1/cards/${intakeCardID}/checklists`, 'calls the expected URL'); 116 | test.equals(trelloPost.callCount, 2, 'trello post called two times'); 117 | test.equals(trelloPost.args[0][0], `/1/checklists`, 'calls the expected URL'); 118 | test.same(trelloPost.args[0][1], expectedChecklistReq, 'sends the expected arguments'); 119 | test.equals(trelloPost.args[1][0], `/1/checklists/${checklistID}/checkItems`, 'calls the expected URL'); 120 | test.same(trelloPost.args[1][1], { name: '7600 SOW' }, 'sends the expected arguments'); 121 | test.equals(e, err, 'returns the expected error'); 122 | }) 123 | .then(test.done); 124 | }); 125 | 126 | trelloPostFirstCheckItemTest.test('trello post (add first item) resolves', trelloPostSecondCheckItemTest => { 127 | trelloPostSecondCheckItemTest.test('trello post (add second item) rejects', test => { 128 | const err = new Error('Test error'); 129 | trelloGet.resolves(getPass.value); 130 | trelloPost.onCall(0).resolves({ id: checklistID }); 131 | trelloPost.onCall(1).resolves(); 132 | trelloPost.onCall(2).rejects(err); 133 | 134 | addIntakeChecklist(intakeCardID) 135 | .then(() => { 136 | test.fail('rejects'); 137 | }) 138 | .catch(e => { 139 | test.pass('rejects'); 140 | test.equals(trelloGet.callCount, 1, 'trello get called one time'); 141 | test.equals(trelloGet.args[0][0], `/1/cards/${intakeCardID}/checklists`, 'calls the expected URL'); 142 | test.equals(trelloPost.callCount, 3, 'trello post called three times'); 143 | test.equals(trelloPost.args[0][0], `/1/checklists`, 'calls the expected URL'); 144 | test.same(trelloPost.args[0][1], expectedChecklistReq, 'sends the expected arguments'); 145 | test.equals(trelloPost.args[1][0], `/1/checklists/${checklistID}/checkItems`, 'calls the expected URL'); 146 | test.same(trelloPost.args[1][1], { name: '7600 SOW' }, 'sends the expected arguments'); 147 | test.equals(trelloPost.args[2][0], `/1/checklists/${checklistID}/checkItems`, 'calls the expected URL'); 148 | test.same(trelloPost.args[2][1], { name: 'Budget Estimate' }, 'sends the expected arguments'); 149 | test.equals(e, err, 'returns the expected error'); 150 | }) 151 | .then(test.done); 152 | }); 153 | 154 | trelloPostSecondCheckItemTest.test('trello post (add second item) resolves', test => { 155 | const err = new Error('Test error'); 156 | trelloGet.resolves(getPass.value); 157 | trelloPost.onCall(0).resolves({ id: checklistID }); 158 | trelloPost.onCall(1).resolves(); 159 | trelloPost.onCall(2).resolves(); 160 | 161 | addIntakeChecklist(intakeCardID) 162 | .then(() => { 163 | test.pass('resolves'); 164 | test.equals(trelloGet.callCount, 1, 'trello get called one time'); 165 | test.equals(trelloGet.args[0][0], `/1/cards/${intakeCardID}/checklists`, 'calls the expected URL'); 166 | test.equals(trelloPost.callCount, 3, 'trello post called three times'); 167 | test.equals(trelloPost.args[0][0], `/1/checklists`, 'calls the expected URL'); 168 | test.same(trelloPost.args[0][1], expectedChecklistReq, 'sends the expected arguments'); 169 | test.equals(trelloPost.args[1][0], `/1/checklists/${checklistID}/checkItems`, 'calls the expected URL'); 170 | test.same(trelloPost.args[1][1], { name: '7600 SOW' }, 'sends the expected arguments'); 171 | test.equals(trelloPost.args[2][0], `/1/checklists/${checklistID}/checkItems`, 'calls the expected URL'); 172 | test.same(trelloPost.args[2][1], { name: 'Budget Estimate' }, 'sends the expected arguments'); 173 | }) 174 | .catch(e => { 175 | test.fail('resolves'); 176 | }) 177 | .then(test.done); 178 | }); 179 | 180 | trelloPostSecondCheckItemTest.done(); 181 | }); 182 | 183 | trelloPostFirstCheckItemTest.done(); 184 | }); 185 | 186 | trelloPostChecklistTest.done(); 187 | }); 188 | }); 189 | 190 | trelloGetTest.done(); 191 | }); 192 | -------------------------------------------------------------------------------- /test/webhookHandlers/move-to-iaa-completed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const tap = require('tap'); 4 | const sinon = require('sinon'); 5 | require('sinon-as-promised'); 6 | 7 | process.env.TRELLO_API_KEY = 'trello-api-key'; 8 | process.env.TRELLO_API_TOK = 'trello-api-tok'; 9 | const trello = require('../../trello'); 10 | 11 | const sandbox = sinon.sandbox.create(); 12 | const trelloGet = sandbox.stub(trello, 'get'); 13 | const trelloPut = sandbox.stub(trello, 'put'); 14 | 15 | const moveToiaaCompleted = require('../../webhookHandlers/intake/move-to-iaa-completed'); 16 | 17 | // Disable logging to the console. 18 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 19 | 20 | tap.beforeEach(done => { 21 | sandbox.reset(); 22 | done(); 23 | }); 24 | tap.teardown(() => { 25 | sandbox.restore(); 26 | }); 27 | 28 | tap.test('webhook handlers - intake: move to IAA Completed', listIDTest => { 29 | const err = new Error('Test error'); 30 | 31 | listIDTest.test('Workshop prep list ID is not ready', test => { 32 | delete process.env.TRELLO_BPA_WORKSHOP_PREP_LIST_ID; 33 | moveToiaaCompleted() 34 | .then(() => { 35 | test.fail('rejects'); 36 | }) 37 | .catch(e => { 38 | test.pass('rejects'); 39 | test.equal(trelloGet.callCount, 0, 'does not call trello get'); 40 | test.equal(trelloPut.callCount, 0, 'does not call trello put'); 41 | test.equal(e.message, 'BPA Workshop Prep list ID not ready', 'returns the expected error'); 42 | }) 43 | .then(test.done); 44 | }); 45 | 46 | listIDTest.test('Workshop prep list ID is ready', eventTest => { 47 | process.env.TRELLO_BPA_WORKSHOP_PREP_LIST_ID = 'bpa-workshop-prep-list-id'; 48 | 49 | eventTest.test('event is not a card move', test => { 50 | const trelloEvent = { 51 | action: { 52 | type: 'Something' 53 | } 54 | }; 55 | moveToiaaCompleted(trelloEvent) 56 | .then(() => { 57 | test.fail('rejects'); 58 | }) 59 | .catch(e => { 60 | test.pass('rejects'); 61 | test.equal(trelloGet.callCount, 0, 'does not call trello get'); 62 | test.equal(trelloPut.callCount, 0, 'does not call trello put'); 63 | test.equal(e.message, 'Not a card move', 'returns the expected error'); 64 | }) 65 | .then(test.done); 66 | }); 67 | 68 | eventTest.test('event is a card move, but not into IAA Completed', test => { 69 | const trelloEvent = { 70 | action: { 71 | type: 'updateCard', 72 | data: { 73 | card: { 74 | id: 'card-id', 75 | name: 'test card' 76 | }, 77 | listBefore: { 78 | name: 'list-before' 79 | }, 80 | listAfter: { 81 | name: 'list-after' 82 | } 83 | } 84 | } 85 | }; 86 | moveToiaaCompleted(trelloEvent) 87 | .then(() => { 88 | test.fail('rejects'); 89 | }) 90 | .catch(e => { 91 | test.pass('rejects'); 92 | test.equal(trelloGet.callCount, 0, 'does not call trello get'); 93 | test.equal(trelloPut.callCount, 0, 'does not call trello put'); 94 | test.equal(e.message, 'Not a move into IAA Completed Work Begin', 'returns the expected error'); 95 | }) 96 | .then(test.done); 97 | }); 98 | 99 | eventTest.test('event is a card move into IAA Completed, but not a BPA card', test => { 100 | const trelloEvent = { 101 | action: { 102 | type: 'updateCard', 103 | data: { 104 | card: { 105 | id: 'card-id', 106 | name: 'test card' 107 | }, 108 | listBefore: { 109 | name: 'list-before' 110 | }, 111 | listAfter: { 112 | name: 'IAA Completed Work Begin' 113 | } 114 | } 115 | } 116 | }; 117 | moveToiaaCompleted(trelloEvent) 118 | .then(() => { 119 | test.fail('rejects'); 120 | }) 121 | .catch(e => { 122 | test.pass('rejects'); 123 | test.equal(trelloGet.callCount, 0, 'does not call trello get'); 124 | test.equal(trelloPut.callCount, 0, 'does not call trello put'); 125 | test.equal(e.message, 'Not an Agile BPA card', 'returns the expected error'); 126 | }) 127 | .then(test.done); 128 | }); 129 | 130 | eventTest.test('event is a BPA card move into IAA Completed', trelloGetTest => { 131 | const trelloEvent = { 132 | action: { 133 | type: 'updateCard', 134 | data: { 135 | card: { 136 | id: 'card-id', 137 | name: `Agile BPA Test Card` 138 | }, 139 | listBefore: { 140 | name: 'list-before' 141 | }, 142 | listAfter: { 143 | name: 'IAA Completed Work Begin' 144 | } 145 | } 146 | } 147 | }; 148 | 149 | trelloGetTest.test('trello get rejects', test => { 150 | trelloGet.rejects(err); 151 | 152 | moveToiaaCompleted(trelloEvent) 153 | .then(() => { 154 | test.fail('rejects'); 155 | }) 156 | .catch(e => { 157 | test.pass('rejects'); 158 | test.equal(trelloGet.callCount, 1, 'calls trello get one time'); 159 | test.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'calls the expected URL'); 160 | test.equal(trelloPut.callCount, 0, 'does not call trello put'); 161 | test.equal(e, err, 'returns the expected error'); 162 | }) 163 | .then(test.done); 164 | }); 165 | 166 | trelloGetTest.test('trello get resolves a card with no BPA links', test => { 167 | trelloGet.resolves({ name: trelloEvent.action.data.card.name, desc: 'Some description' }); 168 | 169 | moveToiaaCompleted(trelloEvent) 170 | .then(() => { 171 | test.fail('rejects'); 172 | }) 173 | .catch(e => { 174 | test.pass('rejects'); 175 | test.equal(trelloGet.callCount, 1, 'calls trello get one time'); 176 | test.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'calls the expected URL'); 177 | test.equal(trelloPut.callCount, 0, 'does not call trello put'); 178 | test.equal(e.message, 'Intake BPA does not have an associated BPA Dashboard card', 'returns the expected error'); 179 | }) 180 | .then(test.done); 181 | }); 182 | 183 | trelloGetTest.test('trello get resolves a card with BPA links', trelloPutTest => { 184 | const bpaCardID = 'bpa-card-id'; 185 | const resolvedCard = { 186 | name: trelloEvent.action.data.card.name, 187 | desc: '### Agile BPA Links\n\n* [BPA Dashboard](https://trello.com/c/' + bpaCardID + '/00-card-name)' 188 | }; 189 | 190 | trelloPutTest.test('trello put rejects', test => { 191 | trelloGet.resolves(resolvedCard); 192 | trelloPut.rejects(err); 193 | 194 | moveToiaaCompleted(trelloEvent) 195 | .then(() => { 196 | test.fail('rejects'); 197 | }) 198 | .catch(e => { 199 | test.pass('rejects'); 200 | test.equal(trelloGet.callCount, 1, 'calls trello get one time'); 201 | test.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'calls the expected URL'); 202 | test.equal(trelloPut.callCount, 1, 'calls trello put one time'); 203 | test.equal(trelloPut.args[0][0], `/1/cards/${bpaCardID}/idList`, 'calls the expected URL'); 204 | test.same(trelloPut.args[0][1], { value: process.env.TRELLO_BPA_WORKSHOP_PREP_LIST_ID }, 'sends the expected list ID'); 205 | test.equal(e, err, 'returns the expected error'); 206 | }) 207 | .then(test.done); 208 | }); 209 | 210 | trelloPutTest.test('trello put resolves', test => { 211 | trelloGet.resolves(resolvedCard); 212 | trelloPut.resolves(); 213 | 214 | moveToiaaCompleted(trelloEvent) 215 | .then(() => { 216 | test.pass('resolves'); 217 | test.equal(trelloGet.callCount, 1, 'calls trello get one time'); 218 | test.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'calls the expected URL'); 219 | test.equal(trelloPut.callCount, 1, 'calls trello put one time'); 220 | test.equal(trelloPut.args[0][0], `/1/cards/${bpaCardID}/idList`, 'calls the expected URL'); 221 | test.same(trelloPut.args[0][1], { value: process.env.TRELLO_BPA_WORKSHOP_PREP_LIST_ID }, 'sends the expected list ID'); 222 | }) 223 | .catch(e => { 224 | test.fail('resolves'); 225 | }) 226 | .then(test.done); 227 | }); 228 | 229 | trelloPutTest.done(); 230 | }); 231 | 232 | trelloGetTest.done(); 233 | }); 234 | 235 | eventTest.done(); 236 | }); 237 | 238 | listIDTest.done(); 239 | }); 240 | -------------------------------------------------------------------------------- /test/webhookHandlers/move-to-iaa-go/create-bpa-components.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap'); 2 | const sinon = require('sinon'); 3 | require('sinon-as-promised'); 4 | 5 | process.env.TRELLO_API_KEY = 'trello-api-key'; 6 | process.env.TRELLO_API_TOK = 'trello-api-tok'; 7 | 8 | const trello = require('../../../trello'); 9 | const actions = require('../../../actions'); 10 | const createBPAComponents = require('../../../webhookHandlers/intake/move-to-iaa-go/create-bpa-components'); 11 | 12 | const sandbox = sinon.sandbox.create(); 13 | const trelloGet = sandbox.stub(trello, 'get'); 14 | const trelloPut = sandbox.stub(trello, 'put'); 15 | const createBPAOrderBoard = sandbox.stub(actions, 'createBPAOrderBoard'); 16 | const createBPAOrderCard = sandbox.stub(actions, 'createBPAOrderCard'); 17 | 18 | // Disable logging to the console. 19 | require('@erdc-itl/simple-logger').setOptions({ console: false }); 20 | 21 | tap.test('webhook handlers - intake: move to IAA Go > create BPA components', t1 => { 22 | t1.test('card name does not start with Agile BPA', t2 => { 23 | const trelloEvent = { 24 | action: { 25 | type: 'updateCard', 26 | data: { 27 | card: { 28 | id: 'card-id', 29 | name: 'test card' 30 | }, 31 | listBefore: { 32 | name: 'list-before' 33 | }, 34 | listAfter: { 35 | name: 'IAA Go' 36 | } 37 | } 38 | } 39 | }; 40 | createBPAComponents(trelloEvent) 41 | .then(() => { 42 | t2.fail('rejects'); 43 | }) 44 | .catch((e) => { 45 | t2.pass('rejects'); 46 | }) 47 | .then(t2.done); 48 | }); 49 | 50 | t1.test('card name does start with Agile BPA', t2 => { 51 | const project = 'Project Name'; 52 | const agency = 'Agency Name'; 53 | const trelloEvent = { 54 | action: { 55 | type: 'updateCard', 56 | data: { 57 | card: { 58 | id: 'card-id', 59 | name: `Agile BPA / ${agency} / ${project}` 60 | }, 61 | listBefore: { 62 | name: 'list-before' 63 | }, 64 | listAfter: { 65 | name: 'IAA Go' 66 | } 67 | } 68 | } 69 | }; 70 | 71 | t2.beforeEach(done => { 72 | sandbox.reset(); 73 | done(); 74 | }); 75 | t2.teardown(() => { 76 | sandbox.restore(); 77 | }); 78 | 79 | t2.test('trello get returns an error', t3 => { 80 | const err = new Error('Test error'); 81 | trelloGet.rejects(err); 82 | createBPAComponents(trelloEvent) 83 | .then(() => { 84 | t3.fail('rejects'); 85 | }) 86 | .catch(e => { 87 | t3.pass('rejects'); 88 | t3.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 89 | t3.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 90 | t3.equal(e, err, 'returns the expected error'); 91 | }) 92 | .then(t3.done); 93 | }); 94 | 95 | t2.test('trello returns a card that already has Agile BPA links', t3 => { 96 | trelloGet.resolves({ desc: '---\n\n### Agile BPA Links\n\n' }); 97 | createBPAComponents(trelloEvent) 98 | .then(() => { 99 | t3.fail('rejects'); 100 | }) 101 | .catch(() => { 102 | t3.pass('rejects'); 103 | t3.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 104 | t3.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 105 | }) 106 | .then(t3.done); 107 | }); 108 | 109 | t2.test('trello returns a card without Agile BPA links', t3 => { 110 | const cardFromTrello = { desc: '', id: 'card-id', name: trelloEvent.action.data.card.name }; 111 | 112 | t3.test('creating BPA order board rejects', t4 => { 113 | trelloGet.resolves(cardFromTrello); 114 | const err = new Error('Test error'); 115 | createBPAOrderBoard.rejects(err); 116 | 117 | createBPAComponents(trelloEvent) 118 | .then(() => { 119 | t4.fail('rejects'); 120 | }) 121 | .catch(e => { 122 | t4.pass('rejects'); 123 | t4.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 124 | t4.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 125 | t4.equal(createBPAOrderBoard.callCount, 1, 'calls create BPA order board one time'); 126 | t4.equal(createBPAOrderBoard.args[0][0], cardFromTrello.name, 'board name argument is card name'); 127 | t4.equal(e, err, 'returns the expected error'); 128 | }) 129 | .then(t4.done); 130 | }); 131 | 132 | t3.test('creating BPA order board resolves', t4 => { 133 | const boardObj = { url: 'https://some.board.url/asdf' }; 134 | 135 | t4.test('creating BPA order card rejects', t5 => { 136 | trelloGet.resolves(cardFromTrello); 137 | createBPAOrderBoard.resolves(boardObj); 138 | const err = new Error('Test error'); 139 | createBPAOrderCard.rejects(err); 140 | 141 | createBPAComponents(trelloEvent) 142 | .then(() => { 143 | t5.fail('rejects'); 144 | }) 145 | .catch(e => { 146 | t5.pass('rejects'); 147 | t5.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 148 | t5.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 149 | t5.equal(createBPAOrderBoard.callCount, 1, 'calls create BPA order board one time'); 150 | t5.equal(createBPAOrderBoard.args[0][0], cardFromTrello.name, 'board name equals card name'); 151 | t5.equal(createBPAOrderCard.callCount, 1, 'calls create BPA order card one time'); 152 | t5.equal(createBPAOrderCard.args[0][0], project, 'project argument is project name from card name'); 153 | t5.equal(createBPAOrderCard.args[0][1], agency, 'agency argument is agency name from card name'); 154 | t5.equal(e, err, 'returns the expected error'); 155 | }) 156 | .then(t5.done); 157 | }); 158 | 159 | t4.test('creating BPA order card resolves', t5 => { 160 | const bpaCard = { url: 'https://some.card.url/fdsa' }; 161 | 162 | t5.test('trello put returns an error', t6 => { 163 | trelloGet.resolves(cardFromTrello); 164 | createBPAOrderBoard.resolves(boardObj); 165 | createBPAOrderCard.resolves(bpaCard); 166 | const err = new Error('Test error'); 167 | trelloPut.rejects(err); 168 | 169 | createBPAComponents(trelloEvent) 170 | .then(() => { 171 | t6.fail('rejects'); 172 | }) 173 | .catch(e => { 174 | t6.pass('rejects'); 175 | t6.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 176 | t6.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 177 | t6.equal(createBPAOrderBoard.callCount, 1, 'calls create BPA order board one time'); 178 | t6.equal(createBPAOrderBoard.args[0][0], cardFromTrello.name, 'board name equals card name'); 179 | t6.equal(createBPAOrderCard.callCount, 1, 'calls create BPA order card one time'); 180 | t6.equal(createBPAOrderCard.args[0][0], project, 'project argument is project name from card name'); 181 | t6.equal(createBPAOrderCard.args[0][1], agency, 'agency argument is agency name from card name'); 182 | t6.equal(trelloPut.callCount, 1, 'calls Trello put one time'); 183 | t6.equal(trelloPut.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}/desc`, 'updates correct card description'); 184 | t6.same(trelloPut.args[0][1], { value: `---\n\n### Agile BPA Links\n\n* [Management Board](${boardObj.url})\n* [BPA Dashboard](${bpaCard.url})`}); 185 | t6.equal(e, err, 'returns the expected error'); 186 | }) 187 | .then(t6.done); 188 | }); 189 | 190 | t5.test('trello put succeeds', t6 => { 191 | trelloGet.resolves(cardFromTrello); 192 | createBPAOrderBoard.resolves(boardObj); 193 | createBPAOrderCard.resolves(bpaCard); 194 | trelloPut.resolves(''); 195 | 196 | createBPAComponents(trelloEvent) 197 | .then(() => { 198 | t6.pass('resolves'); 199 | t6.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 200 | t6.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 201 | t6.equal(createBPAOrderBoard.callCount, 1, 'calls create BPA order board one time'); 202 | t6.equal(createBPAOrderBoard.args[0][0], cardFromTrello.name, 'board name equals card name'); 203 | t6.equal(createBPAOrderCard.callCount, 1, 'calls create BPA order card one time'); 204 | t6.equal(createBPAOrderCard.args[0][0], project, 'project argument is project name from card name'); 205 | t6.equal(createBPAOrderCard.args[0][1], agency, 'agency argument is agency name from card name'); 206 | t6.equal(trelloPut.callCount, 1, 'calls Trello put one time'); 207 | t6.equal(trelloPut.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}/desc`, 'updates correct card description'); 208 | t6.same(trelloPut.args[0][1], { value: `---\n\n### Agile BPA Links\n\n* [Management Board](${boardObj.url})\n* [BPA Dashboard](${bpaCard.url})`}); 209 | }) 210 | .catch(() => { 211 | t6.fail('resolves'); 212 | }) 213 | .then(t6.done); 214 | }); 215 | 216 | t5.test('trello put succeeds, this time where the card already has a description', t6 => { 217 | cardFromTrello.desc = 'An existing description'; 218 | trelloGet.resolves(cardFromTrello); 219 | createBPAOrderBoard.resolves(boardObj); 220 | createBPAOrderCard.resolves(bpaCard); 221 | trelloPut.resolves(''); 222 | 223 | createBPAComponents(trelloEvent) 224 | .then(() => { 225 | t6.pass('resolves'); 226 | t6.equal(trelloGet.callCount, 1, 'calls Trello get one time'); 227 | t6.equal(trelloGet.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}`, 'requests correct card'); 228 | t6.equal(createBPAOrderBoard.callCount, 1, 'calls create BPA order board one time'); 229 | t6.equal(createBPAOrderBoard.args[0][0], cardFromTrello.name, 'board name equals card name'); 230 | t6.equal(createBPAOrderCard.callCount, 1, 'calls create BPA order card one time'); 231 | t6.equal(createBPAOrderCard.args[0][0], project, 'project argument is project name from card name'); 232 | t6.equal(createBPAOrderCard.args[0][1], agency, 'agency argument is agency name from card name'); 233 | t6.equal(trelloPut.callCount, 1, 'calls Trello put one time'); 234 | t6.equal(trelloPut.args[0][0], `/1/cards/${trelloEvent.action.data.card.id}/desc`, 'updates correct card description'); 235 | t6.same(trelloPut.args[0][1], { value: `${cardFromTrello.desc}\n\n---\n\n### Agile BPA Links\n\n* [Management Board](${boardObj.url})\n* [BPA Dashboard](${bpaCard.url})`}); 236 | }) 237 | .catch((e) => { 238 | t6.fail('resolves'); 239 | }) 240 | .then(t6.done); 241 | }); 242 | 243 | t5.done(); 244 | }); 245 | 246 | t4.done(); 247 | }); 248 | 249 | t3.done(); 250 | }); 251 | 252 | t2.done(); 253 | }); 254 | 255 | t1.done(); 256 | }); 257 | --------------------------------------------------------------------------------