├── .gitignore ├── .npmignore ├── .travis.yml ├── Gruntfile.js ├── README.md ├── docs ├── docco.css ├── jira.html └── public │ ├── fonts │ ├── aller-bold.eot │ ├── aller-bold.ttf │ ├── aller-bold.woff │ ├── aller-light.eot │ ├── aller-light.ttf │ ├── aller-light.woff │ ├── novecento-bold.eot │ ├── novecento-bold.ttf │ └── novecento-bold.woff │ └── stylesheets │ └── normalize.css ├── lib └── jira.js ├── package.json └── spec └── jira.spec.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log 16 | 17 | .idea 18 | *.iml 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steves/node-jira/6429788b7e1621cf35cd13839d06e210d7d8be02/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: npm install -g grunt-cli 5 | install: npm install 6 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | "use strict"; 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: '', 6 | jasmine_node: { 7 | all: ["./spec"], 8 | options: { 9 | forceExit: false, 10 | extensions: 'coffee', 11 | jUnit: { 12 | report: false, 13 | savePath : "./build/reports/jasmine/", 14 | useDotNotation: true, 15 | consolidate: true 16 | } 17 | } 18 | }, 19 | docco: { 20 | app: { 21 | src: ['lib/*.js'] 22 | } 23 | }, 24 | watch: { 25 | files: ['lib/**/*.js', 'spec/**/*.coffee'], 26 | tasks: 'default' 27 | }, 28 | jslint: { 29 | client: { 30 | src: ['./Gruntfile.js', 'lib/**/*.js'], 31 | directives: { 32 | indent: 2, 33 | curly: true, 34 | eqeqeq: true, 35 | eqnull: true, 36 | immed: true, 37 | latedef: true, 38 | newcap: true, 39 | noarg: true, 40 | sub: true, 41 | undef: true, 42 | unused: true, 43 | boss: true, 44 | browser: true, 45 | predef: ['module', 'require', 'console', 'exports'] 46 | } 47 | } 48 | } 49 | }); 50 | 51 | // Default task. 52 | grunt.registerTask('default', ['jslint', 'jasmine_node', 'docco']); 53 | grunt.registerTask('prepare', 'default bump'); 54 | grunt.registerTask('test', 'jasmine_node'); 55 | 56 | grunt.loadNpmTasks('grunt-jasmine-node'); 57 | grunt.loadNpmTasks('grunt-docco'); 58 | grunt.loadNpmTasks('grunt-bump'); 59 | grunt.loadNpmTasks('grunt-jslint'); 60 | }; 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaScript JIRA API for node.js # 2 | 3 | [![Build Status](https://travis-ci.org/steves/node-jira.png?branch=master)](https://travis-ci.org/steves/node-jira) 4 | 5 | A node.js module, which provides an object oriented wrapper for the JIRA REST API. 6 | 7 | This library is built to support version `2.0.alpha1` of the JIRA REST API. 8 | This library is also tested with version `2` of the JIRA REST API. 9 | It has been noted that with Jira OnDemand, `2.0.alpha1` does not work, devs 10 | should revert to `2`. If this changes, please notify us. 11 | 12 | JIRA REST API documentation can be found [here](http://docs.atlassian.com/jira/REST/latest/) 13 | 14 | ## Installation ## 15 | 16 | Install with the node package manager [npm](http://npmjs.org): 17 | 18 | $ npm install jira 19 | 20 | or 21 | 22 | Install via git clone: 23 | 24 | $ git clone git://github.com/steves/node-jira.git 25 | $ cd node-jira 26 | $ npm install 27 | 28 | ## Examples ## 29 | 30 | ### Create the JIRA client ### 31 | 32 | JiraApi = require('jira').JiraApi; 33 | 34 | var jira = new JiraApi('https', config.host, config.port, config.user, config.password, '2.0.alpha1'); 35 | 36 | ### Find the status of an issue ### 37 | 38 | jira.findIssue(issueNumber, function(error, issue) { 39 | console.log('Status: ' + issue.fields.status.name); 40 | }); 41 | 42 | 43 | Currently there is no explicit login call necessary as each API call uses Basic Authentication to authenticate. 44 | 45 | ### Get an issue remote links ### 46 | 47 | Returns an array of remote links data. 48 | 49 | jira.getRemoteLinks(issueKey, function(err, links) { 50 | if (!err) { 51 | console.log(issueKey + ' has ' + links.length + ' web links'); 52 | } 53 | }); 54 | 55 | ### Create a remote link on an issue ### 56 | 57 | Returns an array of remote links data. 58 | 59 | // create a web link to a GitHub issue 60 | var linkData = { 61 | "object": { 62 | "url" : "https://github.com/steves/node-jira/issues/1", 63 | "title": "Add getVersions and createVersion functions", 64 | "icon" : { 65 | "url16x16": "https://github.com/favicon.ico" 66 | } 67 | } 68 | }; 69 | 70 | jira.createRemoteLink(issueKey, linkData, function (err, link) { 71 | 72 | }); 73 | 74 | More information can be found by checking [JIRA Developer documentation](https://developer.atlassian.com/display/JIRADEV/JIRA+REST+API+for+Remote+Issue+Links#JIRARESTAPIforRemoteIssueLinks-CreatingLinks). 75 | 76 | ## Options ## 77 | 78 | JiraApi options: 79 | * `protocol`: Typically 'http:' or 'https:' 80 | * `host`: The hostname for your jira server 81 | * `port`: The port your jira server is listening on (probably `80` or `443`) 82 | * `user`: The username to log in with 83 | * `password`: Keep it secret, keep it safe 84 | * `Jira API Version`: Known to work with `2` and `2.0.alpha1` 85 | * `verbose`: Log some info to the console, usually for debugging 86 | * `strictSSL`: Set to false if you have self-signed certs or something non-trustworthy 87 | * `oauth`: A dictionary of `consumer_key`, `consumer_secret`, `access_token` and `access_token_secret` to be used for OAuth authentication. 88 | * `base`: A base slug if your JIRA instance is not at the root of `host` 89 | 90 | ## Implemented APIs ## 91 | 92 | * Authentication 93 | * HTTP 94 | * OAuth 95 | * Projects 96 | * Pulling a project 97 | * List all projects viewable to the user 98 | * List Components 99 | * List Fields 100 | * List Priorities 101 | * Versions 102 | * Pulling versions 103 | * Adding a new version 104 | * Pulling unresolved issues count for a specific version 105 | * Rapid Views 106 | * Find based on project name 107 | * Get the latest Green Hopper sprint 108 | * Gets attached issues 109 | * Issues 110 | * Add a new issue 111 | * Update an issue 112 | * Add watcher to an issue 113 | * Transition an issue 114 | * Pulling an issue 115 | * Issue linking 116 | * Add an issue to a sprint 117 | * Get a users issues (open or all) 118 | * List issue types 119 | * Search using jql 120 | * Set Max Results 121 | * Set Start-At parameter for results 122 | * Add a worklog 123 | * Delete a worklog 124 | * Add new estimate for worklog 125 | * Add a comment 126 | * Remote links (aka Web Links) 127 | * Create a remote link on an issue 128 | * Get all remote links of an issue 129 | * Transitions 130 | * List 131 | * Users 132 | * Search 133 | 134 | ## TODO ## 135 | 136 | * Refactor currently implemented APIs to be more Object Oriented 137 | * Refactor to make use of built-in node.js events and classes 138 | 139 | ## Changelog ## 140 | 141 | 142 | * _0.9.2 Smaller fixes and features added_ 143 | * proper doRequest usage (thanks to [ndamnjanovic](https://github.com/ndamnjanovic)) 144 | * Support for @ in usernames (thanks to [ryanplasma](https://github.com/ryanplasma)) 145 | * Handling empty responses in getIssue 146 | * _0.9.0 Add OAuth Support and New Estimates on addWorklog (thanks to [nagyv](https://github.com/nagyv))_ 147 | * _0.8.2 Fix URL Format Issues (thanks to 148 | [eduardolundgren](https://github.com/eduardolundgren))_ 149 | * _0.8.1 Expanding the transitions options (thanks to 150 | [eduardolundgren](https://github.com/eduardolundgren))_ 151 | * _0.8.0 Ability to search users (thanks to 152 | [eduardolundgren](https://github.com/eduardolundgren))_ 153 | * _0.7.2 Allows HTTP Code 204 on issue update edit (thanks to 154 | [eduardolundgren](https://github.com/eduardolundgren))_ 155 | * _0.7.1 Check if body variable is undef (thanks to 156 | [AlexCline](https://github.com/AlexCline))_ 157 | * _0.7.0 Adds list priorities, list fields, and project components (thanks to 158 | [eduardolundgren](https://github.com/eduardolundgren))_ 159 | * _0.6.0 Comment API implemented (thanks to [StevenMcD](https://github.com/StevenMcD))_ 160 | * _0.5.0 Last param is now for strict SSL checking, defaults to true_ 161 | * _0.4.1 Now handing errors in the request callback (thanks [mrbrookman](https://github.com/mrbrookman))_ 162 | * _0.4.0 Now auto-redirecting between http and https (for both GET and POST)_ 163 | * _0.3.1 [Request](https://github.com/mikeal/request) is broken, setting max request package at 2.15.0_ 164 | * _0.3.0 Now Gets Issues for a Rapidview/Sprint (thanks [donbonifacio](https://github.com/donbonifacio))_ 165 | * _0.2.0 Now allowing startAt and MaxResults to be passed to searchJira, 166 | switching to semantic versioning._ 167 | * _0.1.0 Using Basic Auth instead of cookies, all calls unit tested, URI 168 | creation refactored_ 169 | * _0.0.6 Now linting, preparing to refactor_ 170 | * _0.0.5 JQL search now takes a list of fields_ 171 | * _0.0.4 Added jql search_ 172 | * _0.0.3 Added APIs and Docco documentation_ 173 | * _0.0.2 Initial version_ 174 | -------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Typography ----------------------------*/ 2 | 3 | @font-face { 4 | font-family: 'aller-light'; 5 | src: url('public/fonts/aller-light.eot'); 6 | src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'), 7 | url('public/fonts/aller-light.woff') format('woff'), 8 | url('public/fonts/aller-light.ttf') format('truetype'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: 'aller-bold'; 15 | src: url('public/fonts/aller-bold.eot'); 16 | src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'), 17 | url('public/fonts/aller-bold.woff') format('woff'), 18 | url('public/fonts/aller-bold.ttf') format('truetype'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'novecento-bold'; 25 | src: url('public/fonts/novecento-bold.eot'); 26 | src: url('public/fonts/novecento-bold.eot?#iefix') format('embedded-opentype'), 27 | url('public/fonts/novecento-bold.woff') format('woff'), 28 | url('public/fonts/novecento-bold.ttf') format('truetype'); 29 | font-weight: normal; 30 | font-style: normal; 31 | } 32 | 33 | /*--------------------- Layout ----------------------------*/ 34 | html { height: 100%; } 35 | body { 36 | font-family: "aller-light"; 37 | font-size: 14px; 38 | line-height: 18px; 39 | color: #30404f; 40 | margin: 0; padding: 0; 41 | height:100%; 42 | } 43 | #container { min-height: 100%; } 44 | 45 | a { 46 | color: #000; 47 | } 48 | 49 | b, strong { 50 | font-weight: normal; 51 | font-family: "aller-bold"; 52 | } 53 | 54 | p { 55 | margin: 15px 0 0px; 56 | } 57 | .annotation ul, .annotation ol { 58 | margin: 25px 0; 59 | } 60 | .annotation ul li, .annotation ol li { 61 | font-size: 14px; 62 | line-height: 18px; 63 | margin: 10px 0; 64 | } 65 | 66 | h1, h2, h3, h4, h5, h6 { 67 | color: #112233; 68 | line-height: 1em; 69 | font-weight: normal; 70 | font-family: "novecento-bold"; 71 | text-transform: uppercase; 72 | margin: 30px 0 15px 0; 73 | } 74 | 75 | h1 { 76 | margin-top: 40px; 77 | } 78 | 79 | hr { 80 | border: 0; 81 | background: 1px #ddd; 82 | height: 1px; 83 | margin: 20px 0; 84 | } 85 | 86 | pre, tt, code { 87 | font-size: 12px; line-height: 16px; 88 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 89 | margin: 0; padding: 0; 90 | } 91 | .annotation pre { 92 | display: block; 93 | margin: 0; 94 | padding: 7px 10px; 95 | background: #fcfcfc; 96 | -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 97 | -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 98 | box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 99 | overflow-x: auto; 100 | } 101 | .annotation pre code { 102 | border: 0; 103 | padding: 0; 104 | background: transparent; 105 | } 106 | 107 | 108 | blockquote { 109 | border-left: 5px solid #ccc; 110 | margin: 0; 111 | padding: 1px 0 1px 1em; 112 | } 113 | .sections blockquote p { 114 | font-family: Menlo, Consolas, Monaco, monospace; 115 | font-size: 12px; line-height: 16px; 116 | color: #999; 117 | margin: 10px 0 0; 118 | white-space: pre-wrap; 119 | } 120 | 121 | ul.sections { 122 | list-style: none; 123 | padding:0 0 5px 0;; 124 | margin:0; 125 | } 126 | 127 | /* 128 | Force border-box so that % widths fit the parent 129 | container without overlap because of margin/padding. 130 | 131 | More Info : http://www.quirksmode.org/css/box.html 132 | */ 133 | ul.sections > li > div { 134 | -moz-box-sizing: border-box; /* firefox */ 135 | -ms-box-sizing: border-box; /* ie */ 136 | -webkit-box-sizing: border-box; /* webkit */ 137 | -khtml-box-sizing: border-box; /* konqueror */ 138 | box-sizing: border-box; /* css3 */ 139 | } 140 | 141 | 142 | /*---------------------- Jump Page -----------------------------*/ 143 | #jump_to, #jump_page { 144 | margin: 0; 145 | background: white; 146 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 147 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 148 | font: 16px Arial; 149 | cursor: pointer; 150 | text-align: right; 151 | list-style: none; 152 | } 153 | 154 | #jump_to a { 155 | text-decoration: none; 156 | } 157 | 158 | #jump_to a.large { 159 | display: none; 160 | } 161 | #jump_to a.small { 162 | font-size: 22px; 163 | font-weight: bold; 164 | color: #676767; 165 | } 166 | 167 | #jump_to, #jump_wrapper { 168 | position: fixed; 169 | right: 0; top: 0; 170 | padding: 10px 15px; 171 | margin:0; 172 | } 173 | 174 | #jump_wrapper { 175 | display: none; 176 | padding:0; 177 | } 178 | 179 | #jump_to:hover #jump_wrapper { 180 | display: block; 181 | } 182 | 183 | #jump_page { 184 | padding: 5px 0 3px; 185 | margin: 0 0 25px 25px; 186 | } 187 | 188 | #jump_page .source { 189 | display: block; 190 | padding: 15px; 191 | text-decoration: none; 192 | border-top: 1px solid #eee; 193 | } 194 | 195 | #jump_page .source:hover { 196 | background: #f5f5ff; 197 | } 198 | 199 | #jump_page .source:first-child { 200 | } 201 | 202 | /*---------------------- Low resolutions (> 320px) ---------------------*/ 203 | @media only screen and (min-width: 320px) { 204 | .pilwrap { display: none; } 205 | 206 | ul.sections > li > div { 207 | display: block; 208 | padding:5px 10px 0 10px; 209 | } 210 | 211 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 212 | padding-left: 30px; 213 | } 214 | 215 | ul.sections > li > div.content { 216 | overflow-x:auto; 217 | -webkit-box-shadow: inset 0 0 5px #e5e5ee; 218 | box-shadow: inset 0 0 5px #e5e5ee; 219 | border: 1px solid #dedede; 220 | margin:5px 10px 5px 10px; 221 | padding-bottom: 5px; 222 | } 223 | 224 | ul.sections > li > div.annotation pre { 225 | margin: 7px 0 7px; 226 | padding-left: 15px; 227 | } 228 | 229 | ul.sections > li > div.annotation p tt, .annotation code { 230 | background: #f8f8ff; 231 | border: 1px solid #dedede; 232 | font-size: 12px; 233 | padding: 0 0.2em; 234 | } 235 | } 236 | 237 | /*---------------------- (> 481px) ---------------------*/ 238 | @media only screen and (min-width: 481px) { 239 | #container { 240 | position: relative; 241 | } 242 | body { 243 | background-color: #F5F5FF; 244 | font-size: 15px; 245 | line-height: 21px; 246 | } 247 | pre, tt, code { 248 | line-height: 18px; 249 | } 250 | p, ul, ol { 251 | margin: 0 0 15px; 252 | } 253 | 254 | 255 | #jump_to { 256 | padding: 5px 10px; 257 | } 258 | #jump_wrapper { 259 | padding: 0; 260 | } 261 | #jump_to, #jump_page { 262 | font: 10px Arial; 263 | text-transform: uppercase; 264 | } 265 | #jump_page .source { 266 | padding: 5px 10px; 267 | } 268 | #jump_to a.large { 269 | display: inline-block; 270 | } 271 | #jump_to a.small { 272 | display: none; 273 | } 274 | 275 | 276 | 277 | #background { 278 | position: absolute; 279 | top: 0; bottom: 0; 280 | width: 350px; 281 | background: #fff; 282 | border-right: 1px solid #e5e5ee; 283 | z-index: -1; 284 | } 285 | 286 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 287 | padding-left: 40px; 288 | } 289 | 290 | ul.sections > li { 291 | white-space: nowrap; 292 | } 293 | 294 | ul.sections > li > div { 295 | display: inline-block; 296 | } 297 | 298 | ul.sections > li > div.annotation { 299 | max-width: 350px; 300 | min-width: 350px; 301 | min-height: 5px; 302 | padding: 13px; 303 | overflow-x: hidden; 304 | white-space: normal; 305 | vertical-align: top; 306 | text-align: left; 307 | } 308 | ul.sections > li > div.annotation pre { 309 | margin: 15px 0 15px; 310 | padding-left: 15px; 311 | } 312 | 313 | ul.sections > li > div.content { 314 | padding: 13px; 315 | vertical-align: top; 316 | border: none; 317 | -webkit-box-shadow: none; 318 | box-shadow: none; 319 | } 320 | 321 | .pilwrap { 322 | position: relative; 323 | display: inline; 324 | } 325 | 326 | .pilcrow { 327 | font: 12px Arial; 328 | text-decoration: none; 329 | color: #454545; 330 | position: absolute; 331 | top: 3px; left: -20px; 332 | padding: 1px 2px; 333 | opacity: 0; 334 | -webkit-transition: opacity 0.2s linear; 335 | } 336 | .for-h1 .pilcrow { 337 | top: 47px; 338 | } 339 | .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { 340 | top: 35px; 341 | } 342 | 343 | ul.sections > li > div.annotation:hover .pilcrow { 344 | opacity: 1; 345 | } 346 | } 347 | 348 | /*---------------------- (> 1025px) ---------------------*/ 349 | @media only screen and (min-width: 1025px) { 350 | 351 | body { 352 | font-size: 16px; 353 | line-height: 24px; 354 | } 355 | 356 | #background { 357 | width: 525px; 358 | } 359 | ul.sections > li > div.annotation { 360 | max-width: 525px; 361 | min-width: 525px; 362 | padding: 10px 25px 1px 50px; 363 | } 364 | ul.sections > li > div.content { 365 | padding: 9px 15px 16px 25px; 366 | } 367 | } 368 | 369 | /*---------------------- Syntax Highlighting -----------------------------*/ 370 | 371 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 372 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 373 | /* 374 | 375 | github.com style (c) Vasily Polovnyov 376 | 377 | */ 378 | 379 | pre code { 380 | display: block; padding: 0.5em; 381 | color: #000; 382 | background: #f8f8ff 383 | } 384 | 385 | pre .hljs-comment, 386 | pre .hljs-template_comment, 387 | pre .hljs-diff .hljs-header, 388 | pre .hljs-javadoc { 389 | color: #408080; 390 | font-style: italic 391 | } 392 | 393 | pre .hljs-keyword, 394 | pre .hljs-assignment, 395 | pre .hljs-literal, 396 | pre .hljs-css .hljs-rule .hljs-keyword, 397 | pre .hljs-winutils, 398 | pre .hljs-javascript .hljs-title, 399 | pre .hljs-lisp .hljs-title, 400 | pre .hljs-subst { 401 | color: #954121; 402 | /*font-weight: bold*/ 403 | } 404 | 405 | pre .hljs-number, 406 | pre .hljs-hexcolor { 407 | color: #40a070 408 | } 409 | 410 | pre .hljs-string, 411 | pre .hljs-tag .hljs-value, 412 | pre .hljs-phpdoc, 413 | pre .hljs-tex .hljs-formula { 414 | color: #219161; 415 | } 416 | 417 | pre .hljs-title, 418 | pre .hljs-id { 419 | color: #19469D; 420 | } 421 | pre .hljs-params { 422 | color: #00F; 423 | } 424 | 425 | pre .hljs-javascript .hljs-title, 426 | pre .hljs-lisp .hljs-title, 427 | pre .hljs-subst { 428 | font-weight: normal 429 | } 430 | 431 | pre .hljs-class .hljs-title, 432 | pre .hljs-haskell .hljs-label, 433 | pre .hljs-tex .hljs-command { 434 | color: #458; 435 | font-weight: bold 436 | } 437 | 438 | pre .hljs-tag, 439 | pre .hljs-tag .hljs-title, 440 | pre .hljs-rules .hljs-property, 441 | pre .hljs-django .hljs-tag .hljs-keyword { 442 | color: #000080; 443 | font-weight: normal 444 | } 445 | 446 | pre .hljs-attribute, 447 | pre .hljs-variable, 448 | pre .hljs-instancevar, 449 | pre .hljs-lisp .hljs-body { 450 | color: #008080 451 | } 452 | 453 | pre .hljs-regexp { 454 | color: #B68 455 | } 456 | 457 | pre .hljs-class { 458 | color: #458; 459 | font-weight: bold 460 | } 461 | 462 | pre .hljs-symbol, 463 | pre .hljs-ruby .hljs-symbol .hljs-string, 464 | pre .hljs-ruby .hljs-symbol .hljs-keyword, 465 | pre .hljs-ruby .hljs-symbol .hljs-keymethods, 466 | pre .hljs-lisp .hljs-keyword, 467 | pre .hljs-tex .hljs-special, 468 | pre .hljs-input_number { 469 | color: #990073 470 | } 471 | 472 | pre .hljs-builtin, 473 | pre .hljs-constructor, 474 | pre .hljs-built_in, 475 | pre .hljs-lisp .hljs-title { 476 | color: #0086b3 477 | } 478 | 479 | pre .hljs-preprocessor, 480 | pre .hljs-pi, 481 | pre .hljs-doctype, 482 | pre .hljs-shebang, 483 | pre .hljs-cdata { 484 | color: #999; 485 | font-weight: bold 486 | } 487 | 488 | pre .hljs-deletion { 489 | background: #fdd 490 | } 491 | 492 | pre .hljs-addition { 493 | background: #dfd 494 | } 495 | 496 | pre .hljs-diff .hljs-change { 497 | background: #0086b3 498 | } 499 | 500 | pre .hljs-chunk { 501 | color: #aaa 502 | } 503 | 504 | pre .hljs-tex .hljs-formula { 505 | opacity: 0.5; 506 | } 507 | -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steves/node-jira/6429788b7e1621cf35cd13839d06e210d7d8be02/docs/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steves/node-jira/6429788b7e1621cf35cd13839d06e210d7d8be02/docs/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steves/node-jira/6429788b7e1621cf35cd13839d06e210d7d8be02/docs/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steves/node-jira/6429788b7e1621cf35cd13839d06e210d7d8be02/docs/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steves/node-jira/6429788b7e1621cf35cd13839d06e210d7d8be02/docs/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steves/node-jira/6429788b7e1621cf35cd13839d06e210d7d8be02/docs/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steves/node-jira/6429788b7e1621cf35cd13839d06e210d7d8be02/docs/public/fonts/novecento-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steves/node-jira/6429788b7e1621cf35cd13839d06e210d7d8be02/docs/public/fonts/novecento-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/steves/node-jira/6429788b7e1621cf35cd13839d06e210d7d8be02/docs/public/fonts/novecento-bold.woff -------------------------------------------------------------------------------- /docs/public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects `inline-block` display not defined in IE 8/9. 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | /* 36 | * Prevents modern browsers from displaying `audio` without controls. 37 | * Remove excess height in iOS 5 devices. 38 | */ 39 | 40 | audio:not([controls]) { 41 | display: none; 42 | height: 0; 43 | } 44 | 45 | /* 46 | * Addresses styling for `hidden` attribute not present in IE 8/9. 47 | */ 48 | 49 | [hidden] { 50 | display: none; 51 | } 52 | 53 | /* ========================================================================== 54 | Base 55 | ========================================================================== */ 56 | 57 | /* 58 | * 1. Sets default font family to sans-serif. 59 | * 2. Prevents iOS text size adjust after orientation change, without disabling 60 | * user zoom. 61 | */ 62 | 63 | html { 64 | font-family: sans-serif; /* 1 */ 65 | -webkit-text-size-adjust: 100%; /* 2 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | } 68 | 69 | /* 70 | * Removes default margin. 71 | */ 72 | 73 | body { 74 | margin: 0; 75 | } 76 | 77 | /* ========================================================================== 78 | Links 79 | ========================================================================== */ 80 | 81 | /* 82 | * Addresses `outline` inconsistency between Chrome and other browsers. 83 | */ 84 | 85 | a:focus { 86 | outline: thin dotted; 87 | } 88 | 89 | /* 90 | * Improves readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* ========================================================================== 99 | Typography 100 | ========================================================================== */ 101 | 102 | /* 103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, 104 | * Safari 5, and Chrome. 105 | */ 106 | 107 | h1 { 108 | font-size: 2em; 109 | } 110 | 111 | /* 112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: 1px dotted; 117 | } 118 | 119 | /* 120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bold; 126 | } 127 | 128 | /* 129 | * Addresses styling not present in Safari 5 and Chrome. 130 | */ 131 | 132 | dfn { 133 | font-style: italic; 134 | } 135 | 136 | /* 137 | * Addresses styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | 146 | /* 147 | * Corrects font family set oddly in Safari 5 and Chrome. 148 | */ 149 | 150 | code, 151 | kbd, 152 | pre, 153 | samp { 154 | font-family: monospace, serif; 155 | font-size: 1em; 156 | } 157 | 158 | /* 159 | * Improves readability of pre-formatted text in all browsers. 160 | */ 161 | 162 | pre { 163 | white-space: pre; 164 | white-space: pre-wrap; 165 | word-wrap: break-word; 166 | } 167 | 168 | /* 169 | * Sets consistent quote types. 170 | */ 171 | 172 | q { 173 | quotes: "\201C" "\201D" "\2018" "\2019"; 174 | } 175 | 176 | /* 177 | * Addresses inconsistent and variable font size in all browsers. 178 | */ 179 | 180 | small { 181 | font-size: 80%; 182 | } 183 | 184 | /* 185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers. 186 | */ 187 | 188 | sub, 189 | sup { 190 | font-size: 75%; 191 | line-height: 0; 192 | position: relative; 193 | vertical-align: baseline; 194 | } 195 | 196 | sup { 197 | top: -0.5em; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | /* ========================================================================== 205 | Embedded content 206 | ========================================================================== */ 207 | 208 | /* 209 | * Removes border when inside `a` element in IE 8/9. 210 | */ 211 | 212 | img { 213 | border: 0; 214 | } 215 | 216 | /* 217 | * Corrects overflow displayed oddly in IE 9. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /* 229 | * Addresses margin not present in IE 8/9 and Safari 5. 230 | */ 231 | 232 | figure { 233 | margin: 0; 234 | } 235 | 236 | /* ========================================================================== 237 | Forms 238 | ========================================================================== */ 239 | 240 | /* 241 | * Define consistent border, margin, and padding. 242 | */ 243 | 244 | fieldset { 245 | border: 1px solid #c0c0c0; 246 | margin: 0 2px; 247 | padding: 0.35em 0.625em 0.75em; 248 | } 249 | 250 | /* 251 | * 1. Corrects color not being inherited in IE 8/9. 252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 253 | */ 254 | 255 | legend { 256 | border: 0; /* 1 */ 257 | padding: 0; /* 2 */ 258 | } 259 | 260 | /* 261 | * 1. Corrects font family not being inherited in all browsers. 262 | * 2. Corrects font size not being inherited in all browsers. 263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome 264 | */ 265 | 266 | button, 267 | input, 268 | select, 269 | textarea { 270 | font-family: inherit; /* 1 */ 271 | font-size: 100%; /* 2 */ 272 | margin: 0; /* 3 */ 273 | } 274 | 275 | /* 276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in 277 | * the UA stylesheet. 278 | */ 279 | 280 | button, 281 | input { 282 | line-height: normal; 283 | } 284 | 285 | /* 286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 287 | * and `video` controls. 288 | * 2. Corrects inability to style clickable `input` types in iOS. 289 | * 3. Improves usability and consistency of cursor style between image-type 290 | * `input` and others. 291 | */ 292 | 293 | button, 294 | html input[type="button"], /* 1 */ 295 | input[type="reset"], 296 | input[type="submit"] { 297 | -webkit-appearance: button; /* 2 */ 298 | cursor: pointer; /* 3 */ 299 | } 300 | 301 | /* 302 | * Re-set default cursor for disabled elements. 303 | */ 304 | 305 | button[disabled], 306 | input[disabled] { 307 | cursor: default; 308 | } 309 | 310 | /* 311 | * 1. Addresses box sizing set to `content-box` in IE 8/9. 312 | * 2. Removes excess padding in IE 8/9. 313 | */ 314 | 315 | input[type="checkbox"], 316 | input[type="radio"] { 317 | box-sizing: border-box; /* 1 */ 318 | padding: 0; /* 2 */ 319 | } 320 | 321 | /* 322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. 323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome 324 | * (include `-moz` to future-proof). 325 | */ 326 | 327 | input[type="search"] { 328 | -webkit-appearance: textfield; /* 1 */ 329 | -moz-box-sizing: content-box; 330 | -webkit-box-sizing: content-box; /* 2 */ 331 | box-sizing: content-box; 332 | } 333 | 334 | /* 335 | * Removes inner padding and search cancel button in Safari 5 and Chrome 336 | * on OS X. 337 | */ 338 | 339 | input[type="search"]::-webkit-search-cancel-button, 340 | input[type="search"]::-webkit-search-decoration { 341 | -webkit-appearance: none; 342 | } 343 | 344 | /* 345 | * Removes inner padding and border in Firefox 4+. 346 | */ 347 | 348 | button::-moz-focus-inner, 349 | input::-moz-focus-inner { 350 | border: 0; 351 | padding: 0; 352 | } 353 | 354 | /* 355 | * 1. Removes default vertical scrollbar in IE 8/9. 356 | * 2. Improves readability and alignment in all browsers. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; /* 1 */ 361 | vertical-align: top; /* 2 */ 362 | } 363 | 364 | /* ========================================================================== 365 | Tables 366 | ========================================================================== */ 367 | 368 | /* 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table { 373 | border-collapse: collapse; 374 | border-spacing: 0; 375 | } -------------------------------------------------------------------------------- /lib/jira.js: -------------------------------------------------------------------------------- 1 | // # JavaScript JIRA API for node.js # 2 | // 3 | // [![Build Status](https://travis-ci.org/steves/node-jira.png?branch=master)](https://travis-ci.org/steves/node-jira) 4 | // 5 | // A node.js module, which provides an object oriented wrapper for the JIRA REST API. 6 | // 7 | // This library is built to support version `2.0.alpha1` of the JIRA REST API. 8 | // This library is also tested with version `2` of the JIRA REST API 9 | // It has been noted that with Jira OnDemand, `2.0.alpha1` does not work, devs 10 | // should revert to `2`. If this changes, please notify us. 11 | // 12 | // JIRA REST API documentation can be found [here](http://docs.atlassian.com/jira/REST/latest/) 13 | // 14 | // ## Installation ## 15 | // 16 | // Install with the node package manager [npm](http://npmjs.org): 17 | // 18 | // $ npm install jira 19 | // 20 | // or 21 | // 22 | // Install via git clone: 23 | // 24 | // $ git clone git://github.com/steves/node-jira.git 25 | // $ cd node-jira 26 | // $ npm install 27 | // 28 | // ## Example ## 29 | // 30 | // Find the status of an issue. 31 | // 32 | // JiraApi = require('jira').JiraApi; 33 | // 34 | // var jira = new JiraApi('https', config.host, config.port, config.user, config.password, '2.0.alpha1'); 35 | // jira.findIssue(issueNumber, function(error, issue) { 36 | // console.log('Status: ' + issue.fields.status.name); 37 | // }); 38 | // 39 | // Currently there is no explicit login call necessary as each API call uses Basic Authentication to authenticate. 40 | // 41 | // ## Options ## 42 | // 43 | // JiraApi options: 44 | // * `protocol`: Typically 'http:' or 'https:' 45 | // * `host`: The hostname for your jira server 46 | // * `port`: The port your jira server is listening on (probably `80` or `443`) 47 | // * `user`: The username to log in with 48 | // * `password`: Keep it secret, keep it safe 49 | // * `Jira API Version`: Known to work with `2` and `2.0.alpha1` 50 | // * `verbose`: Log some info to the console, usually for debugging 51 | // * `strictSSL`: Set to false if you have self-signed certs or something non-trustworthy 52 | // * `oauth`: A dictionary of `consumer_key`, `consumer_secret`, `access_token` and `access_token_secret` to be used for OAuth authentication. 53 | // * `base`: Add base slug if your JIRA install is not at the root of the host 54 | // 55 | // ## Implemented APIs ## 56 | // 57 | // * Authentication 58 | // * HTTP 59 | // * OAuth 60 | // * Projects 61 | // * Pulling a project 62 | // * List all projects viewable to the user 63 | // * List Components 64 | // * List Fields 65 | // * List Priorities 66 | // * Versions 67 | // * Pulling versions 68 | // * Adding a new version 69 | // * Pulling unresolved issues count for a specific version 70 | // * Rapid Views 71 | // * Find based on project name 72 | // * Get the latest Green Hopper sprint 73 | // * Gets attached issues 74 | // * Issues 75 | // * Add a new issue 76 | // * Update an issue 77 | // * Transition an issue 78 | // * Pulling an issue 79 | // * Issue linking 80 | // * Add an issue to a sprint 81 | // * Get a users issues (open or all) 82 | // * List issue types 83 | // * Search using jql 84 | // * Set Max Results 85 | // * Set Start-At parameter for results 86 | // * Add a worklog 87 | // * Add new estimate for worklog 88 | // * Add a comment 89 | // * Transitions 90 | // * List 91 | // * Users 92 | // * Search 93 | // 94 | // ## TODO ## 95 | // 96 | // * Refactor currently implemented APIs to be more Object Oriented 97 | // * Refactor to make use of built-in node.js events and classes 98 | // 99 | // ## Changelog ## 100 | // 101 | // 102 | // * _0.9.0 Add OAuth Support and New Estimates on addWorklog (thanks to 103 | // [nagyv](https://github.com/nagyv))_ 104 | // * _0.8.2 Fix URL Format Issues (thanks to 105 | // [eduardolundgren](https://github.com/eduardolundgren))_ 106 | // * _0.8.1 Expanding the transitions options (thanks to 107 | // [eduardolundgren](https://github.com/eduardolundgren))_ 108 | // * _0.8.0 Ability to search users (thanks to 109 | // [eduardolundgren](https://github.com/eduardolundgren))_ 110 | // * _0.7.2 Allows HTTP Code 204 on issue update edit (thanks to 111 | // [eduardolundgren](https://github.com/eduardolundgren))_ 112 | // * _0.7.1 Check if body variable is undef (thanks to 113 | // [AlexCline](https://github.com/AlexCline))_ 114 | // * _0.7.0 Adds list priorities, list fields, and project components (thanks to 115 | // [eduardolundgren](https://github.com/eduardolundgren))_ 116 | // * _0.6.0 Comment API implemented (thanks to [StevenMcD](https://github.com/StevenMcD))_ 117 | // * _0.5.0 Last param is now for strict SSL checking, defaults to true_ 118 | // * _0.4.1 Now handing errors in the request callback (thanks [mrbrookman](https://github.com/mrbrookman))_ 119 | // * _0.4.0 Now auto-redirecting between http and https (for both GET and POST)_ 120 | // * _0.3.1 [Request](https://github.com/mikeal/request) is broken, setting max request package at 2.15.0_ 121 | // * _0.3.0 Now Gets Issues for a Rapidview/Sprint (thanks [donbonifacio](https://github.com/donbonifacio))_ 122 | // * _0.2.0 Now allowing startAt and MaxResults to be passed to searchJira, 123 | // switching to semantic versioning._ 124 | // * _0.1.0 Using Basic Auth instead of cookies, all calls unit tested, URI 125 | // creation refactored_ 126 | // * _0.0.6 Now linting, preparing to refactor_ 127 | // * _0.0.5 JQL search now takes a list of fields_ 128 | // * _0.0.4 Added jql search_ 129 | // * _0.0.3 Added APIs and Docco documentation_ 130 | // * _0.0.2 Initial version_ 131 | var url = require('url'), 132 | logger = console, 133 | OAuth = require("oauth"); 134 | 135 | 136 | var JiraApi = exports.JiraApi = function(protocol, host, port, username, password, apiVersion, verbose, strictSSL, oauth, base) { 137 | this.protocol = protocol; 138 | this.host = host; 139 | this.port = port; 140 | this.username = username; 141 | this.password = password; 142 | this.apiVersion = apiVersion; 143 | this.base = base; 144 | // Default strictSSL to true (previous behavior) but now allow it to be 145 | // modified 146 | if (strictSSL == null) { 147 | strictSSL = true; 148 | } 149 | this.strictSSL = strictSSL; 150 | // This is so we can fake during unit tests 151 | this.request = require('request'); 152 | if (verbose !== true) { logger = { log: function() {} }; } 153 | 154 | // This is the same almost every time, refactored to make changing it 155 | // later, easier 156 | this.makeUri = function(pathname, altBase, altApiVersion) { 157 | var basePath = 'rest/api/'; 158 | if (altBase != null) { 159 | basePath = altBase; 160 | } 161 | if (this.base) { 162 | basePath = this.base + '/' + basePath; 163 | } 164 | 165 | var apiVersion = this.apiVersion; 166 | if (altApiVersion != null) { 167 | apiVersion = altApiVersion; 168 | } 169 | 170 | var uri = url.format({ 171 | protocol: this.protocol, 172 | hostname: this.host, 173 | port: this.port, 174 | pathname: basePath + apiVersion + pathname 175 | }); 176 | return decodeURIComponent(uri); 177 | }; 178 | 179 | this.doRequest = function(options, callback) { 180 | if(oauth && oauth.consumer_key && oauth.consumer_secret) { 181 | options.oauth = { 182 | consumer_key: oauth.consumer_key, 183 | consumer_secret: oauth.consumer_secret, 184 | token: oauth.access_token, 185 | token_secret: oauth.access_token_secret 186 | }; 187 | } else if(this.username && this.password) { 188 | options.auth = { 189 | 'user': this.username, 190 | 'pass': this.password 191 | }; 192 | } 193 | this.request(options, callback); 194 | }; 195 | 196 | }; 197 | 198 | (function() { 199 | // ## Find an issue in jira ## 200 | // ### Takes ### 201 | // 202 | // * issueNumber: the issueNumber to find 203 | // * callback: for when it's done 204 | // 205 | // ### Returns ### 206 | // 207 | // * error: string of the error 208 | // * issue: an object of the issue 209 | // 210 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290709) 211 | this.findIssue = function(issueNumber, callback) { 212 | 213 | var options = { 214 | rejectUnauthorized: this.strictSSL, 215 | uri: this.makeUri('/issue/' + issueNumber), 216 | method: 'GET' 217 | }; 218 | 219 | this.doRequest(options, function(error, response, body) { 220 | 221 | if (error) { 222 | callback(error, null); 223 | return; 224 | } 225 | 226 | if (response.statusCode === 404) { 227 | callback('Invalid issue number.'); 228 | return; 229 | } 230 | 231 | if (response.statusCode !== 200) { 232 | callback(response.statusCode + ': Unable to connect to JIRA during findIssueStatus.'); 233 | return; 234 | } 235 | 236 | if (body === undefined) { 237 | callback('Response body was undefined.'); 238 | return; 239 | } 240 | 241 | callback(null, JSON.parse(body)); 242 | 243 | }); 244 | }; 245 | 246 | // ## Get the unresolved issue count ## 247 | // ### Takes ### 248 | // 249 | // * version: version of your product that you want issues against 250 | // * callback: function for when it's done 251 | // 252 | // ### Returns ### 253 | // * error: string with the error code 254 | // * count: count of unresolved issues for requested version 255 | // 256 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id288524) 257 | this.getUnresolvedIssueCount = function(version, callback) { 258 | var options = { 259 | rejectUnauthorized: this.strictSSL, 260 | uri: this.makeUri('/version/' + version + '/unresolvedIssueCount'), 261 | method: 'GET' 262 | }; 263 | 264 | this.doRequest(options, function(error, response, body) { 265 | 266 | if (error) { 267 | callback(error, null); 268 | return; 269 | } 270 | 271 | if (response.statusCode === 404) { 272 | callback('Invalid version.'); 273 | return; 274 | } 275 | 276 | if (response.statusCode !== 200) { 277 | callback(response.statusCode + ': Unable to connect to JIRA during findIssueStatus.'); 278 | return; 279 | } 280 | 281 | body = JSON.parse(body); 282 | callback(null, body.issuesUnresolvedCount); 283 | 284 | }); 285 | }; 286 | 287 | // ## Get the Project by project key ## 288 | // ### Takes ### 289 | // 290 | // * project: key for the project 291 | // * callback: for when it's done 292 | // 293 | // ### Returns ### 294 | // * error: string of the error 295 | // * project: the json object representing the entire project 296 | // 297 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id289232) 298 | this.getProject = function(project, callback) { 299 | 300 | var options = { 301 | rejectUnauthorized: this.strictSSL, 302 | uri: this.makeUri('/project/' + project), 303 | method: 'GET' 304 | }; 305 | 306 | this.doRequest(options, function(error, response, body) { 307 | 308 | if (error) { 309 | callback(error, null); 310 | return; 311 | } 312 | 313 | if (response.statusCode === 404) { 314 | callback('Invalid project.'); 315 | return; 316 | } 317 | 318 | if (response.statusCode !== 200) { 319 | callback(response.statusCode + ': Unable to connect to JIRA during getProject.'); 320 | return; 321 | } 322 | 323 | body = JSON.parse(body); 324 | callback(null, body); 325 | 326 | }); 327 | }; 328 | 329 | // ## Find the Rapid View for a specified project ## 330 | // ### Takes ### 331 | // 332 | // * projectName: name for the project 333 | // * callback: for when it's done 334 | // 335 | // ### Returns ### 336 | // * error: string of the error 337 | // * rapidView: rapid view matching the projectName 338 | 339 | /** 340 | * Finds the Rapid View that belongs to a specified project. 341 | * 342 | * @param projectName 343 | * @param callback 344 | */ 345 | this.findRapidView = function(projectName, callback) { 346 | 347 | var options = { 348 | rejectUnauthorized: this.strictSSL, 349 | uri: this.makeUri('/rapidviews/list', 'rest/greenhopper/'), 350 | method: 'GET', 351 | json: true 352 | }; 353 | 354 | this.doRequest(options, function(error, response) { 355 | 356 | if (error) { 357 | callback(error, null); 358 | return; 359 | } 360 | 361 | if (response.statusCode === 404) { 362 | callback('Invalid URL'); 363 | return; 364 | } 365 | 366 | if (response.statusCode !== 200) { 367 | callback(response.statusCode + ': Unable to connect to JIRA during rapidView search.'); 368 | return; 369 | } 370 | 371 | if (response.body !== null) { 372 | var rapidViews = response.body.views; 373 | for (var i = 0; i < rapidViews.length; i++) { 374 | if(rapidViews[i].name.toLowerCase() === projectName.toLowerCase()) { 375 | callback(null, rapidViews[i]); 376 | return; 377 | } 378 | } 379 | } 380 | 381 | }); 382 | }; 383 | 384 | // ## Get a list of Sprints belonging to a Rapid View ## 385 | // ### Takes ### 386 | // 387 | // * rapidViewId: the id for the rapid view 388 | // * callback: for when it's done 389 | // 390 | // ### Returns ### 391 | // 392 | // * error: string with the error 393 | // * sprints: the ?array? of sprints 394 | /** 395 | * Returns a list of sprints belonging to a Rapid View. 396 | * 397 | * @param rapidViewId 398 | * @param callback 399 | */ 400 | this.getLastSprintForRapidView = function(rapidViewId, callback) { 401 | 402 | var options = { 403 | rejectUnauthorized: this.strictSSL, 404 | uri: this.makeUri('/sprintquery/' + rapidViewId, 'rest/greenhopper/'), 405 | method: 'GET', 406 | json:true 407 | }; 408 | 409 | this.doRequest(options, function(error, response) { 410 | 411 | if (error) { 412 | callback(error, null); 413 | return; 414 | } 415 | 416 | if (response.statusCode === 404) { 417 | callback('Invalid URL'); 418 | return; 419 | } 420 | 421 | if (response.statusCode !== 200) { 422 | callback(response.statusCode + ': Unable to connect to JIRA during sprints search.'); 423 | return; 424 | } 425 | 426 | if (response.body !== null) { 427 | var sprints = response.body.sprints; 428 | callback(null, sprints.pop()); 429 | return; 430 | } 431 | 432 | }); 433 | }; 434 | 435 | // ## Get the issues for a rapidView / sprint## 436 | // ### Takes ### 437 | // 438 | // * rapidViewId: the id for the rapid view 439 | // * sprintId: the id for the sprint 440 | // * callback: for when it's done 441 | // 442 | // ### Returns ### 443 | // 444 | // * error: string with the error 445 | // * results: the object with the issues and additional sprint information 446 | /** 447 | * Returns sprint and issues information 448 | * 449 | * @param rapidViewId 450 | * @param sprintId 451 | * @param callback 452 | */ 453 | this.getSprintIssues = function getSprintIssues(rapidViewId, sprintId, callback) { 454 | 455 | var options = { 456 | rejectUnauthorized: this.strictSSL, 457 | uri: this.makeUri('/rapid/charts/sprintreport?rapidViewId=' + rapidViewId + '&sprintId=' + sprintId, 'rest/greenhopper/'), 458 | method: 'GET', 459 | json: true 460 | }; 461 | 462 | this.doRequest(options, function(error, response) { 463 | 464 | if (error) { 465 | callback(error, null); 466 | return; 467 | } 468 | 469 | if( response.statusCode === 404 ) { 470 | callback('Invalid URL'); 471 | return; 472 | } 473 | 474 | if( response.statusCode !== 200 ) { 475 | callback(response.statusCode + ': Unable to connect to JIRA during sprints search'); 476 | return; 477 | } 478 | 479 | if(response.body !== null) { 480 | callback(null, response.body); 481 | } else { 482 | callback('No body'); 483 | } 484 | 485 | }); 486 | 487 | }; 488 | 489 | // ## Add an issue to the project's current sprint ## 490 | // ### Takes ### 491 | // 492 | // * issueId: the id of the existing issue 493 | // * sprintId: the id of the sprint to add it to 494 | // * callback: for when it's done 495 | // 496 | // ### Returns ### 497 | // 498 | // * error: string of the error 499 | // 500 | // 501 | // **does this callback if there's success?** 502 | /** 503 | * Adds a given issue to a project's current sprint 504 | * 505 | * @param issueId 506 | * @param sprintId 507 | * @param callback 508 | */ 509 | this.addIssueToSprint = function(issueId, sprintId, callback) { 510 | 511 | var options = { 512 | rejectUnauthorized: this.strictSSL, 513 | uri: this.makeUri('/sprint/' + sprintId + '/issues/add', 'rest/greenhopper/'), 514 | method: 'PUT', 515 | followAllRedirects: true, 516 | json:true, 517 | body: { 518 | issueKeys: [issueId] 519 | } 520 | }; 521 | 522 | logger.log(options.uri); 523 | 524 | this.doRequest(options, function(error, response) { 525 | 526 | if (error) { 527 | callback(error, null); 528 | return; 529 | } 530 | 531 | if (response.statusCode === 404) { 532 | callback('Invalid URL'); 533 | return; 534 | } 535 | 536 | if (response.statusCode !== 204) { 537 | callback(response.statusCode + ': Unable to connect to JIRA to add to sprint.'); 538 | return; 539 | } 540 | 541 | }); 542 | }; 543 | 544 | // ## Create an issue link between two issues ## 545 | // ### Takes ### 546 | // 547 | // * link: a link object 548 | // * callback: for when it's done 549 | // 550 | // ### Returns ### 551 | // * error: string if there was an issue, null if success 552 | // 553 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id296682) 554 | /** 555 | * Creates an issue link between two issues. Link should follow the below format: 556 | * 557 | * { 558 | * 'linkType': 'Duplicate', 559 | * 'fromIssueKey': 'HSP-1', 560 | * 'toIssueKey': 'MKY-1', 561 | * 'comment': { 562 | * 'body': 'Linked related issue!', 563 | * 'visibility': { 564 | * 'type': 'GROUP', 565 | * 'value': 'jira-users' 566 | * } 567 | * } 568 | * } 569 | * 570 | * @param link 571 | * @param callback 572 | */ 573 | this.issueLink = function(link, callback) { 574 | 575 | var options = { 576 | rejectUnauthorized: this.strictSSL, 577 | uri: this.makeUri('/issueLink'), 578 | method: 'POST', 579 | followAllRedirects: true, 580 | json: true, 581 | body: link 582 | }; 583 | 584 | this.doRequest(options, function(error, response) { 585 | 586 | if (error) { 587 | callback(error, null); 588 | return; 589 | } 590 | 591 | if (response.statusCode === 404) { 592 | callback('Invalid project.'); 593 | return; 594 | } 595 | 596 | if (response.statusCode !== 200) { 597 | callback(response.statusCode + ': Unable to connect to JIRA during issueLink.'); 598 | return; 599 | } 600 | 601 | callback(null); 602 | 603 | }); 604 | }; 605 | 606 | /** 607 | * Retrieves the remote links associated with the given issue. 608 | * 609 | * @param issueNumber - The internal id or key of the issue 610 | * @param callback 611 | */ 612 | this.getRemoteLinks = function getRemoteLinks(issueNumber, callback) { 613 | 614 | var options = { 615 | rejectUnauthorized: this.strictSSL, 616 | uri: this.makeUri('/issue/' + issueNumber + '/remotelink'), 617 | method: 'GET', 618 | json: true 619 | }; 620 | 621 | this.doRequest(options, function(error, response) { 622 | 623 | if (error) { 624 | callback(error, null); 625 | return; 626 | } 627 | 628 | if (response.statusCode === 404) { 629 | callback('Invalid issue number.'); 630 | return; 631 | } 632 | 633 | if (response.statusCode !== 200) { 634 | callback(response.statusCode + ': Unable to connect to JIRA during request.'); 635 | return; 636 | } 637 | 638 | callback(null, response.body); 639 | 640 | }); 641 | }; 642 | 643 | /** 644 | * Retrieves the remote links associated with the given issue. 645 | * 646 | * @param issueNumber - The internal id (not the issue key) of the issue 647 | * @param callback 648 | */ 649 | this.createRemoteLink = function createRemoteLink(issueNumber, remoteLink, callback) { 650 | 651 | var options = { 652 | rejectUnauthorized: this.strictSSL, 653 | uri: this.makeUri('/issue/' + issueNumber + '/remotelink'), 654 | method: 'POST', 655 | json: true, 656 | body: remoteLink 657 | }; 658 | 659 | this.doRequest(options, function(error, response) { 660 | 661 | if (error) { 662 | callback(error, null); 663 | return; 664 | } 665 | 666 | if (response.statusCode === 404) { 667 | callback('Cannot create remote link. Invalid issue.'); 668 | return; 669 | } 670 | 671 | if (response.statusCode === 400) { 672 | callback('Cannot create remote link. ' + response.body.errors.title); 673 | return; 674 | } 675 | 676 | if (response.statusCode !== 200) { 677 | callback(response.statusCode + ': Unable to connect to JIRA during request.'); 678 | return; 679 | } 680 | 681 | callback(null, response.body); 682 | 683 | }); 684 | }; 685 | 686 | 687 | // ## Get Versions for a project ## 688 | // ### Takes ### 689 | // * project: A project key 690 | // * callback: for when it's done 691 | // 692 | // ### Returns ### 693 | // * error: a string with the error 694 | // * versions: array of the versions for a product 695 | // 696 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id289653) 697 | this.getVersions = function(project, callback) { 698 | 699 | var options = { 700 | rejectUnauthorized: this.strictSSL, 701 | uri: this.makeUri('/project/' + project + '/versions'), 702 | method: 'GET' 703 | }; 704 | 705 | this.doRequest(options, function(error, response, body) { 706 | 707 | if (error) { 708 | callback(error, null); 709 | return; 710 | } 711 | 712 | if (response.statusCode === 404) { 713 | callback('Invalid project.'); 714 | return; 715 | } 716 | 717 | if (response.statusCode !== 200) { 718 | callback(response.statusCode + ': Unable to connect to JIRA during getVersions.'); 719 | return; 720 | } 721 | 722 | body = JSON.parse(body); 723 | callback(null, body); 724 | 725 | }); 726 | }; 727 | 728 | // ## Create a version ## 729 | // ### Takes ### 730 | // 731 | // * version: an object of the new version 732 | // * callback: for when it's done 733 | // 734 | // ### Returns ### 735 | // 736 | // * error: error text 737 | // * version: should be the same version you passed up 738 | // 739 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id288232) 740 | // 741 | /* { 742 | * "description": "An excellent version", 743 | * "name": "New Version 1", 744 | * "archived": false, 745 | * "released": true, 746 | * "releaseDate": "2010-07-05", 747 | * "userReleaseDate": "5/Jul/2010", 748 | * "project": "PXA" 749 | * } 750 | */ 751 | this.createVersion = function(version, callback) { 752 | 753 | var options = { 754 | rejectUnauthorized: this.strictSSL, 755 | uri: this.makeUri('/version'), 756 | method: 'POST', 757 | followAllRedirects: true, 758 | json: true, 759 | body: version 760 | }; 761 | this.doRequest(options, function(error, response, body) { 762 | 763 | if (error) { 764 | callback(error, null); 765 | return; 766 | } 767 | 768 | if (response.statusCode === 404) { 769 | callback('Version does not exist or the currently authenticated user does not have permission to view it'); 770 | return; 771 | } 772 | 773 | if (response.statusCode === 403) { 774 | callback('The currently authenticated user does not have permission to edit the version'); 775 | return; 776 | } 777 | 778 | if (response.statusCode !== 201) { 779 | callback(response.statusCode + ': Unable to connect to JIRA during createVersion.'); 780 | return; 781 | } 782 | 783 | callback(null, body); 784 | 785 | }); 786 | }; 787 | 788 | // ## Update a version ## 789 | // ### Takes ### 790 | // 791 | // * version: an object of the new version 792 | // * callback: for when it's done 793 | // 794 | // ### Returns ### 795 | // 796 | // * error: error text 797 | // * version: should be the same version you passed up 798 | // 799 | // [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#d2e510) 800 | // 801 | /* { 802 | * "id": The ID of the version being updated. Required. 803 | * "description": "An excellent version", 804 | * "name": "New Version 1", 805 | * "archived": false, 806 | * "released": true, 807 | * "releaseDate": "2010-07-05", 808 | * "userReleaseDate": "5/Jul/2010", 809 | * "project": "PXA" 810 | * } 811 | */ 812 | this.updateVersion = function(version, callback) { 813 | var options = { 814 | rejectUnauthorized: this.strictSSL, 815 | uri: this.makeUri('/version/'+version.id), 816 | method: 'PUT', 817 | followAllRedirects: true, 818 | json: true, 819 | body: version 820 | }; 821 | 822 | this.doRequest(options, function(error, response, body) { 823 | 824 | if (error) { 825 | callback(error, null); 826 | return; 827 | } 828 | 829 | if (response.statusCode === 404) { 830 | callback('Version does not exist or the currently authenticated user does not have permission to view it'); 831 | return; 832 | } 833 | 834 | if (response.statusCode === 403) { 835 | callback('The currently authenticated user does not have permission to edit the version'); 836 | return; 837 | } 838 | 839 | if (response.statusCode !== 200) { 840 | callback(response.statusCode + ': Unable to connect to JIRA during updateVersion.'); 841 | return; 842 | } 843 | 844 | callback(null, body); 845 | 846 | }); 847 | }; 848 | 849 | // ## Pass a search query to Jira ## 850 | // ### Takes ### 851 | // 852 | // * searchString: jira query string 853 | // * optional: object containing any of the following properties 854 | // * startAt: optional index number (default 0) 855 | // * maxResults: optional max results number (default 50) 856 | // * fields: optional array of desired fields, defaults when null: 857 | // * "summary" 858 | // * "status" 859 | // * "assignee" 860 | // * "description" 861 | // * callback: for when it's done 862 | // 863 | // ### Returns ### 864 | // 865 | // * error: string if there's an error 866 | // * issues: array of issues for the user 867 | // 868 | // [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#d2e4424) 869 | this.searchJira = function(searchString, optional, callback) { 870 | // backwards compatibility 871 | optional = optional || {}; 872 | if (Array.isArray(optional)) { 873 | optional = { fields: optional }; 874 | } 875 | 876 | var options = { 877 | rejectUnauthorized: this.strictSSL, 878 | uri: this.makeUri('/search'), 879 | method: 'POST', 880 | json: true, 881 | followAllRedirects: true, 882 | body: { 883 | jql: searchString, 884 | startAt: optional.startAt || 0, 885 | maxResults: optional.maxResults || 50, 886 | fields: optional.fields || ["summary", "status", "assignee", "description"], 887 | expand: optional.expand || ['schema', 'names'] 888 | } 889 | }; 890 | 891 | this.doRequest(options, function(error, response, body) { 892 | 893 | if (error) { 894 | callback(error, null); 895 | return; 896 | } 897 | 898 | if (response.statusCode === 400) { 899 | callback('Problem with the JQL query'); 900 | return; 901 | } 902 | 903 | if (response.statusCode !== 200) { 904 | callback(response.statusCode + ': Unable to connect to JIRA during search.'); 905 | return; 906 | } 907 | 908 | callback(null, body); 909 | 910 | }); 911 | }; 912 | 913 | // ## Search user on Jira ## 914 | // ### Takes ### 915 | // 916 | // username: A query string used to search username, name or e-mail address 917 | // startAt: The index of the first user to return (0-based) 918 | // maxResults: The maximum number of users to return (defaults to 50). 919 | // includeActive: If true, then active users are included in the results (default true) 920 | // includeInactive: If true, then inactive users are included in the results (default false) 921 | // * callback: for when it's done 922 | // 923 | // ### Returns ### 924 | // 925 | // * error: string if there's an error 926 | // * users: array of users for the user 927 | // 928 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#d2e3756) 929 | // 930 | this.searchUsers = function(username, startAt, maxResults, includeActive, includeInactive, callback) { 931 | startAt = (startAt !== undefined) ? startAt : 0; 932 | maxResults = (maxResults !== undefined) ? maxResults : 50; 933 | includeActive = (includeActive !== undefined) ? includeActive : true; 934 | includeInactive = (includeInactive !== undefined) ? includeInactive : false; 935 | 936 | var options = { 937 | rejectUnauthorized: this.strictSSL, 938 | uri: this.makeUri( 939 | '/user/search?username=' + username + 940 | '&startAt=' + startAt + 941 | '&maxResults=' + maxResults + 942 | '&includeActive=' + includeActive + 943 | '&includeInactive=' + includeInactive), 944 | method: 'GET', 945 | json: true, 946 | followAllRedirects: true 947 | }; 948 | 949 | this.doRequest(options, function(error, response, body) { 950 | 951 | if (error) { 952 | callback(error, null); 953 | return; 954 | } 955 | 956 | if (response.statusCode === 400) { 957 | callback('Unable to search'); 958 | return; 959 | } 960 | 961 | if (response.statusCode !== 200) { 962 | callback(response.statusCode + ': Unable to connect to JIRA during search.'); 963 | return; 964 | } 965 | 966 | callback(null, body); 967 | 968 | }); 969 | }; 970 | 971 | // ## Get all users in group on Jira ## 972 | // ### Takes ### 973 | // 974 | // groupName: A query string used to search users in group 975 | // startAt: The index of the first user to return (0-based) 976 | // maxResults: The maximum number of users to return (defaults to 50). 977 | // 978 | // ### Returns ### 979 | // 980 | // * error: string if there's an error 981 | // * users: array of users for the user 982 | 983 | this.getUsersInGroup = function(groupName, startAt, maxResults, callback) { 984 | startAt = (startAt !== undefined) ? startAt : 0; 985 | maxResults = (maxResults !== undefined) ? maxResults : 50; 986 | 987 | var options = { 988 | rejectUnauthorized: this.strictSSL, 989 | uri: this.makeUri( 990 | '/group?groupname=' + groupName + 991 | '&expand=users[' + startAt + ':' + maxResults + ']'), 992 | method: 'GET', 993 | json: true, 994 | followAllRedirects: true 995 | }; 996 | 997 | this.doRequest(options, function(error, response, body) { 998 | 999 | if (error) { 1000 | callback(error, null); 1001 | return; 1002 | } 1003 | 1004 | if (response.statusCode === 400) { 1005 | callback('Unable to search'); 1006 | return; 1007 | } 1008 | 1009 | if (response.statusCode !== 200) { 1010 | callback(response.statusCode + ': Unable to connect to JIRA during search.'); 1011 | return; 1012 | } 1013 | 1014 | callback(null, body); 1015 | 1016 | }); 1017 | }; 1018 | 1019 | // ## Get issues related to a user ## 1020 | // ### Takes ### 1021 | // 1022 | // * user: username of user to search for 1023 | // * open: `boolean` determines if only open issues should be returned 1024 | // * callback: for when it's done 1025 | // 1026 | // ### Returns ### 1027 | // 1028 | // * error: string if there's an error 1029 | // * issues: array of issues for the user 1030 | // 1031 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id296043) 1032 | // 1033 | this.getUsersIssues = function(username, open, callback) { 1034 | if (username.indexOf("@") > -1) { 1035 | username = username.replace("@", '\\u0040'); 1036 | } 1037 | var jql = "assignee = " + username; 1038 | var openText = ' AND status in (Open, "In Progress", Reopened)'; 1039 | if (open) { jql += openText; } 1040 | this.searchJira(jql, {}, callback); 1041 | }; 1042 | 1043 | // ## Add issue to Jira ## 1044 | // ### Takes ### 1045 | // 1046 | // * issue: Properly Formatted Issue 1047 | // * callback: for when it's done 1048 | // 1049 | // ### Returns ### 1050 | // * error object (check out the Jira Doc) 1051 | // * success object 1052 | // 1053 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290028) 1054 | this.addNewIssue = function(issue, callback) { 1055 | var options = { 1056 | rejectUnauthorized: this.strictSSL, 1057 | uri: this.makeUri('/issue'), 1058 | method: 'POST', 1059 | followAllRedirects: true, 1060 | json: true, 1061 | body: issue 1062 | }; 1063 | 1064 | this.doRequest(options, function(error, response, body) { 1065 | 1066 | if (error) { 1067 | callback(error, null); 1068 | return; 1069 | } 1070 | 1071 | if (response.statusCode === 400) { 1072 | callback(body); 1073 | return; 1074 | } 1075 | 1076 | if ((response.statusCode !== 200) && (response.statusCode !== 201)) { 1077 | callback(response.statusCode + ': Unable to connect to JIRA during search.'); 1078 | return; 1079 | } 1080 | 1081 | callback(null, body); 1082 | 1083 | }); 1084 | }; 1085 | 1086 | // ## Add a user as a watcher on an issue ## 1087 | // ### Takes ### 1088 | // 1089 | // * issueKey: the key of the existing issue 1090 | // * username: the jira username to add as a watcher to the issue 1091 | // * callback: for when it's done 1092 | // 1093 | // ### Returns ### 1094 | // 1095 | // * error: string of the error 1096 | // 1097 | // 1098 | // Empty callback on success 1099 | /** 1100 | * Adds a given user as a watcher to the given issue 1101 | * 1102 | * @param issueKey 1103 | * @param username 1104 | * @param callback 1105 | */ 1106 | this.addWatcher = function (issueKey, username, callback) { 1107 | 1108 | var options = { 1109 | rejectUnauthorized: this.strictSSL, 1110 | uri: this.makeUri('/issue/' + issueKey + '/watchers'), 1111 | method: 'POST', 1112 | followAllRedirects: true, 1113 | json: true, 1114 | body: JSON.stringify(username) 1115 | }; 1116 | 1117 | this.doRequest(options, function (error, response) { 1118 | if (error) { 1119 | return callback(error, null); 1120 | } 1121 | 1122 | if (response.statusCode === 404) { 1123 | return callback('Invalid URL'); 1124 | } 1125 | 1126 | if (response.statusCode !== 204) { 1127 | return callback(response.statusCode + ': Unable to connect to JIRA to add user as watcher.'); 1128 | } 1129 | callback(); 1130 | }); 1131 | }; 1132 | 1133 | // ## Delete issue to Jira ## 1134 | // ### Takes ### 1135 | // 1136 | // * issueId: the Id of the issue to delete 1137 | // * callback: for when it's done 1138 | // 1139 | // ### Returns ### 1140 | // * error string 1141 | // * success object 1142 | // 1143 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290791) 1144 | this.deleteIssue = function(issueNum, callback) { 1145 | var options = { 1146 | rejectUnauthorized: this.strictSSL, 1147 | uri: this.makeUri('/issue/' + issueNum), 1148 | method: 'DELETE', 1149 | followAllRedirects: true, 1150 | json: true 1151 | }; 1152 | 1153 | this.doRequest(options, function(error, response) { 1154 | 1155 | if (error) { 1156 | callback(error, null); 1157 | return; 1158 | } 1159 | 1160 | if (response.statusCode === 204) { 1161 | callback(null, "Success"); 1162 | return; 1163 | } 1164 | 1165 | callback(response.statusCode + ': Error while deleting'); 1166 | 1167 | }); 1168 | }; 1169 | 1170 | // ## Update issue in Jira ## 1171 | // ### Takes ### 1172 | // 1173 | // * issueId: the Id of the issue to delete 1174 | // * issueUpdate: update Object 1175 | // * callback: for when it's done 1176 | // 1177 | // ### Returns ### 1178 | // * error string 1179 | // * success string 1180 | // 1181 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290878) 1182 | this.updateIssue = function(issueNum, issueUpdate, callback) { 1183 | var options = { 1184 | rejectUnauthorized: this.strictSSL, 1185 | uri: this.makeUri('/issue/' + issueNum), 1186 | body: issueUpdate, 1187 | method: 'PUT', 1188 | followAllRedirects: true, 1189 | json: true 1190 | }; 1191 | 1192 | this.doRequest(options, function(error, response) { 1193 | 1194 | if (error) { 1195 | callback(error, null); 1196 | return; 1197 | } 1198 | 1199 | if (response.statusCode === 200 || response.statusCode === 204) { 1200 | callback(null, "Success"); 1201 | return; 1202 | } 1203 | 1204 | callback(response.statusCode + ': Error while updating'); 1205 | 1206 | }); 1207 | }; 1208 | 1209 | // ## List Components ## 1210 | // ### Takes ### 1211 | // 1212 | // * project: key for the project 1213 | // * callback: for when it's done 1214 | // 1215 | // ### Returns ### 1216 | // * error string 1217 | // * array of components 1218 | // 1219 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290489) 1220 | /* 1221 | * [{ 1222 | * "self": "http://localhostname:8090/jira/rest/api/2.0/component/1234", 1223 | * "id": "1234", 1224 | * "name": "name", 1225 | * "description": "Description.", 1226 | * "assigneeType": "PROJECT_DEFAULT", 1227 | * "assignee": { 1228 | * "self": "http://localhostname:8090/jira/rest/api/2.0/user?username=user@domain.com", 1229 | * "name": "user@domain.com", 1230 | * "displayName": "SE Support", 1231 | * "active": true 1232 | * }, 1233 | * "realAssigneeType": "PROJECT_DEFAULT", 1234 | * "realAssignee": { 1235 | * "self": "http://localhostname:8090/jira/rest/api/2.0/user?username=user@domain.com", 1236 | * "name": "user@domain.com", 1237 | * "displayName": "User name", 1238 | * "active": true 1239 | * }, 1240 | * "isAssigneeTypeValid": true 1241 | * }] 1242 | */ 1243 | this.listComponents = function(project, callback) { 1244 | var options = { 1245 | rejectUnauthorized: this.strictSSL, 1246 | uri: this.makeUri('/project/' + project + '/components'), 1247 | method: 'GET', 1248 | json: true 1249 | }; 1250 | 1251 | this.doRequest(options, function(error, response, body) { 1252 | 1253 | if (error) { 1254 | callback(error, null); 1255 | return; 1256 | } 1257 | 1258 | if (response.statusCode === 200) { 1259 | callback(null, body); 1260 | return; 1261 | } 1262 | if (response.statusCode === 404) { 1263 | callback("Project not found"); 1264 | return; 1265 | } 1266 | 1267 | callback(response.statusCode + ': Error while updating'); 1268 | 1269 | }); 1270 | }; 1271 | 1272 | // ## Add component to Jira ## 1273 | // ### Takes ### 1274 | // 1275 | // * issue: Properly Formatted Component 1276 | // * callback: for when it's done 1277 | // 1278 | // ### Returns ### 1279 | // * error object (check out the Jira Doc) 1280 | // * success object 1281 | // 1282 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290028) 1283 | this.addNewComponent = function (component, callback) { 1284 | var options = { 1285 | rejectUnauthorized: this.strictSSL, 1286 | uri: this.makeUri('/component'), 1287 | method: 'POST', 1288 | followAllRedirects: true, 1289 | json: true, 1290 | body: component 1291 | }; 1292 | 1293 | this.doRequest(options, function (error, response, body) { 1294 | 1295 | if (error) { 1296 | callback(error, null); 1297 | return; 1298 | } 1299 | 1300 | if (response.statusCode === 400) { 1301 | callback(body); 1302 | return; 1303 | } 1304 | 1305 | if ((response.statusCode !== 200) && (response.statusCode !== 201)) { 1306 | callback(response.statusCode + ': Unable to connect to JIRA during search.'); 1307 | return; 1308 | } 1309 | 1310 | callback(null, body); 1311 | 1312 | }); 1313 | }; 1314 | 1315 | // ## Delete component to Jira ## 1316 | // ### Takes ### 1317 | // 1318 | // * componentId: the Id of the component to delete 1319 | // * callback: for when it's done 1320 | // 1321 | // ### Returns ### 1322 | // * error string 1323 | // * success object 1324 | // 1325 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290791) 1326 | this.deleteComponent = function (componentNum, callback) { 1327 | var options = { 1328 | rejectUnauthorized: this.strictSSL, 1329 | uri: this.makeUri('/component/' + componentNum), 1330 | method: 'DELETE', 1331 | followAllRedirects: true, 1332 | json: true 1333 | }; 1334 | 1335 | this.doRequest(options, function (error, response) { 1336 | 1337 | if (error) { 1338 | callback(error, null); 1339 | return; 1340 | } 1341 | 1342 | if (response.statusCode === 204) { 1343 | callback(null, "Success"); 1344 | return; 1345 | } 1346 | 1347 | callback(response.statusCode + ': Error while deleting'); 1348 | 1349 | }); 1350 | }; 1351 | 1352 | // ## List listFields ## 1353 | // ### Takes ### 1354 | // 1355 | // * callback: for when it's done 1356 | // 1357 | // ### Returns ### 1358 | // * error string 1359 | // * array of priorities 1360 | // 1361 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290489) 1362 | /* 1363 | * [{ 1364 | * "id": "field", 1365 | * "name": "Field", 1366 | * "custom": false, 1367 | * "orderable": true, 1368 | * "navigable": true, 1369 | * "searchable": true, 1370 | * "schema": { 1371 | * "type": "string", 1372 | * "system": "field" 1373 | * } 1374 | * }] 1375 | */ 1376 | this.listFields = function(callback) { 1377 | var options = { 1378 | rejectUnauthorized: this.strictSSL, 1379 | uri: this.makeUri('/field'), 1380 | method: 'GET', 1381 | json: true 1382 | }; 1383 | 1384 | this.doRequest(options, function(error, response, body) { 1385 | 1386 | if (error) { 1387 | callback(error, null); 1388 | return; 1389 | } 1390 | 1391 | if (response.statusCode === 200) { 1392 | callback(null, body); 1393 | return; 1394 | } 1395 | if (response.statusCode === 404) { 1396 | callback("Not found"); 1397 | return; 1398 | } 1399 | 1400 | callback(response.statusCode + ': Error while updating'); 1401 | 1402 | }); 1403 | }; 1404 | 1405 | // ## List listPriorities ## 1406 | // ### Takes ### 1407 | // 1408 | // * callback: for when it's done 1409 | // 1410 | // ### Returns ### 1411 | // * error string 1412 | // * array of priorities 1413 | // 1414 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290489) 1415 | /* 1416 | * [{ 1417 | * "self": "http://localhostname:8090/jira/rest/api/2.0/priority/1", 1418 | * "statusColor": "#ff3300", 1419 | * "description": "Crashes, loss of data, severe memory leak.", 1420 | * "name": "Major", 1421 | * "id": "2" 1422 | * }] 1423 | */ 1424 | this.listPriorities = function(callback) { 1425 | var options = { 1426 | rejectUnauthorized: this.strictSSL, 1427 | uri: this.makeUri('/priority'), 1428 | method: 'GET', 1429 | json: true 1430 | }; 1431 | 1432 | this.doRequest(options, function(error, response, body) { 1433 | 1434 | if (error) { 1435 | callback(error, null); 1436 | return; 1437 | } 1438 | 1439 | if (response.statusCode === 200) { 1440 | callback(null, body); 1441 | return; 1442 | } 1443 | if (response.statusCode === 404) { 1444 | callback("Not found"); 1445 | return; 1446 | } 1447 | 1448 | callback(response.statusCode + ': Error while updating'); 1449 | 1450 | }); 1451 | }; 1452 | 1453 | // ## List Transitions ## 1454 | // ### Takes ### 1455 | // 1456 | // * issueId: get transitions available for the issue 1457 | // * callback: for when it's done 1458 | // 1459 | // ### Returns ### 1460 | // * error string 1461 | // * array of transitions 1462 | // 1463 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290489) 1464 | /* 1465 | * { 1466 | * "expand": "transitions", 1467 | * "transitions": [ 1468 | * { 1469 | * "id": "2", 1470 | * "name": "Close Issue", 1471 | * "to": { 1472 | * "self": "http://localhostname:8090/jira/rest/api/2.0/status/10000", 1473 | * "description": "The issue is currently being worked on.", 1474 | * "iconUrl": "http://localhostname:8090/jira/images/icons/progress.gif", 1475 | * "name": "In Progress", 1476 | * "id": "10000" 1477 | * }, 1478 | * "fields": { 1479 | * "summary": { 1480 | * "required": false, 1481 | * "schema": { 1482 | * "type": "array", 1483 | * "items": "option", 1484 | * "custom": "com.atlassian.jira.plugin.system.customfieldtypes:multiselect", 1485 | * "customId": 10001 1486 | * }, 1487 | * "name": "My Multi Select", 1488 | * "operations": [ 1489 | * "set", 1490 | * "add" 1491 | * ], 1492 | * "allowedValues": [ 1493 | * "red", 1494 | * "blue" 1495 | * ] 1496 | * } 1497 | * } 1498 | * } 1499 | * ]} 1500 | */ 1501 | this.listTransitions = function(issueId, callback) { 1502 | var options = { 1503 | rejectUnauthorized: this.strictSSL, 1504 | uri: this.makeUri('/issue/' + issueId + '/transitions?expand=transitions.fields'), 1505 | method: 'GET', 1506 | json: true 1507 | }; 1508 | 1509 | this.doRequest(options, function(error, response, body) { 1510 | 1511 | if (error) { 1512 | callback(error, null); 1513 | return; 1514 | } 1515 | 1516 | if (response.statusCode === 200) { 1517 | callback(null, body); 1518 | return; 1519 | } 1520 | if (response.statusCode === 404) { 1521 | callback("Issue not found"); 1522 | return; 1523 | } 1524 | 1525 | callback(response.statusCode + ': Error while updating'); 1526 | 1527 | }); 1528 | }; 1529 | 1530 | // ## Transition issue in Jira ## 1531 | // ### Takes ### 1532 | // 1533 | // * issueId: the Id of the issue to delete 1534 | // * issueTransition: transition Object 1535 | // * callback: for when it's done 1536 | // 1537 | // ### Returns ### 1538 | // * error string 1539 | // * success string 1540 | // 1541 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id290489) 1542 | this.transitionIssue = function(issueNum, issueTransition, callback) { 1543 | var options = { 1544 | rejectUnauthorized: this.strictSSL, 1545 | uri: this.makeUri('/issue/' + issueNum + '/transitions'), 1546 | body: issueTransition, 1547 | method: 'POST', 1548 | followAllRedirects: true, 1549 | json: true 1550 | }; 1551 | 1552 | this.doRequest(options, function(error, response) { 1553 | 1554 | if (error) { 1555 | callback(error, null); 1556 | return; 1557 | } 1558 | 1559 | if (response.statusCode === 204) { 1560 | callback(null, "Success"); 1561 | return; 1562 | } 1563 | 1564 | callback(response.statusCode + ': Error while updating'); 1565 | 1566 | }); 1567 | }; 1568 | 1569 | // ## List all Viewable Projects ## 1570 | // ### Takes ### 1571 | // 1572 | // * callback: for when it's done 1573 | // 1574 | // ### Returns ### 1575 | // * error string 1576 | // * array of projects 1577 | // 1578 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id289193) 1579 | /* 1580 | * Result items are in the format: 1581 | * { 1582 | * "self": "http://www.example.com/jira/rest/api/2/project/ABC", 1583 | * "id": "10001", 1584 | * "key": "ABC", 1585 | * "name": "Alphabetical", 1586 | * "avatarUrls": { 1587 | * "16x16": "http://www.example.com/jira/secure/projectavatar?size=small&pid=10001", 1588 | * "48x48": "http://www.example.com/jira/secure/projectavatar?size=large&pid=10001" 1589 | * } 1590 | * } 1591 | */ 1592 | this.listProjects = function(callback) { 1593 | var options = { 1594 | rejectUnauthorized: this.strictSSL, 1595 | uri: this.makeUri('/project'), 1596 | method: 'GET', 1597 | json: true 1598 | }; 1599 | 1600 | this.doRequest(options, function(error, response, body) { 1601 | 1602 | if (error) { 1603 | callback(error, null); 1604 | return; 1605 | } 1606 | 1607 | if (response.statusCode === 200) { 1608 | callback(null, body); 1609 | return; 1610 | } 1611 | if (response.statusCode === 500) { 1612 | callback(response.statusCode + ': Error while retrieving list.'); 1613 | return; 1614 | } 1615 | 1616 | callback(response.statusCode + ': Error while updating'); 1617 | 1618 | }); 1619 | }; 1620 | 1621 | // ## Add a comment to an issue ## 1622 | // ### Takes ### 1623 | // * issueId: Issue to add a comment to 1624 | // * comment: string containing comment 1625 | // * callback: for when it's done 1626 | // 1627 | // ### Returns ### 1628 | // * error string 1629 | // * success string 1630 | // 1631 | // [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#id108798) 1632 | this.addComment = function(issueId, comment, callback){ 1633 | var options = { 1634 | rejectUnauthorized: this.strictSSL, 1635 | uri: this.makeUri('/issue/' + issueId + '/comment'), 1636 | body: { 1637 | "body": comment 1638 | }, 1639 | method: 'POST', 1640 | followAllRedirects: true, 1641 | json: true 1642 | }; 1643 | 1644 | this.doRequest(options, function(error, response, body) { 1645 | if (error) { 1646 | callback(error, null); 1647 | return; 1648 | } 1649 | 1650 | if (response.statusCode === 201) { 1651 | callback(null, "Success"); 1652 | return; 1653 | } 1654 | 1655 | if (response.statusCode === 400) { 1656 | callback("Invalid Fields: " + JSON.stringify(body)); 1657 | return; 1658 | }; 1659 | 1660 | callback(response.statusCode + ': Error while adding comment'); 1661 | }); 1662 | }; 1663 | 1664 | // ## Add a worklog to a project ## 1665 | // ### Takes ### 1666 | // * issueId: Issue to add a worklog to 1667 | // * worklog: worklog object 1668 | // * callback: for when it's done 1669 | // 1670 | // ### Returns ### 1671 | // * error string 1672 | // * success string 1673 | // 1674 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id291617) 1675 | /* 1676 | * Worklog item is in the format: 1677 | * { 1678 | * "self": "http://www.example.com/jira/rest/api/2.0/issue/10010/worklog/10000", 1679 | * "author": { 1680 | * "self": "http://www.example.com/jira/rest/api/2.0/user?username=fred", 1681 | * "name": "fred", 1682 | * "displayName": "Fred F. User", 1683 | * "active": false 1684 | * }, 1685 | * "updateAuthor": { 1686 | * "self": "http://www.example.com/jira/rest/api/2.0/user?username=fred", 1687 | * "name": "fred", 1688 | * "displayName": "Fred F. User", 1689 | * "active": false 1690 | * }, 1691 | * "comment": "I did some work here.", 1692 | * "visibility": { 1693 | * "type": "group", 1694 | * "value": "jira-developers" 1695 | * }, 1696 | * "started": "2012-11-22T04:19:46.736-0600", 1697 | * "timeSpent": "3h 20m", 1698 | * "timeSpentSeconds": 12000, 1699 | * "id": "100028" 1700 | * } 1701 | */ 1702 | this.addWorklog = function(issueId, worklog, newEstimate, callback) { 1703 | if(typeof callback == 'undefined') { 1704 | callback = newEstimate; 1705 | newEstimate = false; 1706 | } 1707 | var options = { 1708 | rejectUnauthorized: this.strictSSL, 1709 | uri: this.makeUri('/issue/' + issueId + '/worklog' + (newEstimate ? "?adjustEstimate=new&newEstimate=" + newEstimate : "")), 1710 | body: worklog, 1711 | method: 'POST', 1712 | followAllRedirects: true, 1713 | json: true 1714 | }; 1715 | 1716 | this.doRequest(options, function(error, response, body) { 1717 | 1718 | if (error) { 1719 | callback(error, null); 1720 | return; 1721 | } 1722 | 1723 | if (response.statusCode === 201) { 1724 | callback(null, "Success"); 1725 | return; 1726 | } 1727 | if (response.statusCode === 400) { 1728 | callback("Invalid Fields: " + JSON.stringify(body)); 1729 | return; 1730 | } 1731 | if (response.statusCode === 403) { 1732 | callback("Insufficient Permissions"); 1733 | return; 1734 | } 1735 | 1736 | callback(response.statusCode + ': Error while updating'); 1737 | 1738 | }); 1739 | }; 1740 | 1741 | // ## Delete worklog from issue ## 1742 | // ### Takes ### 1743 | // 1744 | // * issueId: the Id of the issue to delete 1745 | // * worklogId: the Id of the worklog in issue to delete 1746 | // * callback: for when it's done 1747 | // 1748 | // ### Returns ### 1749 | // * error string 1750 | // * success object 1751 | // 1752 | // [Jira Doc](https://docs.atlassian.com/jira/REST/latest/#d2e1673) 1753 | 1754 | this.deleteWorklog = function(issueId, worklogId, callback) { 1755 | 1756 | var options = { 1757 | rejectUnauthorized: this.strictSSL, 1758 | uri: this.makeUri('/issue/' + issueId + '/worklog/' + worklogId), 1759 | method: 'DELETE', 1760 | followAllRedirects: true, 1761 | json: true 1762 | }; 1763 | 1764 | this.doRequest(options, function(error, response) { 1765 | 1766 | if (error) { 1767 | callback(error, null); 1768 | return; 1769 | } 1770 | 1771 | if (response.statusCode === 204) { 1772 | callback(null, "Success"); 1773 | return; 1774 | } 1775 | 1776 | callback(response.statusCode + ': Error while deleting'); 1777 | 1778 | }); 1779 | 1780 | }; 1781 | 1782 | // ## List all Issue Types ## 1783 | // ### Takes ### 1784 | // 1785 | // * callback: for when it's done 1786 | // 1787 | // ### Returns ### 1788 | // * error string 1789 | // * array of types 1790 | // 1791 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id295946) 1792 | /* 1793 | * Result items are in the format: 1794 | * { 1795 | * "self": "http://localhostname:8090/jira/rest/api/2.0/issueType/3", 1796 | * "id": "3", 1797 | * "description": "A task that needs to be done.", 1798 | * "iconUrl": "http://localhostname:8090/jira/images/icons/task.gif", 1799 | * "name": "Task", 1800 | * "subtask": false 1801 | * } 1802 | */ 1803 | this.listIssueTypes = function(callback) { 1804 | var options = { 1805 | rejectUnauthorized: this.strictSSL, 1806 | uri: this.makeUri('/issuetype'), 1807 | method: 'GET', 1808 | json: true 1809 | }; 1810 | 1811 | this.doRequest(options, function(error, response, body) { 1812 | 1813 | if (error) { 1814 | callback(error, null); 1815 | return; 1816 | } 1817 | 1818 | if (response.statusCode === 200) { 1819 | callback(null, body); 1820 | return; 1821 | } 1822 | 1823 | callback(response.statusCode + ': Error while retrieving issue types'); 1824 | 1825 | }); 1826 | }; 1827 | 1828 | // ## Register a webhook ## 1829 | // ### Takes ### 1830 | // 1831 | // * webhook: properly formatted webhook 1832 | // * callback: for when it's done 1833 | // 1834 | // ### Returns ### 1835 | // * error string 1836 | // * success object 1837 | // 1838 | // [Jira Doc](https://developer.atlassian.com/display/JIRADEV/JIRA+Webhooks+Overview) 1839 | /* 1840 | * Success object in the format: 1841 | * { 1842 | * name: 'my first webhook via rest', 1843 | * events: [], 1844 | * url: 'http://www.example.com/webhooks', 1845 | * filter: '', 1846 | * excludeIssueDetails: false, 1847 | * enabled: true, 1848 | * self: 'http://localhost:8090/rest/webhooks/1.0/webhook/5', 1849 | * lastUpdatedUser: 'user', 1850 | * lastUpdatedDisplayName: 'User Name', 1851 | * lastUpdated: 1383247225784 1852 | * } 1853 | */ 1854 | this.registerWebhook = function(webhook, callback) { 1855 | var options = { 1856 | rejectUnauthorized: this.strictSSL, 1857 | uri: this.makeUri('/webhook', 'rest/webhooks/', '1.0'), 1858 | method: 'POST', 1859 | json: true, 1860 | body: webhook 1861 | }; 1862 | 1863 | this.request(options, function(error, response, body) { 1864 | 1865 | if (error) { 1866 | callback(error, null); 1867 | return; 1868 | } 1869 | 1870 | if (response.statusCode === 201) { 1871 | callback(null, body); 1872 | return; 1873 | } 1874 | 1875 | callback(response.statusCode + ': Error while registering new webhook'); 1876 | 1877 | }); 1878 | }; 1879 | 1880 | // ## List all registered webhooks ## 1881 | // ### Takes ### 1882 | // 1883 | // * callback: for when it's done 1884 | // 1885 | // ### Returns ### 1886 | // * error string 1887 | // * array of webhook objects 1888 | // 1889 | // [Jira Doc](https://developer.atlassian.com/display/JIRADEV/JIRA+Webhooks+Overview) 1890 | /* 1891 | * Webhook object in the format: 1892 | * { 1893 | * name: 'my first webhook via rest', 1894 | * events: [], 1895 | * url: 'http://www.example.com/webhooks', 1896 | * filter: '', 1897 | * excludeIssueDetails: false, 1898 | * enabled: true, 1899 | * self: 'http://localhost:8090/rest/webhooks/1.0/webhook/5', 1900 | * lastUpdatedUser: 'user', 1901 | * lastUpdatedDisplayName: 'User Name', 1902 | * lastUpdated: 1383247225784 1903 | * } 1904 | */ 1905 | this.listWebhooks = function(callback) { 1906 | var options = { 1907 | rejectUnauthorized: this.strictSSL, 1908 | uri: this.makeUri('/webhook', 'rest/webhooks/', '1.0'), 1909 | method: 'GET', 1910 | json: true 1911 | }; 1912 | 1913 | this.request(options, function(error, response, body) { 1914 | 1915 | if (error) { 1916 | callback(error, null); 1917 | return; 1918 | } 1919 | 1920 | if (response.statusCode === 200) { 1921 | callback(null, body); 1922 | return; 1923 | } 1924 | 1925 | callback(response.statusCode + ': Error while listing webhooks'); 1926 | 1927 | }); 1928 | }; 1929 | 1930 | // ## Get a webhook by its ID ## 1931 | // ### Takes ### 1932 | // 1933 | // * webhookID: id of webhook to get 1934 | // * callback: for when it's done 1935 | // 1936 | // ### Returns ### 1937 | // * error string 1938 | // * webhook object 1939 | // 1940 | // [Jira Doc](https://developer.atlassian.com/display/JIRADEV/JIRA+Webhooks+Overview) 1941 | this.getWebhook = function(webhookID, callback) { 1942 | var options = { 1943 | rejectUnauthorized: this.strictSSL, 1944 | uri: this.makeUri('/webhook/' + webhookID, 'rest/webhooks/', '1.0'), 1945 | method: 'GET', 1946 | json: true 1947 | }; 1948 | 1949 | this.request(options, function(error, response, body) { 1950 | 1951 | if (error) { 1952 | callback(error, null); 1953 | return; 1954 | } 1955 | 1956 | if (response.statusCode === 200) { 1957 | callback(null, body); 1958 | return; 1959 | } 1960 | 1961 | callback(response.statusCode + ': Error while getting webhook'); 1962 | 1963 | }); 1964 | }; 1965 | 1966 | // ## Delete a registered webhook ## 1967 | // ### Takes ### 1968 | // 1969 | // * webhookID: id of the webhook to delete 1970 | // * callback: for when it's done 1971 | // 1972 | // ### Returns ### 1973 | // * error string 1974 | // * success string 1975 | // 1976 | // [Jira Doc](https://developer.atlassian.com/display/JIRADEV/JIRA+Webhooks+Overview) 1977 | this.deleteWebhook = function(webhookID, callback) { 1978 | var options = { 1979 | rejectUnauthorized: this.strictSSL, 1980 | uri: this.makeUri('/webhook/' + webhookID, 'rest/webhooks/', '1.0'), 1981 | method: 'DELETE', 1982 | json: true 1983 | }; 1984 | 1985 | this.request(options, function(error, response, body) { 1986 | 1987 | if (error) { 1988 | callback(error, null); 1989 | return; 1990 | } 1991 | 1992 | if (response.statusCode === 204) { 1993 | callback(null, "Success"); 1994 | return; 1995 | } 1996 | 1997 | callback(response.statusCode + ': Error while deleting webhook'); 1998 | 1999 | }); 2000 | }; 2001 | 2002 | // ## Describe the currently authenticated user ## 2003 | // ### Takes ### 2004 | // 2005 | // * callback: for when it's done 2006 | // 2007 | // ### Returns ### 2008 | // * error string 2009 | // * user object 2010 | // 2011 | // [Jira Doc](http://docs.atlassian.com/jira/REST/latest/#id2e865) 2012 | /* 2013 | * User object in the format: 2014 | * { 2015 | * self: 'http://localhost:8090/rest/api/latest/user?username=user', 2016 | * name: 'user', 2017 | * loginInfo: 2018 | * { 2019 | * failedLoginCount: 2, 2020 | * loginCount: 114, 2021 | * lastFailedLoginTime: '2013-10-29T13:33:26.702+0000', 2022 | * previousLoginTime: '2013-10-31T20:30:51.924+0000' 2023 | * } 2024 | * } 2025 | */ 2026 | this.getCurrentUser = function(callback) { 2027 | var options = { 2028 | rejectUnauthorized: this.strictSSL, 2029 | uri: this.makeUri('/session', 'rest/auth/', '1'), 2030 | method: 'GET', 2031 | json: true 2032 | }; 2033 | 2034 | this.doRequest(options, function(error, response, body) { 2035 | 2036 | if (error) { 2037 | callback(error, null); 2038 | return; 2039 | } 2040 | 2041 | if (response.statusCode === 200) { 2042 | callback(null, body); 2043 | return; 2044 | } 2045 | 2046 | callback(response.statusCode + ': Error while getting current user'); 2047 | 2048 | }); 2049 | }; 2050 | 2051 | // ## Retrieve the backlog of a certain Rapid View ## 2052 | // ### Takes ### 2053 | // * rapidViewId: rapid view id 2054 | // * callback: for when it's done 2055 | // 2056 | // ### Returns ### 2057 | // * error string 2058 | // * backlog object 2059 | /* 2060 | * Backlog item is in the format: 2061 | * { 2062 | * "sprintMarkersMigrated": true, 2063 | * "issues": [ 2064 | * { 2065 | * "id": 67890, 2066 | * "key": "KEY-1234", 2067 | * "summary": "Issue Summary", 2068 | * ... 2069 | * } 2070 | * ], 2071 | * "rankCustomFieldId": 12345, 2072 | * "sprints": [ 2073 | * { 2074 | * "id": 123, 2075 | * "name": "Sprint Name", 2076 | * "state": "FUTURE", 2077 | * ... 2078 | * } 2079 | * ], 2080 | * "supportsPages": true, 2081 | * "projects": [ 2082 | * { 2083 | * "id": 567, 2084 | * "key": "KEY", 2085 | * "name": "Project Name" 2086 | * } 2087 | * ], 2088 | * "epicData": { 2089 | * "epics": [ 2090 | * { 2091 | * "id": 9876, 2092 | * "key": "KEY-4554", 2093 | * "typeName": "Epic", 2094 | * ... 2095 | * } 2096 | * ], 2097 | * "canEditEpics": true, 2098 | * "supportsPages": true 2099 | * }, 2100 | * "canManageSprints": true, 2101 | * "maxIssuesExceeded": false, 2102 | * "queryResultLimit": 2147483647, 2103 | * "versionData": { 2104 | * "versionsPerProject": { 2105 | * "567": [ 2106 | * { 2107 | * "id": 8282, 2108 | * "name": "Version Name", 2109 | * ... 2110 | * } 2111 | * ] 2112 | * }, 2113 | * "canCreateVersion": true 2114 | * } 2115 | * } 2116 | */ 2117 | this.getBacklogForRapidView = function(rapidViewId, callback) { 2118 | var options = { 2119 | rejectUnauthorized: this.strictSSL, 2120 | uri: this.makeUri('/xboard/plan/backlog/data?rapidViewId=' + rapidViewId, 'rest/greenhopper/'), 2121 | method: 'GET', 2122 | json: true 2123 | }; 2124 | 2125 | this.doRequest(options, function(error, response) { 2126 | if (error) { 2127 | callback(error, null); 2128 | 2129 | return; 2130 | } 2131 | 2132 | if (response.statusCode === 200) { 2133 | callback(null, response.body); 2134 | 2135 | return; 2136 | } 2137 | 2138 | callback(response.statusCode + ': Error while retrieving backlog'); 2139 | }); 2140 | }; 2141 | 2142 | }).call(JiraApi.prototype); 2143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jira", 3 | "version": "0.9.2", 4 | "description": "Wrapper for the JIRA API", 5 | "author": "Steven Surowiec ", 6 | "contributors": [ 7 | "Chris Moultrie ", 8 | "Lucas Vogelsang " 9 | ], 10 | "homepage": "http://github.com/steves/node-jira", 11 | "repository": { 12 | "type": "git", 13 | "url": "http://github.com/steves/node-jira.git" 14 | }, 15 | "engine": { 16 | "node": ">=0.4.0" 17 | }, 18 | "main": "./lib/jira.js", 19 | "licenses": [ 20 | { 21 | "type": "The MIT License", 22 | "url": "http://www.opensource.org/licenses/mit-license.php" 23 | } 24 | ], 25 | "dependencies": { 26 | "request": "<2.16.0", 27 | "oauth": "^0.9.11" 28 | }, 29 | "scripts": { 30 | "test": "grunt test" 31 | }, 32 | "devDependencies": { 33 | "grunt": "^0.4.4", 34 | "grunt-bump": "0.0.13", 35 | "grunt-docco": "^0.3.3", 36 | "grunt-jasmine-node": "^0.2.1", 37 | "coffee-script": "^1.7.1", 38 | "grunt-jslint": "^1.1.8", 39 | "rewire": "^2.0.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spec/jira.spec.coffee: -------------------------------------------------------------------------------- 1 | url = require 'url' 2 | 3 | rewire = require 'rewire' 4 | nodeJira = rewire '../lib/jira' 5 | 6 | describe "Node Jira Tests", -> 7 | makeUrl = (path, altBase) -> 8 | base = 'rest/api/2/' 9 | base = 'rest/greenhopper/2/' if altBase? 10 | decodeURIComponent( 11 | url.format 12 | protocol: 'http:' 13 | hostname: 'localhost' 14 | port: 80 15 | pathname: "#{base}#{path}") 16 | 17 | 18 | beforeEach -> 19 | OAuth = nodeJira.__get__ "OAuth" 20 | OAuth.OAuth.prototype = jasmine.createSpyObj 'OAuth', ['getOAuthRequestToken', '_encodeData'] 21 | nodeJira.__set__ "OAuth", OAuth 22 | 23 | @jira = new nodeJira.JiraApi 'http', 'localhost', 80, 'test', 'test', 2 24 | spyOn @jira, 'request' 25 | @cb = jasmine.createSpy 'callback' 26 | 27 | it "Sets basic auth if oauth is not passed in", -> 28 | options = 29 | auth = 30 | user: 'test' 31 | pass: 'test' 32 | @jira.doRequest options, @cb 33 | expect(@jira.request) 34 | .toHaveBeenCalledWith(options, jasmine.any(Function)) 35 | 36 | it "Sets OAuth oauth for the requests if oauth is passed in", -> 37 | options = 38 | oauth = 39 | consumer_key: 'ck' 40 | consumer_secret: 'cs' 41 | access_token: 'ac' 42 | access_token_secret: 'acs' 43 | # oauth = new OAuth.OAuth(null, null, oauth.consumer_key, oauth.consumer_secret, null, null, "RSA-SHA1") 44 | @jira = new nodeJira.JiraApi 'http', 'localhost', 80, 'test', 'test', 2, false, false, options.oauth 45 | spyOn @jira, 'request' 46 | 47 | @jira.doRequest options, @cb 48 | expect(@jira.request) 49 | .toHaveBeenCalledWith(options, jasmine.any(Function)) 50 | 51 | it "Sets strictSSL to false when passed in", -> 52 | expected = false 53 | jira = new nodeJira.JiraApi 'http', 'localhost', 80, 'test', 'test', 2, false, expected 54 | expect(jira.strictSSL).toEqual(expected) 55 | 56 | it "Sets strictSSL to true when passed in", -> 57 | expected = true 58 | jira = new nodeJira.JiraApi 'http', 'localhost', 80, 'test', 'test', 2, false, expected 59 | expect(jira.strictSSL).toEqual(expected) 60 | 61 | it "Sets strictSSL to true when not passed in", -> 62 | expected = true 63 | expect(@jira.strictSSL).toEqual(expected) 64 | 65 | it "Finds an issue", -> 66 | options = 67 | rejectUnauthorized: true 68 | uri: makeUrl "issue/1" 69 | method: 'GET' 70 | auth: 71 | user: 'test' 72 | pass: 'test' 73 | @jira.findIssue 1, @cb 74 | expect(@jira.request) 75 | .toHaveBeenCalledWith(options, jasmine.any(Function)) 76 | 77 | # Invalid issue number (different than unable to find??) 78 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 79 | expect(@cb).toHaveBeenCalledWith 'Invalid issue number.' 80 | 81 | # Unable to find issue 82 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 83 | expect(@cb).toHaveBeenCalledWith( 84 | '401: Unable to connect to JIRA during findIssueStatus.') 85 | 86 | # Successful Request 87 | @jira.request.mostRecentCall.args[1] null, 88 | statusCode:200, '{"body":"none"}' 89 | expect(@cb).toHaveBeenCalledWith(null, body: 'none') 90 | 91 | it "Gets the unresolved issue count", -> 92 | options = 93 | rejectUnauthorized: true 94 | uri: makeUrl "version/1/unresolvedIssueCount" 95 | method: 'GET' 96 | auth: 97 | user: 'test' 98 | pass: 'test' 99 | 100 | @jira.getUnresolvedIssueCount 1, @cb 101 | expect(@jira.request) 102 | .toHaveBeenCalledWith options, jasmine.any(Function) 103 | 104 | # Invalid Version 105 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 106 | expect(@cb).toHaveBeenCalledWith 'Invalid version.' 107 | 108 | # Unable to connect 109 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 110 | expect(@cb).toHaveBeenCalledWith( 111 | '401: Unable to connect to JIRA during findIssueStatus.') 112 | 113 | # Successful Request 114 | @jira.request.mostRecentCall.args[1] null, 115 | statusCode:200, '{"issuesUnresolvedCount":1}' 116 | expect(@cb).toHaveBeenCalledWith null, 1 117 | 118 | it "Gets the project from a key", -> 119 | options = 120 | rejectUnauthorized: true 121 | uri: makeUrl "project/ABC" 122 | method: 'GET' 123 | auth: 124 | user: 'test' 125 | pass: 'test' 126 | 127 | @jira.getProject 'ABC', @cb 128 | expect(@jira.request) 129 | .toHaveBeenCalledWith options, jasmine.any(Function) 130 | 131 | # Invalid Version 132 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 133 | expect(@cb).toHaveBeenCalledWith 'Invalid project.' 134 | 135 | # Successful Request 136 | @jira.request.mostRecentCall.args[1] null, 137 | statusCode:200, '{"body":"none"}' 138 | expect(@cb).toHaveBeenCalledWith null, body:"none" 139 | 140 | it "Finds a Rapid View", -> 141 | options = 142 | rejectUnauthorized: true 143 | uri: makeUrl("rapidviews/list", true) 144 | method: 'GET' 145 | json: true 146 | auth: 147 | user: 'test' 148 | pass: 'test' 149 | 150 | @jira.findRapidView 'ABC', @cb 151 | expect(@jira.request) 152 | .toHaveBeenCalledWith options, jasmine.any(Function) 153 | 154 | # Invalid URL 155 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 156 | expect(@cb).toHaveBeenCalledWith 'Invalid URL' 157 | 158 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 159 | expect(@cb).toHaveBeenCalledWith( 160 | '401: Unable to connect to JIRA during rapidView search.') 161 | 162 | # Successful Request 163 | @jira.request.mostRecentCall.args[1] null, 164 | statusCode:200, 165 | body: 166 | views: [name: 'ABC'] 167 | 168 | expect(@cb).toHaveBeenCalledWith null, name: 'ABC' 169 | 170 | it "Gets the last sprint for a Rapid View", -> 171 | options = 172 | rejectUnauthorized: true 173 | uri: makeUrl("sprintquery/1", true) 174 | method: 'GET' 175 | json: true 176 | auth: 177 | user: 'test' 178 | pass: 'test' 179 | 180 | @jira.getLastSprintForRapidView 1, @cb 181 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 182 | 183 | # Invalid URL 184 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 185 | expect(@cb).toHaveBeenCalledWith 'Invalid URL' 186 | 187 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 188 | expect(@cb).toHaveBeenCalledWith( 189 | '401: Unable to connect to JIRA during sprints search.') 190 | 191 | # Successful Request 192 | @jira.request.mostRecentCall.args[1] null, 193 | statusCode:200, 194 | body: 195 | sprints: [name: 'ABC'] 196 | 197 | expect(@cb).toHaveBeenCalledWith null, name: 'ABC' 198 | 199 | it "Adds an issue to a sprint", -> 200 | options = 201 | rejectUnauthorized: true 202 | uri: makeUrl("sprint/1/issues/add", true) 203 | method: 'PUT' 204 | json: true 205 | followAllRedirects: true 206 | body: 207 | issueKeys: [2] 208 | auth: 209 | user: 'test' 210 | pass: 'test' 211 | 212 | @jira.addIssueToSprint 2, 1, @cb 213 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 214 | 215 | # Invalid URL 216 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 217 | expect(@cb).toHaveBeenCalledWith 'Invalid URL' 218 | 219 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 220 | expect(@cb).toHaveBeenCalledWith( 221 | '401: Unable to connect to JIRA to add to sprint.') 222 | 223 | it "Creates a Link Between two Issues", -> 224 | options = 225 | rejectUnauthorized: true 226 | uri: makeUrl "issueLink" 227 | method: 'POST' 228 | json: true 229 | body: 'test' 230 | followAllRedirects: true 231 | auth: 232 | user: 'test' 233 | pass: 'test' 234 | 235 | @jira.issueLink 'test', @cb 236 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 237 | 238 | # Invalid Project 239 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 240 | expect(@cb).toHaveBeenCalledWith 'Invalid project.' 241 | 242 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 243 | expect(@cb).toHaveBeenCalledWith( 244 | '401: Unable to connect to JIRA during issueLink.') 245 | 246 | # Successful Request 247 | @jira.request.mostRecentCall.args[1] null, statusCode:200 248 | expect(@cb).toHaveBeenCalledWith null 249 | 250 | it "Gets versions for a project", -> 251 | options = 252 | rejectUnauthorized: true 253 | uri: makeUrl "project/ABC/versions" 254 | method: 'GET' 255 | auth: 256 | user: 'test' 257 | pass: 'test' 258 | 259 | @jira.getVersions 'ABC', @cb 260 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 261 | 262 | # Invalid Project 263 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 264 | expect(@cb).toHaveBeenCalledWith 'Invalid project.' 265 | 266 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 267 | expect(@cb).toHaveBeenCalledWith( 268 | '401: Unable to connect to JIRA during getVersions.') 269 | 270 | # Successful Request 271 | @jira.request.mostRecentCall.args[1] null, 272 | statusCode:200, '{"body":"none"}' 273 | expect(@cb).toHaveBeenCalledWith null, body:'none' 274 | 275 | it "Creates a version for a project", -> 276 | options = 277 | rejectUnauthorized: true 278 | uri: makeUrl "version" 279 | method: 'POST' 280 | json: true 281 | body: 'ABC' 282 | followAllRedirects: true 283 | auth: 284 | user: 'test' 285 | pass: 'test' 286 | 287 | @jira.createVersion 'ABC', @cb 288 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 289 | 290 | # Invalid Project 291 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 292 | expect(@cb).toHaveBeenCalledWith 'Version does not exist or the 293 | currently authenticated user does not have permission to view it' 294 | 295 | @jira.request.mostRecentCall.args[1] null, statusCode:403, null 296 | expect(@cb).toHaveBeenCalledWith( 297 | 'The currently authenticated user does not have 298 | permission to edit the version') 299 | 300 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 301 | expect(@cb).toHaveBeenCalledWith( 302 | '401: Unable to connect to JIRA during createVersion.') 303 | 304 | # Successful Request 305 | @jira.request.mostRecentCall.args[1] null, 306 | statusCode:201, '{"body":"none"}' 307 | expect(@cb).toHaveBeenCalledWith null, '{"body":"none"}' 308 | 309 | it "Passes a search query to Jira, default options", -> 310 | fields = ["summary", "status", "assignee", "description"] 311 | options = 312 | rejectUnauthorized: true 313 | uri: makeUrl "search" 314 | method: 'POST' 315 | json: true 316 | followAllRedirects: true 317 | body: 318 | jql: 'aJQLstring' 319 | startAt: 0 320 | maxResults: 50 321 | fields: fields 322 | auth: 323 | user: 'test' 324 | pass: 'test' 325 | 326 | @jira.searchJira 'aJQLstring', {}, @cb 327 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 328 | 329 | # Invalid Project 330 | @jira.request.mostRecentCall.args[1] null, statusCode:400, null 331 | expect(@cb).toHaveBeenCalledWith 'Problem with the JQL query' 332 | 333 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 334 | expect(@cb).toHaveBeenCalledWith( 335 | '401: Unable to connect to JIRA during search.') 336 | 337 | # Successful Request 338 | @jira.request.mostRecentCall.args[1] null, 339 | statusCode:200, '{"body":"none"}' 340 | expect(@cb).toHaveBeenCalledWith null, '{"body":"none"}' 341 | 342 | it "Passes a search query to Jira, non-default options", -> 343 | fields = ["assignee", "description", "test"] 344 | options = 345 | rejectUnauthorized: true 346 | uri: makeUrl "search" 347 | method: 'POST' 348 | json: true 349 | followAllRedirects: true 350 | body: 351 | jql: 'aJQLstring' 352 | startAt: 200 353 | maxResults: 100 354 | fields: fields 355 | auth: 356 | user: 'test' 357 | pass: 'test' 358 | 359 | @jira.searchJira 'aJQLstring', { maxResults: 100, fields: fields, startAt: 200 }, @cb 360 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 361 | 362 | it "Gets a specified User's OPEN Issues", -> 363 | spyOn @jira, 'searchJira' 364 | expected = "assignee = test AND status in (Open, \"In Progress\", 365 | Reopened)" 366 | 367 | @jira.getUsersIssues 'test', true, @cb 368 | expect(@jira.searchJira).toHaveBeenCalledWith expected, {}, 369 | jasmine.any(Function) 370 | 371 | it "Properly Escapes @'s in Usernames", -> 372 | spyOn @jira, 'searchJira' 373 | expected = "assignee = email\\u0040example.com AND status in (Open, \"In Progress\", 374 | Reopened)" 375 | 376 | @jira.getUsersIssues 'email@example.com', true, @cb 377 | expect(@jira.searchJira).toHaveBeenCalledWith expected, {}, 378 | jasmine.any(Function) 379 | 380 | it "Gets the sprint issues and information", -> 381 | options = 382 | rejectUnauthorized: true 383 | uri: makeUrl("rapid/charts/sprintreport?rapidViewId=1&sprintId=1", true) 384 | method: 'GET' 385 | json: true 386 | auth: 387 | user: 'test' 388 | pass: 'test' 389 | 390 | @jira.getSprintIssues 1, 1, @cb 391 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 392 | 393 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 394 | expect(@cb).toHaveBeenCalledWith 'Invalid URL' 395 | 396 | it "Gets ALL a specified User's Issues", -> 397 | spyOn @jira, 'searchJira' 398 | expected = "assignee = test" 399 | 400 | @jira.getUsersIssues 'test', false, @cb 401 | expect(@jira.searchJira).toHaveBeenCalledWith expected, {}, 402 | jasmine.any(Function) 403 | 404 | it "Deletes an Issue", -> 405 | options = 406 | rejectUnauthorized: true 407 | uri: makeUrl "issue/1" 408 | method: 'DELETE' 409 | json: true 410 | followAllRedirects: true 411 | auth: 412 | user: 'test' 413 | pass: 'test' 414 | 415 | @jira.deleteIssue 1, @cb 416 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 417 | 418 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 419 | expect(@cb).toHaveBeenCalledWith '401: Error while deleting' 420 | 421 | # Successful Request 422 | @jira.request.mostRecentCall.args[1] null, statusCode:204 423 | expect(@cb).toHaveBeenCalledWith null, 'Success' 424 | 425 | it "Updates an Issue", -> 426 | options = 427 | rejectUnauthorized: true 428 | uri: makeUrl "issue/1" 429 | body: 'updateGoesHere' 430 | method: 'PUT' 431 | json: true 432 | followAllRedirects: true 433 | auth: 434 | user: 'test' 435 | pass: 'test' 436 | 437 | @jira.updateIssue 1, 'updateGoesHere', @cb 438 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 439 | 440 | @jira.request.mostRecentCall.args[1] null, statusCode:401 441 | expect(@cb).toHaveBeenCalledWith '401: Error while updating' 442 | 443 | # Successful Request 444 | @jira.request.mostRecentCall.args[1] null, statusCode:200 445 | expect(@cb).toHaveBeenCalledWith null, 'Success' 446 | 447 | it "Lists Transitions", -> 448 | options = 449 | rejectUnauthorized: true 450 | uri: makeUrl "issue/1/transitions?expand=transitions.fields" 451 | method: 'GET' 452 | json: true 453 | auth: 454 | user: 'test' 455 | pass: 'test' 456 | 457 | @jira.listTransitions 1, @cb 458 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 459 | 460 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 461 | expect(@cb).toHaveBeenCalledWith 'Issue not found' 462 | 463 | @jira.request.mostRecentCall.args[1] null, statusCode:401 464 | expect(@cb).toHaveBeenCalledWith '401: Error while updating' 465 | 466 | # Successful Request 467 | @jira.request.mostRecentCall.args[1] null, statusCode:200, 468 | transitions:"someTransitions" 469 | expect(@cb).toHaveBeenCalledWith null, {transitions:"someTransitions"} 470 | 471 | it "Transitions an issue", -> 472 | options = 473 | rejectUnauthorized: true 474 | uri: makeUrl "issue/1/transitions" 475 | body: 'someTransition' 476 | method: 'POST' 477 | followAllRedirects: true 478 | json: true 479 | auth: 480 | user: 'test' 481 | pass: 'test' 482 | 483 | @jira.transitionIssue 1, 'someTransition', @cb 484 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 485 | 486 | @jira.request.mostRecentCall.args[1] null, statusCode:401 487 | expect(@cb).toHaveBeenCalledWith '401: Error while updating' 488 | 489 | # Successful Request 490 | @jira.request.mostRecentCall.args[1] null, statusCode:204 491 | expect(@cb).toHaveBeenCalledWith null, "Success" 492 | 493 | it "Lists Projects", -> 494 | options = 495 | rejectUnauthorized: true 496 | uri: makeUrl "project" 497 | method: 'GET' 498 | json: true 499 | auth: 500 | user: 'test' 501 | pass: 'test' 502 | 503 | @jira.listProjects @cb 504 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 505 | 506 | @jira.request.mostRecentCall.args[1] null, statusCode:401 507 | expect(@cb).toHaveBeenCalledWith '401: Error while updating' 508 | 509 | @jira.request.mostRecentCall.args[1] null, statusCode:500 510 | expect(@cb).toHaveBeenCalledWith '500: Error while retrieving list.' 511 | 512 | # Successful Request 513 | @jira.request.mostRecentCall.args[1] null, statusCode:200, "body" 514 | expect(@cb).toHaveBeenCalledWith null, "body" 515 | 516 | it "Adds a comment to an issue", -> 517 | options = 518 | rejectUnauthorized: true 519 | uri: makeUrl "issue/1/comment" 520 | body: { 521 | 'body': 'aComment' 522 | } 523 | method: 'POST' 524 | followAllRedirects: true 525 | json: true 526 | auth: 527 | user: 'test' 528 | pass: 'test' 529 | 530 | @jira.addComment 1, 'aComment', @cb 531 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 532 | 533 | @jira.request.mostRecentCall.args[1] null, statusCode:400, 534 | '{"body:"none"}' 535 | expect(@cb).toHaveBeenCalledWith 'Invalid Fields: "{\\"body:\\"none\\"}"' 536 | 537 | # Successful Request 538 | @jira.request.mostRecentCall.args[1] null, statusCode:201 539 | expect(@cb).toHaveBeenCalledWith null, "Success" 540 | 541 | it "Adds a watcher to an issue", -> 542 | options = 543 | rejectUnauthorized: true 544 | uri: makeUrl "issue/1/watchers" 545 | body: JSON.stringify "testuser" 546 | method: 'POST' 547 | followAllRedirects: true 548 | json: true 549 | auth: 550 | user: 'test' 551 | pass: 'test' 552 | 553 | @jira.addWatcher 1, "testuser", @cb 554 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 555 | 556 | @jira.request.mostRecentCall.args[1] null, statusCode: 400 557 | expect(@cb).toHaveBeenCalledWith '400: Unable to connect to JIRA to add user as watcher.' 558 | 559 | # Successful Request 560 | @jira.request.mostRecentCall.args[1] null, statusCode: 204 561 | expect(@cb).toHaveBeenCalledWith 562 | 563 | it "Adds a worklog to a project", -> 564 | options = 565 | rejectUnauthorized: true 566 | uri: makeUrl "issue/1/worklog" 567 | body: 'aWorklog' 568 | method: 'POST' 569 | followAllRedirects: true 570 | json: true 571 | auth: 572 | user: 'test' 573 | pass: 'test' 574 | 575 | @jira.addWorklog 1, 'aWorklog', @cb 576 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 577 | 578 | @jira.request.mostRecentCall.args[1] null, statusCode:400, 579 | '{"body:"none"}' 580 | expect(@cb).toHaveBeenCalledWith 'Invalid Fields: "{\\"body:\\"none\\"}"' 581 | 582 | @jira.request.mostRecentCall.args[1] null, statusCode:403 583 | expect(@cb).toHaveBeenCalledWith 'Insufficient Permissions' 584 | 585 | @jira.request.mostRecentCall.args[1] null, statusCode:401 586 | expect(@cb).toHaveBeenCalledWith '401: Error while updating' 587 | 588 | # Successful Request 589 | @jira.request.mostRecentCall.args[1] null, statusCode:201 590 | expect(@cb).toHaveBeenCalledWith null, "Success" 591 | 592 | it "Adds a worklog to a project with remaining time set", -> 593 | options = 594 | rejectUnauthorized: true 595 | uri: makeUrl "issue/1/worklog?adjustEstimate=new&newEstimate=1h" 596 | body: 'aWorklog' 597 | method: 'POST' 598 | followAllRedirects: true 599 | json: true 600 | auth: 601 | user: 'test' 602 | pass: 'test' 603 | 604 | @jira.addWorklog 1, 'aWorklog', '1h', @cb 605 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 606 | 607 | @jira.request.mostRecentCall.args[1] null, statusCode:400, 608 | '{"body:"none"}' 609 | expect(@cb).toHaveBeenCalledWith 'Invalid Fields: "{\\"body:\\"none\\"}"' 610 | 611 | @jira.request.mostRecentCall.args[1] null, statusCode:403 612 | expect(@cb).toHaveBeenCalledWith 'Insufficient Permissions' 613 | 614 | @jira.request.mostRecentCall.args[1] null, statusCode:401 615 | expect(@cb).toHaveBeenCalledWith '401: Error while updating' 616 | 617 | # Successful Request 618 | @jira.request.mostRecentCall.args[1] null, statusCode:201 619 | expect(@cb).toHaveBeenCalledWith null, "Success" 620 | 621 | it "Lists Issue Types", -> 622 | options = 623 | rejectUnauthorized: true 624 | uri: makeUrl "issuetype" 625 | method: 'GET' 626 | json: true 627 | auth: 628 | user: 'test' 629 | pass: 'test' 630 | 631 | @jira.listIssueTypes @cb 632 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 633 | 634 | @jira.request.mostRecentCall.args[1] null, statusCode:401 635 | expect(@cb).toHaveBeenCalledWith '401: Error while retrieving issue types' 636 | 637 | # Successful Request 638 | @jira.request.mostRecentCall.args[1] null, statusCode:200, "body" 639 | expect(@cb).toHaveBeenCalledWith null, "body" 640 | 641 | it "Retrieves a Rapid View Backlog", -> 642 | options = 643 | rejectUnauthorized: true 644 | uri: makeUrl("xboard/plan/backlog/data?rapidViewId=123", true) 645 | method: 'GET' 646 | json: true 647 | auth: 648 | user: 'test' 649 | pass: 'test' 650 | 651 | @jira.getBacklogForRapidView 123, @cb 652 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 653 | 654 | @jira.request.mostRecentCall.args[1] null, statusCode:500 655 | expect(@cb).toHaveBeenCalledWith '500: Error while retrieving backlog' 656 | 657 | # Successful Request 658 | @jira.request.mostRecentCall.args[1] null, statusCode:200, body: issues: ['test'] 659 | expect(@cb).toHaveBeenCalledWith null, issues: ['test'] 660 | 661 | it "Finds the remote links of an issue", -> 662 | options = 663 | rejectUnauthorized: true 664 | uri: makeUrl "issue/1/remotelink" 665 | method: 'GET' 666 | json: true 667 | auth: 668 | user: 'test' 669 | pass: 'test' 670 | @jira.getRemoteLinks 1, @cb 671 | expect(@jira.request) 672 | .toHaveBeenCalledWith(options, jasmine.any(Function)) 673 | 674 | # Invalid issue number (different than unable to find??) 675 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 676 | expect(@cb).toHaveBeenCalledWith 'Invalid issue number.' 677 | 678 | # Unable to find issue 679 | @jira.request.mostRecentCall.args[1] null, statusCode:401, null 680 | expect(@cb).toHaveBeenCalledWith( 681 | '401: Unable to connect to JIRA during request.') 682 | 683 | # Successful Request 684 | @jira.request.mostRecentCall.args[1] null, 685 | statusCode:200, body:"none" 686 | expect(@cb).toHaveBeenCalledWith(null, 'none') 687 | 688 | it "Creates a remote link", -> 689 | options = 690 | rejectUnauthorized: true 691 | uri: makeUrl "issue/1/remotelink" 692 | method: 'POST' 693 | json: true 694 | body: 'test' 695 | auth: 696 | user: 'test' 697 | pass: 'test' 698 | 699 | @jira.createRemoteLink 1, 'test', @cb 700 | expect(@jira.request).toHaveBeenCalledWith options, jasmine.any(Function) 701 | 702 | # Invalid Issue 703 | @jira.request.mostRecentCall.args[1] null, statusCode:404, null 704 | expect(@cb).toHaveBeenCalledWith 'Cannot create remote link. Invalid issue.' 705 | 706 | response = 707 | statusCode:400 708 | body: 709 | errors: 710 | title: 'test' 711 | @jira.request.mostRecentCall.args[1] null, response 712 | expect(@cb).toHaveBeenCalledWith( 713 | 'Cannot create remote link. test') 714 | 715 | --------------------------------------------------------------------------------