├── .nvmrc ├── .travis.yml ├── Dockerfile ├── resources ├── avatar.png └── logo.sketch ├── index.js ├── .eslintrc ├── .gitignore ├── start.example.sh ├── wallaby.config.js ├── .editorconfig ├── bot.yml ├── src ├── constants.js ├── bot.js ├── server.js └── utils.js ├── LICENSE ├── package.json ├── README.md └── test └── utils.spec.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 6 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6-onbuild 2 | -------------------------------------------------------------------------------- /resources/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildbit/beanstalk-code-snippet-bot/HEAD/resources/avatar.png -------------------------------------------------------------------------------- /resources/logo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildbit/beanstalk-code-snippet-bot/HEAD/resources/logo.sketch -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Initialize storage 2 | const storage = require('node-persist') 3 | storage.initSync() 4 | 5 | require('./src/bot') 6 | require('./src/server') 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-wildbit", 3 | "env": { 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "rules": { 8 | "no-console": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directory 7 | node_modules 8 | 9 | # Optional npm cache directory 10 | .npm 11 | 12 | # IDE 13 | .idea 14 | start.sh 15 | persist 16 | 17 | # Build dir 18 | build 19 | -------------------------------------------------------------------------------- /start.example.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PORT=80 4 | export SLACK_VERIFY_TOKEN=[YOUR_SLACK_VERIFY_TOKEN] # https://api.slack.com/slash-commands 5 | export BEEPBOOP_ID=[BEEP BOOP ID] 6 | export BEEPBOOP_RESOURCER=[BEEP BOOP RESOURCER URL] 7 | export BEEPBOOP_TOKEN=[BEEP BOOP TOKEN] 8 | 9 | npm run start:dev 10 | -------------------------------------------------------------------------------- /wallaby.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return { 3 | files: [ 4 | 'testSetup.js', 5 | 'src/*.js' 6 | ], 7 | 8 | tests: ['test/*.js'], 9 | 10 | env: { 11 | type: 'node' 12 | }, 13 | 14 | testFramework: 'mocha' 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | continuation_indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.js] 13 | quote_type = single 14 | spaces_around_operators = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /bot.yml: -------------------------------------------------------------------------------- 1 | name: beanstalk-code-snippet-bot 2 | description: A Slack Bot that displays a code snippet for a specific file URL from Beanstalk 3 | avatar: resources/avatar.png 4 | slackscopes: 5 | - bot 6 | - commands 7 | - incoming-webhook 8 | config: 9 | - name: BS_USERNAME 10 | friendly_name: Beanstalk Username 11 | info: Your Beanstalk Username. This is case sensitive. 12 | default: beanstalk_username 13 | type: text 14 | - name: BS_AUTH_TOKEN 15 | friendly_name: Beanstalk Auth Token 16 | info: Your Beanstalk Authorization Token 17 | default: beanstalk_auth_token 18 | type: secret 19 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len: 0 */ 2 | 3 | const HELP_MESSAGE = 'I paste snippet code from Beanstalk links. \n\n' + 4 | '_How to get started_\n' + 5 | '1. Browse to a specific file in one of your Beanstalk repositories \n' + 6 | '2. In Beanstalk, click on the line of code you want to share \n' + 7 | '3. Copy the URL from your browser and paste it into slack \n' + 8 | ':champagne: :champagne:' 9 | 10 | const EMPTY_REQUEST = "I'll need a valid URL to the file on Beanstalk" 11 | 12 | const ERROR_MESSAGE = 'We had an issue getting the snippet from Beanstalk. Please make sure that you entered the correct username and authorization token.' 13 | 14 | const MISSING_AUTH = 'We could not find your Team\'s Beanstalk Authorization info. Please go fill it out.' 15 | 16 | const UNRECOGNIZED_REQUEST = 'I didn\'t understand that. Try asking for `help` or paste a Beanstalk file URL.' 17 | 18 | const BS_URL_MATCH = '.beanstalkapp.com/' 19 | 20 | module.exports = { 21 | HELP_MESSAGE, 22 | EMPTY_REQUEST, 23 | ERROR_MESSAGE, 24 | MISSING_AUTH, 25 | UNRECOGNIZED_REQUEST, 26 | BS_URL_MATCH 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Wildbit 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beanstalk-code-snippet-bot", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A Slack Bot that displays the contents of a file from your Beanstalk repository.", 6 | "main": "index.js", 7 | "dependencies": { 8 | "axios": "^0.9.1", 9 | "beepboop-botkit": "^1.3.0", 10 | "body-parser": "^1.15.0", 11 | "botkit": "0.0.15", 12 | "easy-crc32": "0.0.2", 13 | "express": "^4.13.4", 14 | "lodash": "^4.11.1", 15 | "morgan": "^1.7.0", 16 | "node-persist": "0.0.11" 17 | }, 18 | "devDependencies": { 19 | "eslint": "^2.7.0", 20 | "eslint-config-wildbit": "^2.0.1", 21 | "expect": "^1.16.0", 22 | "lint-staged": "^0.2.1", 23 | "mocha": "^2.4.5", 24 | "ngrok": "^2.1.8", 25 | "pre-commit": "^1.1.2" 26 | }, 27 | "scripts": { 28 | "start": "node index.js", 29 | "test": "mocha ./test/*.js", 30 | "proxy": "ngrok http -subdomain bscodebot 3000", 31 | "test:watch": "npm test -- --watch", 32 | "lint": "eslint src", 33 | "lint:fix": "npm run lint -- --fix", 34 | "eslint-staged": "eslint-staged" 35 | }, 36 | "pre-commit": [ 37 | "eslint-staged" 38 | ], 39 | "repository": { 40 | "type": "git", 41 | "url": "git+https://github.com/wildbit/beanstalk-code-snippet-bot.git" 42 | }, 43 | "keywords": [], 44 | "authors": [ 45 | "Andrey Okonetchnikov ", 46 | "Derek Rushforth " 47 | ], 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/wildbit/beanstalk-code-snippet-bot/issues" 51 | }, 52 | "homepage": "https://github.com/wildbit/beanstalk-code-snippet-bot#readme" 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # beanstalk-code-snippet-bot [![Build Status](https://travis-ci.org/wildbit/beanstalk-code-snippet-bot.svg?branch=master)](https://travis-ci.org/wildbit/beanstalk-code-snippet-bot) Add to Slack 2 | 3 | A Slack Bot that displays the contents of a file from your Beanstalk repository. So, instead of this: 4 | 5 | ![Before](https://dl.dropboxusercontent.com/s/pkylbo81nn1294n/2016-05-18%20at%2012.10.png) 6 | 7 | you can see something like this: 8 | 9 | ![After](https://dl.dropboxusercontent.com/s/ktzdwn5qax4065v/2016-05-16%20at%2016.52.png) 10 | 11 | ## Usage 12 | - In Slack, use the `/code` command to add a file from Beanstalk as a code snippet. `/code [Beanstalk file URL]` 13 | - This bot will listen for Beanstalk file URLs in any channel that it's a member of. Send an invite using `/invite @beanstalk-snippet-bot` 14 | 15 | ## Build and manage it yourself 16 | Build and manage your own instance of this bot by following these instructions. 17 | 18 | ### Requirements 19 | * [Beep Boop](https://beepboophq.com) account 20 | * [Beanstalk](https://beanstalkapp.com) account with authorization token 21 | * [Slack](https://slack.com) account with admin privileges 22 | 23 | ### Set up 24 | 1. Fork this repo onto your Github account 25 | 1. Create a new project on [Beep Boop](https://beepboophq.com) from your forked repo. 26 | 1. You might need to make an empty commit to trigger the build. 27 | 1. Once Beep Boop successfully builds, navigate to the settings page on Beep Boop. 28 | 1. Create a bot integration on your slack team and enter the API token onto Beep Boop's settings page. 29 | 1. Enter your Beanstalk username and authorization token onto Beep Boop's settings page. 30 | 1. Start your bot! 31 | 32 | ## MIT license 33 | See the [LICENSE](https://github.com/wildbit/beanstalk-code-snippet-bot/blob/master/LICENSE) file (MIT). 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/bot.js: -------------------------------------------------------------------------------- 1 | const getFileContents = require('./utils').getFileContents 2 | const Botkit = require('botkit') 3 | const BeepBoop = require('beepboop-botkit') 4 | const storage = require('node-persist') 5 | const { 6 | HELP_MESSAGE, 7 | ERROR_MESSAGE, 8 | MISSING_AUTH, 9 | BS_URL_MATCH, 10 | UNRECOGNIZED_REQUEST 11 | } = require('./constants') 12 | 13 | const controller = Botkit.slackbot() 14 | const beepboop = BeepBoop.start(controller) 15 | 16 | function setStorage(message) { 17 | storage.setItem(message.resourceID, { 18 | bsUsername: message.resource.BS_USERNAME, 19 | bsAuthToken: message.resource.BS_AUTH_TOKEN, 20 | slackTeamID: message.resource.SlackTeamID 21 | }) 22 | } 23 | 24 | beepboop.on('add_resource', (message) => { 25 | // When a team connects we persist their data so we can look it up later. 26 | // This also runs for each connected team every time the bot is started. 27 | setStorage(message) 28 | }) 29 | 30 | beepboop.on('update_resource', (message) => { 31 | // When a team updates their auth info we update their persisted data. 32 | setStorage(message) 33 | }) 34 | 35 | beepboop.on('remove_resource', (message) => { 36 | // When a team removes this bot we remove their data. 37 | storage.removeItem(message.resourceID) 38 | }) 39 | 40 | 41 | controller.hears( 42 | [BS_URL_MATCH], 43 | ['ambient', 'direct_mention', 'direct_message', 'mention'], 44 | (botInstance, message) => { 45 | 46 | const team = storage.getItem(botInstance.config.resourceID) 47 | 48 | // Validate Beanstalk Auth Info 49 | if (team.bsUsername === '' || team.bsAuthToken === '') { 50 | botInstance.reply(message, MISSING_AUTH) 51 | } 52 | 53 | // Make sure the message isn't from the slash command 54 | if (message.text.substr(0, 5) !== '/code') { 55 | getFileContents(message.text, { 56 | username: team.bsUsername, 57 | token: team.bsAuthToken 58 | }, (err, res) => { 59 | if (err) { 60 | botInstance.reply(message, ERROR_MESSAGE) 61 | throw new Error(`Error getting file contents: ${ err.message }`) 62 | } 63 | 64 | botInstance.reply(message, res) 65 | }) 66 | } 67 | 68 | }) 69 | 70 | controller.hears( 71 | ['help'], 72 | ['direct_message', 'direct_mention', 'mention'], 73 | (botInstance, message) => { 74 | botInstance.reply(message, HELP_MESSAGE) 75 | }) 76 | 77 | controller.hears( 78 | ['.*'], 79 | ['direct_message', 'direct_mention', 'mention'], 80 | (botInstance, message) => { 81 | botInstance.reply(message, UNRECOGNIZED_REQUEST) 82 | }) 83 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return: 0 */ 2 | const express = require('express') 3 | const morgan = require('morgan') 4 | const bodyParser = require('body-parser') 5 | const storage = require('node-persist') 6 | const { getFileContents } = require('./utils') 7 | const { 8 | HELP_MESSAGE, 9 | EMPTY_REQUEST, 10 | ERROR_MESSAGE, 11 | BS_URL_MATCH, 12 | UNRECOGNIZED_REQUEST 13 | } = require('./constants') 14 | 15 | const { SLACK_VERIFY_TOKEN, PORT } = process.env 16 | if (!SLACK_VERIFY_TOKEN) { 17 | console.error('SLACK_VERIFY_TOKEN is required') 18 | process.exit(1) 19 | } 20 | if (!PORT) { 21 | console.error('PORT is required') 22 | process.exit(1) 23 | } 24 | 25 | const app = express() 26 | app.use(morgan('dev')) 27 | 28 | app.route('/code') 29 | .get((req, res) => { 30 | res.sendStatus(200) 31 | }) 32 | .post(bodyParser.urlencoded({ extended: true }), (req, res) => { 33 | if (req.body.token !== SLACK_VERIFY_TOKEN) { 34 | return res.sendStatus(401) 35 | } 36 | 37 | const { text, team_id } = req.body 38 | 39 | // Handle empty request 40 | if (!text) { 41 | return res.json({ 42 | response_type: 'ephemeral', 43 | text: EMPTY_REQUEST 44 | }) 45 | } 46 | 47 | // Handle any help requests 48 | if (text === 'help') { 49 | return res.json({ 50 | response_type: 'ephemeral', 51 | text: HELP_MESSAGE 52 | }) 53 | } 54 | 55 | if (text.indexOf(BS_URL_MATCH) > -1) { 56 | // Iterate through storage data 57 | storage.forEach((key, value) => { 58 | 59 | // Match team ID in storage from request 60 | if (value.slackTeamID === team_id) { // eslint-disable-line 61 | 62 | // Get file contents from Beanstalk 63 | getFileContents(text, { 64 | username: value.bsUsername, 65 | token: value.bsAuthToken 66 | }, (err, content) => { 67 | if (err) { 68 | return res.json({ 69 | response_type: 'ephemeral', 70 | text: ERROR_MESSAGE 71 | }) 72 | } 73 | 74 | /* 75 | * TODO: If this request takes more than 3000ms slack will not post our 76 | * response. Instead we should probably return an initial message to the 77 | * user("Looking up your file"). Then after send the file as an incoming 78 | * webhook using response_url. 79 | * */ 80 | return res.json( 81 | Object.assign({ 82 | response_type: 'in_channel' 83 | }, content) 84 | ) 85 | }) 86 | } 87 | }) 88 | } else { 89 | return res.json({ 90 | response_type: 'ephemeral', 91 | text: UNRECOGNIZED_REQUEST 92 | }) 93 | } 94 | 95 | }) 96 | 97 | app.listen(PORT, (err) => { 98 | if (err) { 99 | return console.error('Error starting server: ', err) 100 | } 101 | 102 | return console.log('Server successfully started on port %s', PORT) 103 | }) 104 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const crc = require('easy-crc32') 2 | const axios = require('axios') 3 | const padStart = require('lodash/padStart') 4 | 5 | // const NAME = 'Beanstalk Code Snippet Bot' 6 | const LINES_OFFSET = 3 7 | 8 | function getSanitizedPath(path) { 9 | return path 10 | .replace(/^\//, '') 11 | .replace(/\/$/, '') 12 | } 13 | 14 | function getLocCrc(filepath, lineNum) { 15 | return crc.calculate(getSanitizedPath(filepath)) + crc.calculate(`${ lineNum }`) 16 | } 17 | 18 | function parseUrl(url) { 19 | const re = new RegExp(/([\w-_]+)\.beanstalkapp\.com\/([\w-_]+)\/browse\/([^/]+)\/([\w-_/.]+)(\?ref=c-(\w+))?(#L(\d+))?/g) // eslint-disable-line 20 | const matches = re.exec(url) 21 | if (matches) { 22 | const [, accountName, repositoryName, gitOrSubversionBaseDir, filepath, ...rest] = matches 23 | const [, revision, , locHash] = rest 24 | return { 25 | accountName, 26 | repositoryName, 27 | filepath: ((gitOrSubversionBaseDir === 'git') ? filepath : gitOrSubversionBaseDir + '/' + filepath), // eslint-disable-line 28 | locHash, 29 | revision 30 | } 31 | } 32 | return {} 33 | } 34 | 35 | function linesHashMap(content, filepath) { 36 | return content.split('\n').map((line, lineNumber) => getLocCrc(filepath, lineNumber + 1)) 37 | } 38 | 39 | function getLineNumberFromHash(locHash, content, filepath) { 40 | return linesHashMap(content, filepath).indexOf(parseInt(locHash, 10)) + 1 41 | } 42 | 43 | function linesAsArrayWithLineNumbers(content) { 44 | const lines = content.split('\n') 45 | const padding = (`${ lines.length }`).length 46 | return lines.map((line, idx) => `${ padStart(idx + 1, padding, '0') }. ${ line }`) 47 | } 48 | 49 | function getLinesAround(content, line, offset = LINES_OFFSET) { 50 | // line is 1-based so we will need to convert it to 0-based everywhere 51 | const lines = linesAsArrayWithLineNumbers(content) 52 | const totalLines = lines.length - 2 53 | const minLine = Math.max(line - 1 - offset, 0) 54 | const maxLine = Math.min(line - 1 + offset, totalLines) 55 | const newLines = minLine > 0 ? ['...'] : [] 56 | for (let currLine = minLine; currLine <= maxLine; currLine++) { 57 | newLines.push(lines[currLine]) 58 | } 59 | if (maxLine < totalLines) { 60 | newLines.push('...') 61 | } 62 | return newLines 63 | } 64 | 65 | function getContentWithAttachements(response, url) { 66 | let lineNumber 67 | let fileContents 68 | const { locHash } = parseUrl(url) 69 | const { path, contents, revision, repository } = response.data 70 | if (locHash) { 71 | lineNumber = getLineNumberFromHash(locHash, contents, path) 72 | fileContents = getLinesAround(contents, lineNumber).join('\n') 73 | } else { 74 | fileContents = linesAsArrayWithLineNumbers(contents).join('\n') 75 | } 76 | 77 | 78 | return { 79 | username: path, 80 | text: `\`\`\`${ fileContents }\n\`\`\``, 81 | attachments: [{ 82 | fallback: path, 83 | fields: [{ 84 | title: 'Repository', 85 | value: repository.title, 86 | short: true 87 | }, { 88 | title: 'Revision', 89 | value: revision, 90 | short: true 91 | }] 92 | }] 93 | } 94 | } 95 | 96 | function getFileContents(url, options, cb) { 97 | const { username, token } = options 98 | if (!username || !token) { 99 | throw new Error('Beanstalk username and token are required') 100 | } 101 | const { accountName, repositoryName, filepath, revision } = parseUrl(url) 102 | const apiUrl = `https://${ accountName }.beanstalkapp.com/api` 103 | const authStr = `${ username }:${ token }` 104 | const encodedAuthStr = new Buffer(authStr).toString('base64') 105 | axios 106 | .get(`${ apiUrl }/repositories/${ repositoryName }/node.json`, { 107 | params: { 108 | path: filepath, 109 | revision, 110 | contents: true 111 | }, 112 | headers: { 113 | Authorization: `Basic ${ encodedAuthStr }` 114 | } 115 | }) 116 | .then(res => cb(null, getContentWithAttachements(res, url))) 117 | .catch(err => cb(err, null)) 118 | } 119 | 120 | module.exports = { 121 | parseUrl, 122 | getContentWithAttachements, 123 | getSanitizedPath, 124 | getLocCrc, 125 | linesHashMap, 126 | getLineNumberFromHash, 127 | linesAsArrayWithLineNumbers, 128 | getLinesAround, 129 | getFileContents 130 | } 131 | -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | 3 | const expect = require('expect') 4 | const { 5 | parseUrl, 6 | getContentWithAttachements, 7 | getSanitizedPath, 8 | getLocCrc, 9 | linesHashMap, 10 | getLineNumberFromHash, 11 | linesAsArrayWithLineNumbers, 12 | getLinesAround, 13 | getFileContents 14 | } = require('../src/utils') 15 | 16 | describe('utils', () => { 17 | describe('getFileContents', () => { 18 | it('should be defined', () => { 19 | expect(getFileContents).toExist() 20 | }) 21 | }) 22 | 23 | describe('getSanitizedPath', () => { 24 | it('should remove leading and trailing slashed from the path', () => { 25 | expect(getSanitizedPath('test')).toEqual('test') 26 | expect(getSanitizedPath('/test')).toEqual('test') 27 | expect(getSanitizedPath('test/')).toEqual('test') 28 | expect(getSanitizedPath('/test/')).toEqual('test') 29 | expect(getSanitizedPath('/test/test2')).toEqual('test/test2') 30 | expect(getSanitizedPath('/test/test2/')).toEqual('test/test2') 31 | expect(getSanitizedPath('test/test2')).toEqual('test/test2') 32 | }) 33 | }) 34 | 35 | describe('getLocCrc', () => { 36 | it('should return proper hashes', () => { 37 | expect(getLocCrc('/config.ru', 4)).toEqual(7082412740) 38 | expect(getLocCrc('index.js', 1)).toEqual(4259026361) 39 | expect(getLocCrc('index.js', 5)).toEqual(4272935344) 40 | expect(getLocCrc('index.js', 6)).toEqual(2545360918) 41 | expect(getLocCrc('app/schemas/v1/multi_release_schema.rb', 11)).toEqual(3830012464) 42 | }) 43 | }) 44 | 45 | describe('linesHashMap', () => { 46 | it('should return proper hashes', () => { 47 | const content = 48 | `line 1 49 | line 2 50 | ` 51 | expect(linesHashMap(content, 'index.js')).toBeAn('array') 52 | expect(linesHashMap(content, 'index.js').length).toEqual(3) 53 | expect(linesHashMap(content, 'index.js')).toEqual([4259026361, 2496947215, 3889247389]) 54 | }) 55 | }) 56 | 57 | describe('getLineNumberFromHash', () => { 58 | it('should return proper hashes', () => { 59 | const content = 60 | `line 1 61 | line 2 62 | 63 | line 4 64 | ` 65 | expect(getLineNumberFromHash(4259026361, content, 'index.js')).toEqual(1) 66 | expect(getLineNumberFromHash(3889247389, content, 'index.js')).toEqual(3) 67 | expect(getLineNumberFromHash(4272935344, content, 'index.js')).toEqual(5) 68 | }) 69 | }) 70 | 71 | describe('parseUrl', () => { 72 | const url1 = '' 73 | const url2 = 'http://test-complex123.beanstalkapp.com/testRepo/browse/git/index.js' 74 | const url3 = 'test-complex123.beanstalkapp.com/' 75 | const url4 = 'https://derekandrey.beanstalkapp.com/codesnippet-tets/browse/git/index.js?ref=c-397c63ede5221cfeef426a2b861132255e35a7bf#L3830012464' 76 | const url5 = 'https://myaccount.beanstalkapp.com/myrepo/browse/trunk/path/file.ext?ref=c-12345#L54321' 77 | 78 | it('should return an empty object for no mathces', () => { 79 | expect(parseUrl(undefined)).toEqual({}) 80 | expect(parseUrl(null)).toEqual({}) 81 | expect(parseUrl('some random string')).toEqual({}) 82 | }) 83 | 84 | it('should return account name', () => { 85 | expect(parseUrl(url1).accountName).toEqual('wb') 86 | expect(parseUrl(url2).accountName).toEqual('test-complex123') 87 | expect(parseUrl(url3)).toEqual({}) 88 | expect(parseUrl(url4).accountName).toEqual('derekandrey') 89 | }) 90 | 91 | it('should return repository name', () => { 92 | expect(parseUrl(url1).repositoryName).toEqual('beanstalk') 93 | expect(parseUrl(url2).repositoryName).toEqual('testRepo') 94 | expect(parseUrl(url4).repositoryName).toEqual('codesnippet-tets') 95 | }) 96 | 97 | it('should return filepath', () => { 98 | expect(parseUrl(url1).filepath).toEqual('app/schemas/v1/multi_release_schema.rb') 99 | expect(parseUrl(url2).filepath).toEqual('index.js') 100 | expect(parseUrl(url4).filepath).toEqual('index.js') 101 | expect(parseUrl(url5).filepath).toEqual('trunk/path/file.ext') 102 | }) 103 | 104 | it('should return line hash', () => { 105 | expect(parseUrl(url1).locHash).toEqual('3830012464') 106 | expect(parseUrl(url2).locHash).toBe(undefined) 107 | expect(parseUrl(url4).locHash).toEqual('3830012464') 108 | expect(parseUrl(url5).locHash).toEqual('54321') 109 | }) 110 | 111 | it('should return revision number', () => { 112 | expect(parseUrl(url1).revision).toBe(undefined) 113 | expect(parseUrl(url2).revision).toBe(undefined) 114 | expect(parseUrl(url4).revision).toEqual('397c63ede5221cfeef426a2b861132255e35a7bf') 115 | expect(parseUrl(url5).revision).toEqual('12345') 116 | }) 117 | 118 | }) 119 | 120 | describe('withLineNumbers', () => { 121 | it('should return content with line numbers', () => { 122 | /* eslint-disable */ 123 | const content = 124 | `line 1 125 | line 2 126 | 127 | line 4 128 | ` 129 | const expected = 130 | `1. line 1 131 | 2. line 2 132 | 3. 133 | 4. line 4 134 | 5. ` 135 | expect(linesAsArrayWithLineNumbers(content).join('\n')).toEqual(expected) 136 | }) 137 | 138 | it('should pad line numbers', () => { 139 | const content = 140 | `line 1 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | line10 150 | ` 151 | const expected = 152 | `01. line 1 153 | 02. 154 | 03. 155 | 04. 156 | 05. 157 | 06. 158 | 07. 159 | 08. 160 | 09. 161 | 10. line10 162 | 11. ` 163 | /* eslint-enable */ 164 | expect(linesAsArrayWithLineNumbers(content).join('\n')).toEqual(expected) 165 | }) 166 | }) 167 | describe('getLinesAround', () => { 168 | it('should return lines around the specified line +- offset', () => { 169 | const content = 170 | `line 1 171 | line 2 172 | line 3 173 | line 4 174 | line 5 175 | line 6 176 | line 7 177 | line 8 178 | line 9 179 | line 10 180 | ` 181 | const expected1 = 182 | `01. line 1 183 | 02. line 2 184 | 03. line 3 185 | ...` 186 | 187 | const expected2 = 188 | `... 189 | 03. line 3 190 | 04. line 4 191 | 05. line 5 192 | 06. line 6 193 | 07. line 7 194 | ...` 195 | 196 | const expected3 = 197 | `... 198 | 08. line 8 199 | 09. line 9 200 | 10. line 10` 201 | /* eslint-enable */ 202 | expect(getLinesAround(content, 1, 2).join('\n')).toEqual(expected1) 203 | expect(getLinesAround(content, 5, 2).join('\n')).toEqual(expected2) 204 | expect(getLinesAround(content, 10, 2).join('\n')).toEqual(expected3) 205 | }) 206 | }) 207 | 208 | describe('getContentWithAttachements', () => { 209 | const contents = `var Botkit = require('botkit') 210 | 211 | // Expect a SLACK_TOKEN environment variable 212 | var slackToken = process.env.SLACK_TOKEN 213 | if (!slackToken) { 214 | console.error('SLACK_TOKEN is required!') 215 | process.exit(1) 216 | } 217 | 218 | ` 219 | const response = { 220 | /* eslint-disable */ 221 | "data": { 222 | "repository": { 223 | "id": 686137, 224 | "account_id": 184458, 225 | "name": "codesnippet-tets", 226 | "created_at": "2016/04/12 21:16:42 +0000", 227 | "updated_at": "2016/04/12 21:22:09 +0000", 228 | "title": "codesnippet-tests", 229 | "color_label": "white", 230 | "storage_used_bytes": 52224, 231 | "last_commit_at": "2016/04/12 21:22:07 +0000", 232 | "type": "GitRepository", 233 | "default_branch": "master", 234 | "vcs": "git", 235 | "repository_url": "git@derekandrey.git.beanstalkapp.com:/derekandrey/codesnippet-tets.git", 236 | "repository_url_https": "https://derekandrey.git.beanstalkapp.com/codesnippet-tets.git", 237 | }, 238 | "name": "index.js", 239 | "path": "index.js", 240 | "revision": "397c63ede5221cfeef426a2b861132255e35a7bf", 241 | "directory": false, 242 | "file": true, 243 | "binary": false, 244 | "mime_type": "application/javascript", 245 | "language": "javascript", 246 | "contents": contents 247 | } 248 | /* eslint-enable */ 249 | } 250 | it('should return an object with attachement', () => { 251 | const url = 'https://derekandrey.beanstalkapp.com/codesnippet-tets/browse/git/index.js?ref=c-397c63ede5221cfeef426a2b861132255e35a7bf' 252 | const expected = { 253 | /* eslint-disable */ 254 | "username": "index.js", 255 | "text": `\`\`\`01. var Botkit = require('botkit') 256 | 02. 257 | 03. // Expect a SLACK_TOKEN environment variable 258 | 04. var slackToken = process.env.SLACK_TOKEN 259 | 05. if (!slackToken) { 260 | 06. console.error('SLACK_TOKEN is required!') 261 | 07. process.exit(1) 262 | 08. } 263 | 09. 264 | 10. 265 | \`\`\``, 266 | "attachments": [ 267 | { 268 | "fallback": "index.js", 269 | "fields": [ 270 | { 271 | "title": "Repository", 272 | "value": "codesnippet-tests", 273 | "short": true 274 | }, 275 | { 276 | "title": "Revision", 277 | "value": "397c63ede5221cfeef426a2b861132255e35a7bf", 278 | "short": true 279 | } 280 | ] 281 | } 282 | /* eslint-enable */ 283 | ] 284 | } 285 | expect(getContentWithAttachements(response, url)).toEqual(expected) 286 | }) 287 | 288 | it('should support LOC anchors', () => { 289 | const url = 'https://derekandrey.beanstalkapp.com/codesnippet-tets/browse/git/index.js?ref=c-397c63ede5221cfeef426a2b861132255e35a7bf#L4272935344' 290 | const expected = { 291 | /* eslint-disable */ 292 | "username": "index.js", 293 | "text": `\`\`\`... 294 | 02. 295 | 03. // Expect a SLACK_TOKEN environment variable 296 | 04. var slackToken = process.env.SLACK_TOKEN 297 | 05. if (!slackToken) { 298 | 06. console.error('SLACK_TOKEN is required!') 299 | 07. process.exit(1) 300 | 08. } 301 | ... 302 | \`\`\``, 303 | "attachments": [ 304 | { 305 | "fallback": "index.js", 306 | "fields": [ 307 | { 308 | "title": "Repository", 309 | "value": "codesnippet-tests", 310 | "short": true 311 | }, 312 | { 313 | "title": "Revision", 314 | "value": "397c63ede5221cfeef426a2b861132255e35a7bf", 315 | "short": true 316 | } 317 | ] 318 | } 319 | /* eslint-enable */ 320 | ] 321 | } 322 | expect(getContentWithAttachements(response, url)).toEqual(expected) 323 | }) 324 | }) 325 | }) 326 | --------------------------------------------------------------------------------