<|start_filename|>lib/ralio.js<|end_filename|>
var url = require('url'),
    querystring = require('querystring'),
    request = require('request'),
    _ = require('underscore'),
    temp = require('temp'),
    editor = require('editor'),
    package = require('../package.json'),
    fs = require('fs');

function Ralio(hostname, username, password, project, team) {
  this.hostname = hostname;
  this.username = username;
  this.password = password;
  this.project = project;
  this.team = team;
  this._editor = editor; //needed for testability - the editor module exports a raw function and not an object so there's no direct way to stub it out.  :/
}

Ralio.test = false;

Ralio.prototype.prefixes = function (workspaceId) {
  // Hard coded for now - we should load these from WSAPI because they're configurable per workspace
  return {
    'US': 'hierarchicalrequirement',
    'S' : 'hierarchicalrequirement',
    'TA': 'task',
    'DE': 'defect'
  };
}

Ralio.prototype.prefixOf = function (formattedID) {
  return formattedID.match(/^[^0-9]+/)[0].toUpperCase();
}

Ralio.prototype.rallyWsapiUrl = function (type, query, options) {
  var rally_url = url.parse(this.hostname);

  options = options || {};
  query = query || {};
  return url.format({
    protocol: rally_url.protocol,
    auth: this.username + ':' + this.password,
    hostname: rally_url.hostname,
    port: rally_url.port,
    pathname: options.pathname ? options.pathname : '/slm/webservice/1.42/' + type + '.js',
    query: query
  });
}

Ralio.prototype.bulkUrl = function (options) {
  return this.rallyWsapiUrl('adhoc', {}, options);
};

Ralio.prototype.artifactUrl = function (formattedID, query) {
  var type = this.prefixes()[this.prefixOf(formattedID)];
  return this.rallyWsapiUrl(type, query);
};

Ralio.prototype.typedefUrl = function (query) {
  return this.rallyWsapiUrl('typedefinition', query);
};

Ralio.prototype.rallyWsapiRequest = function (options) {
  return _.extend({
    method: 'GET',
    json: {},
    strictSSL: !Ralio.test
    }, options);
}

Ralio.prototype.domainObject = function (displayName, url, callback) {
  var self = this;
  var req = this.rallyWsapiRequest({url: url});

  this.request(req, function (err, results){
    if (err !== null) {
      callback(err);
    } else if (results.QueryResult.TotalResultCount > 1) {
      callback('Query for single object [' + displayName + '] returned multiple results.  Errors: ' + results.QueryResult.Errors);
    } else if (results.QueryResult.TotalResultCount == 0) {
      callback(displayName + ' not found.  Errors: ' + results.QueryResult.Errors);
    } else {
      var result = results.QueryResult.Results[0];
      result._QueryResult = results.QueryResult;
      callback(err, result);
    }
  });
}

Ralio.prototype.typedef = function (type, query, callback) {
  query = _.extend({
    query: "(TypePath = \"" + type + "\")",
    fetch: true,
    pagesize: 1,
    }, query);

  var url = this.typedefUrl(query);
  this.domainObject('TypeDef [' + type + ']', url, callback);
}

Ralio.prototype.artifact = function (formattedID, query, opts, callback) {
  var self = this;
  query = _.extend({
    query: "(FormattedID = " + formattedID + ")",
    fetch: "FormattedID",
    typeDefinition: false,
    pagesize: 1
    }, query);

  opts = _.extend({
    typeDefinition: false
    }, opts);

  var url = this.artifactUrl(formattedID, query);

  this.domainObject(formattedID, url, function (err, result){
    if (err !== null) {
      callback(err);
    } else {
      if (opts.typeDefinition) {
        self.typedef(result._type, {}, function(err, typedef) {
          result._typeDefinition = typedef;

          callback(err, result);
        });
      } else {
        callback(err, result);
      }
    }
  });
}

Ralio.prototype.date = function () {
  return new Date().toISOString().slice(0, 10);
}

Ralio.prototype.bulk = function (query, callback) {
  var data = {};
  for (var key in query) {
    if (query.hasOwnProperty(key)) {
      if (typeof query[key] === 'object') {
        data[key] = '/' + key + '?' + querystring.stringify(query[key]);
      } else {
        data[key] = query[key];
      }
    }
  }

  var options = {
    url: this.bulkUrl(),
    method: 'POST',
    json: data,
    strictSSL: !Ralio.test
  };

  this.request(options, callback);
};

Ralio.prototype.comments = function (itemID, callback) {
  var self = this,
      query = {
        fetch: 'PostNumber,Text,User',
        query: '(Artifact.FormattedID = "' + itemID + '")'
      },
      ret = {};

  self.artifact(itemID, {
    fetch: 'FormattedID,Name,Owner,Project,Discussion'
  }, {}, function(error, item) {
    if (error !== null) {
      callback(error);
    } else {
      ret.artifact = item;
      self.bulk({
        conversationpost: query
      }, function (error, data) {
        if (error !== null) {
          callback(error);
        } else {
          var posts = data.conversationpost.Results;
          posts = _.sortBy(posts, function (s) { return s.PostNumber });
          ret.comments = posts;
          callback(null, ret);
        }
      });
    }
  });
};

Ralio.prototype.addComment = function (itemID, text, callback) {
  var self = this,
      options = {
        url: self.bulkUrl({"pathname":"/slm/webservice/1.36/conversationpost/create.js"}),
        method: 'POST',
        json: {"ConversationPost":{
          "Text": text
        }},
        strictSSL: !Ralio.test
      };

  self.artifact(itemID, {
    fetch: 'FormattedID,Name,Owner,Project,Discussion'
  }, {}, function(error, item) {
    if (error !== null) {
      callback(error);
    } else {
      options.json.ConversationPost.Artifact = item._ref;
      options.json.ConversationPost.PostNumber = item.Discussion.length;
      self.request(options, function(error, data){
        if (error !== null) {
          callback(error);
        } else if (data.CreateResult && data.CreateResult['Object']) {
          callback(null, data.CreateResult['Object']);
        } else {
          callback("Failed to create a new discussion post.", data);
        }
      });
    }
  });
};

Ralio.prototype.time = function(taskID, hours, callback) {
  var self = this,
      type = taskID.slice(0,2).toUpperCase(),
      query = {
        fetch: 'Project,WorkProduct',
        query: '((Project.Name = "' + this.project + '") AND (FormattedID = "' + taskID + '"))'
      };

  if (type === "TA") {
    query = {task: query};
  } else {
    callback("Ralio can only put timesheet entries against a task (TA).");
  }

  var addTEV = function (project, item, hours) {
    var date = new Date();
    date.setUTCHours(0,0,0,0);
    date = date.toISOString();
    self.bulk({
      timeentryvalue: {
        fetch: 'Hours',
        query: '((TimeEntryItem.Task.FormattedID = "' + taskID + '") AND (DateVal = "' + date + '"))'
      }
    }, function (error, data) {
      if (error !== null) {
        callback(error);
      } else {
        var value = data.timeentryvalue.Results;
        if (value.length) {
          self.update(value[0]._ref, {
            TimeEntryValue: {
              Hours: value[0].Hours + hours
            }
          }, function (error) {
            if (error) {
              callback(error);
            } else {
              callback(null, {
                Hours: value[0].Hours + hours
              });
            }
          });
        } else {
          var options = {
            url: self.bulkUrl({"pathname":"/slm/webservice/1.36/timeentryvalue/create.js"}),
            method: 'POST',
            json: {"TimeEntryValue":{
              "Project": project,
              "TimeEntryItem": item,
              "DateVal": date,
              "Hours": hours
            }},
            strictSSL: !Ralio.test
          };
          self.request(options, function(error, data){
            if (error !== null) {
              callback(error);
            } else if (data.CreateResult && data.CreateResult['Object']) {
              callback(null, data.CreateResult['Object']);
            } else {
              callback("Failed to create a time entry.", data);
            }
          });
        }
      }
    });
  }

  this.bulk(query, function (error, data) {
    if (error !== null) {
      callback(error);
    } else {
      var task = data.task.Results;
      if (!task.length) {
        callback("No task found with that ID.");
      } else {
        var project = task[0].Project._ref,
            story   = task[0].WorkProduct._ref,
            taskRef = task[0]._ref;

        self.bulk({
          timeentryitem: {
            fetch: 'WeekStartDate',
            query: '(Task.FormattedID = "' + taskID + '")'
          }
        }, function (error, data) {
          if (error !== null) {
            callback(error);
          } else {
            var item = data.timeentryitem.Results;
            if (item.length) {
              addTEV(project, item[0]._ref, hours);
            } else {
              var itemCreation = setInterval(function() {
                var weekstart = new Date();
                do {
                  weekstart.setDate(weekstart.getDate() - 1);
                } while (weekstart.getDay());
                var options = {
                  url: self.bulkUrl({"pathname":"/slm/webservice/1.36/timeentryitem/create.js"}),
                  method: 'POST',
                  json: {"TimeEntryItem":{
                    "Project": project,
                    "WorkProduct": story,
                    "Task": taskRef,
                    "WeekStartDate": weekstart.toISOString()
                  }},
                  strictSSL: !Ralio.test
                };
                self.request(options, function(error, data) {
                  if (error !== null) {
                    callback(error);
                  } else {
                    addTEV(project, data.CreateResult['Object']._ref, hours);
                  }
                });
                clearInterval(itemCreation);
              }, 300);
            }
          }
        });
      }
    }
  });
}

Ralio.prototype.headers =  function (headers) {
  return _.defaults(headers || {}, {
    "X-RallyIntegrationLibrary": "Node.js Native",
    "X-RallyIntegrationName": package.name,
    "X-RallyIntegrationVendor": package.repository.url,
    "X-RallyIntegrationVersion": package.version,
    "User-Agent": "Node.JS (request:" + package.dependencies.request + ")"
  });
}

Ralio.prototype.request = function(options, callback) {
  options.headers = this.headers(options.headers);

  request(options, function (error, response, data) {
    if (error !== null) {
      callback(error);
    } else if (response.statusCode === 401) {
      var error = "Authentication failed! Check your ~/.raliorc file!";
      callback(Ralio.test ? error : error.red);
    } else if (response.statusCode !== 200) {
      callback(response.body);
    } else {
      callback(null, data);
    }
  });
};

Ralio.prototype.updateArtifact = function (artifact, updates, callback) {
  var up={};
  up[artifact._type] = updates;
  this.update(artifact._ref, up, callback);
}

Ralio.prototype.update = function (ref, changes, callback) {
  ref = url.parse(ref);
  ref.auth = this.username + ':' + this.password;
  ref = url.format(ref);

  var options = {
    url: ref,
    method: 'POST',
    headers: this.headers(),
    json: changes,
    strictSSL: !Ralio.test
  };

  request(options, function (error, response, data) {
    if (error !== null) {
      callback(error);
    } else if (response.statusCode !== 200) {
      callback(response.body);
    } else if (response.body.OperationResult.Errors.length > 0) {
      callback(response.body.OperationResult.Errors);
    } else {
      callback(null);
    }
  });
};

Ralio.prototype.backlog = function (options, callback) {
  var defaults = {
    projectName: null,
    pagesize: 100,
    tag: null
  };
  options = _.extend({}, defaults, options);
  var query = {
    fetch: 'Name,FormattedID,Rank,PlanEstimate,Tags',
    order: 'Rank',
    query: (options.tag ?
      '((Project.Name = "' + options.projectName + '") AND ((Iteration = NULL) AND (Tags.Name = "' + options.tag + '")))' :
      '((Project.Name = "' + options.projectName + '") AND (Iteration = NULL))'),
    pagesize: options.pagesize
  };
  this.bulk(
    {
      hierarchicalrequirement: query,
      defect: query
    },
    function (error, data) {
      if (error !== null) {
        callback(error);
      } else {
        var stories = data.hierarchicalrequirement.Results.concat(data.defect.Results);
        stories = _.sortBy(stories, function (s) { return s.Rank });
        callback(null, stories);
      }
    }
  );
};

Ralio.prototype.sprint = function (projectName, options, callback) {

  // How to build Rally's queries.
  // 1 Filter: (Project.Name = "Hokage")
  // 2 Filters: ((Project.Name = "Hokage") AND (Iteration.StartDate <= "2013-03-07"))
  // 3 Filters: ((Project.Name = "Hokage") AND ((Iteration.StartDate <= "2013-03-07") AND (Iteration.EndDate >= "2013-03-07")))
  // 4 Filters: ((Project.Name = "Hokage") AND ((Iteration.StartDate <= "2013-03-07") AND ((Iteration.EndDate >= "2013-03-07") AND (Project.Name = "Hokage"))))

  var self = this,
      d = this.date(),
      fetch = 'Name,FormattedID,Rank,PlanEstimate,ScheduleState,Tasks,Tags,Pair,Defects,State,Owner,TaskIndex,Blocked',
      query_string = '((Project.Name = "' + projectName + '") AND ((Iteration.StartDate <= "' + d + '") AND (Iteration.EndDate >= "' + d + '")))';

  if(options.find && options.find !== true)
    query_string = '((Project.Name = "' + projectName + '") AND ((Iteration.StartDate <= "' + d + '") AND ((Iteration.EndDate >= "' + d + '") AND (Name contains "'+ options.find + '"))))';

  var query = {
    fetch: fetch,
    order: 'Rank',
    query: query_string,
    pagesize: 100
  };

  this.bulk(
    {hierarchicalrequirement: query, defect: query},
    function (error, data) {
      if (error !== null) {
        callback(error);
      } else {
        var stories = data.hierarchicalrequirement.Results.concat(data.defect.Results);
        callback(null, self.orderStories(stories));
      }
    }
  );
}

Ralio.prototype.story = function (storyID, callback) {
  var self = this,
      type = storyID.slice(0,2).toUpperCase(),
      query = {
        fetch: 'Name,FormattedID,PlanEstimate,ScheduleState,Tasks,Tags,Pair,Defects,State,Owner,TaskIndex,Blocked,Project,ObjectID,Description',
        query: '(FormattedID = "' + storyID + '")'
      };

  if (type === 'US') {
    query = {hierarchicalrequirement: query};
  } else if (type === "DE") {
    query = {defect: query};
  } else if (type === "TA") {
    query = {task: query};
  } else {
    callback("Ralio's can only show detailed information of a defect(DE), story(US) or tasks(TA).");
  }

  this.bulk(query, function (error, data) {
    if (error !== null) {
      callback(error);
    } else {
      var data = data.hierarchicalrequirement || data.defect || data.task;

      if(data.TotalResultCount === 0){
        callback(storyID + " Not Found!")
      }

      var story = data.Results[0];

      if (story !== null) {
        story.Tasks = self.orderTasks(story.Defects ? story.Tasks.concat(story.Defects) : story.Tasks);
      }

      callback(null, story);
    }
  });
};

Ralio.prototype.setTaskState = function (task_id, options, callback) {
  options = options || {};
  var self = this,
      type = task_id.slice(0,2).toUpperCase(),
      query = {
        fetch: true,
        query: '(FormattedID = "' + task_id + '")'
      };

  if (type === "DE") {
    query = {defect: query, user: {}};
  } else if (type === "TA") {
    query = {task: query, user: {}};
  } else if (type === "US") {
    query = {hierarchicalrequirement: query, user: {}};
  } else {
    callback("You can only start a Task (TA) or a Defect (DE)");
  }

  this.bulk(query, function (error, data) {
    if (error !== null) {
      callback(error);
    } else {
      var user = data.user,
          data =  data.defect || data.task || data.hierarchicalrequirement,
          task = null;

      if(data.TotalResultCount === 0)
        callback(task_id + " Not Found!")

      if (type === "DE") {
        task = data.Results[0];

        var update = {
          Defect: {
            _ref: task._ref,
          }
        },
        taskObject = update.Defect;

      } else if (type === "TA") {
        task = data.Results[0];

        var update = {
          Task: {
            _ref: task._ref,
          }
        },
        taskObject = update.Task;
      } else if (type === "US") {
        task = data.Results[0];

        var update = {
          HierarchicalRequirement: {
            _ref: task._ref,
          }
        },
        taskObject = update.HierarchicalRequirement;
      }

      if (typeof options.pair === "string") {
        taskObject.Pair = options.pair
      } else if (options.pair === true){
        taskObject.Pair = '';
      }

      if (options.state) {

        if (type === "US" || type === "DE") {
          taskObject.ScheduleState = options.state;
        } else {
          taskObject.State = options.state;
        }

        if (options.state === "In-Progress" || options.state === "Defined") {
          taskObject.ToDo = 1.0;
          taskObject.Estimate = taskObject.ToDo;

          if (type === "DE") {
            taskObject.State = "Open";
          }
        }

        if (options.state === 'Completed') {
          taskObject.ToDo = 0.0;

          if (type === "DE") {
            taskObject.State = "Fixed";
          }

        }
      }

      if (options.blocked !== undefined) {
        taskObject.Blocked = options.blocked;
      }

      taskObject.Owner = options.own ? user._ref : null;

      if (options.rootcause !== undefined) {
        taskObject.RootCause = options.rootcause;
      }

      if (options.resolution !== undefined) {
        taskObject.Resolution = options.resolution;
      }

      self.update(task._ref, update, function (error) {
        if (error) {
          callback(error.join('\n\n'));
        } else {
          self.bulk(query, function (error, data) {
            if (error) {
              callback(error);
            } else {
              data = data.defect || data.task || data.hierarchicalrequirement;
              callback(null, data.Results[0]);
            }
          });
        }
      });
    }
  });
}

Ralio.prototype.current = function (projectName, callback) {
  var self = this, d = this.date();
  this.bulk(
    {
      user: {},
      hierarchicalrequirement: {
        fetch: 'Name,FormattedID,Rank,PlanEstimate,ScheduleState,Tasks,Tags,Pair,State,Owner,TaskIndex,Blocked',
        order: 'Rank',
        query: '((Project.Name = "' + projectName + '") AND ((Iteration.StartDate <= "' + d + '") AND (Iteration.EndDate >= "' + d + '")))',
        pagesize: 100
      },
      defect: {
        fetch: 'Name,FormattedID,Rank,PlanEstimate,ScheduleState,Tasks,Tags,Pair,State,Owner,TaskIndex,Blocked',
        order: 'Rank',
        query: '((Project.Name = "' + projectName + '") AND ((Iteration.StartDate <= "' + d + '") AND (Iteration.EndDate >= "' + d + '")))',
        pagesize: 100
      }
    },
    function (error, data) {
      if (error !== null) {
        callback(error);
      } else {
        var user = data.user,
            stories = data.hierarchicalrequirement.Results,
            defects = data.defect.Results;

        stories = _.filter(stories, function (story) {
          return _.any(story.Tasks, function (task) {
            return task.Owner !== null && task.State === 'In-Progress' && task.Owner._ref == user._ref;
          });
        });

        defects = _.filter(defects, function (defect) {
            return defect.Owner !== null && defect.State === 'Open' && defect.Owner._ref == user._ref;
        });

        callback(null, self.orderStories(stories.concat(defects)));
      }
    }
  );
}

Ralio.prototype.point = function (storyID, points, callback) {
  var self = this,
      type = storyID.slice(0,2).toUpperCase(),
      query = {
        fetch: true,
        query: '(FormattedID = "' + storyID + '")'
      };

  if (type === "DE") {
    query = {defect: query, user: {}};
  } else if (type === "US") {
    query = {hierarchicalrequirement: query, user: {}};
  } else {
    callback("You can only point a Defect (DE) or a Story (US)");
  }

  self.bulk(query, function (error, data) {

    if (error !== null) {
      callback(error);
    } else {

      var update = {},
          data = data.hierarchicalrequirement || data.defect;

      if(data.TotalResultCount === 0)
        callback(storyID + " Not Found!")

      var task = data.Results[0];
      if (type === "US") {
        update.HierarchicalRequirement = {
          _ref: task._ref,
          PlanEstimate: points
        };
      } else if (type === "DE") {
        update.Defect = {
          _ref: task._ref,
          PlanEstimate: points
        };
      }

      self.update(task._ref, update, function (error) {
        if (error) {
          callback(error);
        } else {

          self.bulk(query, function (error, data) {
            if (error) {
              callback(error);
            } else {
              callback(null, (data.hierarchicalrequirement || data.defect).Results[0]);
            }
          });

        }
      });
    }
  });
};

Ralio.prototype.createTask = function (projectName, storyID, taskName, tags, callback) {
  var self = this,
      tagsObject  = [];

  self.getTags(tags, function (error, data) {
    if (error){
      callback(error)
    } else {
      if (typeof data._ref !== "undefined")
        tagsObject.push({_ref: data._ref});
    }
  });

  var taskCreation = setInterval(function() {
    if (tagsObject.length === tags.length || Ralio.test) {
      self.story(storyID, function(error, data) {
        if (error)
          callback(error)

        var story = data._ref,
            project = data.Project._ref,
            new_task = {"Task":{
                "Name": taskName,
                "Project": project,
                "WorkProduct": story,
                "ToDo": 1.0,
                "Estimate": 1.0,
                "Tags": tagsObject
              }
            };

        var options = {
          url: self.bulkUrl({"pathname":"/slm/webservice/1.42/task/create.js"}),
          method: 'POST',
          json: new_task,
          strictSSL: !Ralio.test
        };

        self.request(options, function(error, data){
          callback(null, null, data.CreateResult.Object, taskName);
        });
      });
      clearInterval(taskCreation);
    }
  }, 300);
};

Ralio.prototype.deleteTask = function (projectName, storyID, callback) {
  var self = this;

  self.getTask(storyID, function(error, task) {
    if (error || typeof task !== "object")
      return callback(error);

    var deletePath = url.parse(task._ref).pathname;

    var options = {
          url: self.bulkUrl({"pathname": deletePath}),
          method: 'DELETE',
          json: {},
          strictSSL: !Ralio.test
        };

    self.request(options, function(error, data) {
      var message = Ralio.test ? storyID + " deleted" : storyID.yellow + " deleted".green;
      callback(message);
    });
  });
};

Ralio.prototype.task = function (option, projectName, storyID, taskName, tags, callback) {
  switch(option){
    case "create": this.createTask(projectName, storyID, taskName, tags, callback); break;
    case "delete": this.deleteTask(projectName, storyID, callback); break;
    default: callback("Unknown option " + option); break;
  }
};

Ralio.prototype.getTag = function (tag, callback) {
  this.bulk({
      tag: {
        fetch: true,
        query: '(Name = "' + tag + '")'
      }
    }, function (error, data) {
      error = data.tag.TotalResultCount === 0 ? 'Tag ' + tag + ' Not Found!' : null;
      if (error) {
        callback(error);
      } else {
        callback(null, data.tag.Results[0]);
      }
    });
};

Ralio.prototype.getTags = function (tags, callback) {
  tags = (typeof tags === "object") ? tags : tags.split(',');
  for (i in tags) {
    this.getTag(tags[i], function(error, data){
      if(error){
        callback(error);
      } else {
        callback(null, data);
      }
    });
  }
};

Ralio.prototype.getTask = function (taskID, callback){
  this.bulk({
    user: {},
    task: {query: '(FormattedID = "' + taskID + '")'}
  }, function(error, data){
    if(data.task.TotalResultCount === 0) {
      var error = "Task Not Found!";
      callback(Ralio.test ? error : error.red, null);
    } else {
      callback(null, data.task.Results[0]);
    }
  })
};

Ralio.prototype.orderStories = function (stories) {
  stories = _.sortBy(stories, function (s) { return s.Rank });
  for (var i = 0; i < stories.length; i++) {
    stories[i].Tasks = this.orderTasks(stories[i].Tasks);
  }
  return stories;
}

Ralio.prototype.orderTasks = function (tasks) {
 return _.sortBy(tasks, function (t) { return t.TaskIndex });
}

Ralio.prototype.editor = function (contents, opts, callback) {
  opts = opts || {};
  var suffix = opts.suffix || '.txt';
  var self = this;

  var tmp_file = temp.open({suffix: suffix}, function (err, tmp_file) {
    fs.write(tmp_file.fd, contents);
    fs.close(tmp_file.fd, function(err) {
      if (err && (typeof callback == 'function')) {
        callback({success: false, err: err});
        return;
      }

      self._editor(tmp_file.path, function (code, sig) {
        if (0 == code) {
          if (typeof callback == 'function') {
            var new_contents = fs.readFileSync(tmp_file.path).toString().trimRight();
            callback ({
              success: true,
              value: new_contents,
              editor: {
                exit_code: code,
                exit_signal: sig
              }});
          }
        } else {
          if (typeof callback == 'function') {
            callback ({
              success: false,
              editor: {
                exit_code: code,
                exit_signal: sig
              }});
          }
        }
      }); // editor(tmp_file.path)
    }); //fs.close(tmp_file.fd)
  }); // temp.open()

}

module.exports = Ralio;

<|start_filename|>lib/cli.js<|end_filename|>
var path = require('path'),
    fs = require('fs'),
    q = require('q'),
    child_process = require('child_process'),
    temp = require('temp'),
    colors = require('colors'),
    _ = require('underscore'),
    html = require("html"),
    Ralio = require('./ralio');

function CLI(hostname) {
  this.hostname = hostname || 'https://rally1.rallydev.com/';
  this.configure();
  this.ralio = new Ralio(this.hostname, this.config.username, this.config.password, this.config.project, this.config.team);
}

CLI.prototype.configure = function () {
  var config_path = path.join(process.env.HOME, '.raliorc');
  
  try {
    this.config = JSON.parse(fs.readFileSync(config_path));  
  } catch (e){
    console.log('~/.raliorc doesn\'t exist!'.red)
    process.kill();
  }

  var quickIDs_path = path.join(process.env.HOME, '.ralio_recent');
  if (fs.existsSync(quickIDs_path)) {
    this.quickIDs = JSON.parse(fs.readFileSync(quickIDs_path));
  }
};

CLI.prototype.backlog = function (options) {
  var self = this, 
      team = this.config.team,
      project = options.project || this.config.project,
      projectName = project || team,
      opts = {
        projectName: project || team,
        pagesize: options.all ? 100 : 20,
        tag: options.tag
      };
  this.ralio.backlog(opts, function (error, stories) {
    self.errors(error);
    if (!options.all) {
      stories = stories.slice(0, 20);
    }
    stories.forEach(function (story) {
      self.printStoryLine(story, {state: false, tags: true});
    });
    self.saveQuickIDs();
  });
};

CLI.prototype.sprint = function (options) {
  var self = this,
      team = options.group || this.config.team,
      project = options.project || this.config.project,
      projectName = project || team;
  this.ralio.sprint(projectName, options, function (error, stories) {
    self.errors(error);
    stories.forEach(function (story) {
      if (story.ScheduleState != 'Accepted' || options.accepted) {
        var tasks = _.extend(story.Tasks, story.Defects)
        if(tasks.length > 0){
          self.printStoryLine(story, {owners: !options.tasks});
          if (options.tasks) {
            tasks.forEach(function (task) {
              self.printTaskLine(task);
            });
          }
        } else {
          self.printTaskLine(story, {tab: false, points: true});
        }
      }
    });
    self.saveQuickIDs();
  });
};

CLI.prototype.current = function () {
  var self = this;
  this.ralio.current(this.config.team, function (error, stories) {
    self.errors(error);
    stories.forEach(function (story) {
      self.printStoryLine(story);
      story.Tasks.forEach(function (task) {
        self.printTaskLine(task);
      });
    });
    self.saveQuickIDs();
  });
};

CLI.prototype.preFormatHtml = function(html_in) {
  var formatted = html.prettyPrint(html_in, {indent_size: 2});
  return formatted;
};

CLI.prototype.ATTRIBUTE_OPTIONS = {
  supported: [
    {type: "STRING", suffix: '.txt'},
    {type: "BOOLEAN", suffix: '.txt'},
    {type: "INTEGER", suffix: '.txt'},
    {type: "TEXT", suffix: '.html', preformat: CLI.prototype.preFormatHtml}
    ],

  forType: function (type) {
    return _.find(this.supported, function(ok_type){return ok_type.type == type});
  }
};

CLI.prototype.edit = function (formattedID, field) {
  var self = this;

  this.ralio.artifact(formattedID, {fetch: field}, {typeDefinition: true}, function (err, artifact) {
    self.errors(err);

    if (typeof artifact[field] === 'undefined') {
      return self.errors('Field ' + field + ' not found.');
    }

    var fieldType = _.find(artifact._typeDefinition.Attributes, function(attr){
      return attr.ElementName == field 
    }).AttributeType;

    var fieldOpts = self.ATTRIBUTE_OPTIONS.forType(fieldType);
    if (typeof fieldOpts === 'undefined') {
      return self.errors('Field ' + field + ' is not a type suported by this tool. [' + fieldType + ']');
    }

    var suffix = fieldOpts.suffix || '.txt',
        old_value = artifact[field];

    if (typeof fieldOpts.preformat === 'function') {
      old_value = fieldOpts.preformat(old_value);
    }

    self.ralio.editor(old_value, {suffix: suffix}, function (result) {
      if (false === result.success) {
        return self.errors('EDITOR returned error.  Exit code: ' +  result.editor.exit_code + ', Exit signal: ' + result.editor.exit_signal);
      }

      if (old_value !== result.value) {
        var updates = {};
        updates[field] = result.value;
        
        self.ralio.updateArtifact(artifact, updates, function (err) {
          if (err !== null){
            return self.errors('Error updating ' + formattedID + '.  Err: ' + err);
          } else {
            console.log(formattedID.yellow + ' ' + old_value.grey + ' ▶ ' + result.value.green);
          }
        });
      }
    });
  });
};

CLI.prototype.show = function (story) {
  var self = this;
  this.ralio.story(this.fetchID(story), function (error, story) {
    self.errors(error);
    if (story === null) {
      return self.errors('No story found :(');
    } else {

      var descTempPath = temp.path({suffix: '.html'});
      fs.writeFileSync(descTempPath, story.Description);
      child_process.exec('elinks -dump ' + descTempPath, function (err, stdout, stderr) {
        
        if (err) {
          var description =
            '  There was an error formatting the description text :(\n'.red +
            '  Is elinks installed?  (hint: brew install elinks or sudo apt-get install elinks).'.red;
        } else {
          var description = stdout.replace(/^\n+/, '').replace(/\n+$/g, '');
        }

        fs.unlinkSync(descTempPath);

        (story.Tasks.length > 0) ? 
          self.printStoryLine(story) : self.printTaskLine(story, {tab: false, points: true});

        if (description != '') {
          console.log('\n' + description + '\n');
        }

        story.Tasks.forEach(function (task) {
          self.printTaskLine(task);
        });

        self.saveQuickIDs();
      });
    }
  });
};

CLI.prototype.open = function (story) {
  var self = this,
      openCommand = process.platform === "linux" ? "xdg-open" : "open"
  this.ralio.story(this.fetchID(story), function (error, story) {
    self.errors(error ? error.red : error);
    if (story === null) {
      return self.errors('No story found :(');
    } else {
      var story_type = null;
      switch (story._type) {
        case 'Defect': story_type = 'defect'; break;
        case 'HierarchicalRequirement': story_type = 'userstory'; break;
        case 'Task': story_type = 'task'; break;
      }
      child_process.exec(
        openCommand + ' ' +
        self.hostname + '#/' +       
        story.Project.ObjectID +
        'd/detail/' + story_type +
        '/' + story.ObjectID);
    }
  });
};

CLI.prototype.start = function (task, options) {
  var self = this,
      update_options = {state: 'In-Progress', own: true, blocked: false, pair: options.pair};

  this.ralio.setTaskState(this.fetchID(task), update_options, function (error, task) {
    self.errors(error);
    self.printTaskLine(task, {quickid: false});
  });
};

CLI.prototype.finish = function (task, options) {
  var self = this,
      update_options = {state: 'Completed', own: true, blocked: false, pair: options.pair};

  if (options.rootcause)
    update_options = _.extend(update_options, {rootcause: options.rootcause})
  if (options.resolution)
    update_options = _.extend(update_options, {resolution: options.resolution});

  this.ralio.setTaskState(this.fetchID(task), update_options, function (error, task) {
    self.errors(error);
    self.printTaskLine(task, {quickid: false});
  });
};

CLI.prototype.discuss = function (option, item, message, options) {
  var self = this,
      deferred = q.defer();

  if (option == 'add') {
    this.ralio.addComment(this.fetchID(item), message, function(error, data) {
      self.errors(error);
      if (typeof data.PostNumber === "undefined") {
        deferred.reject();
      } else {
        deferred.resolve();
      }
    });
  } else if (option == 'show') {
    deferred.resolve();
  } else {
    return self.errors('Option to discuss command must be one of: show, add.');
  }

  deferred.promise.then(function() {
    self.ralio.comments(self.fetchID(item), function (error, comments) {
      self.errors(error);
      self.printTaskLine(comments.artifact, {quickid: false, tab: false});

      for (var i in comments.comments) {
        self.printDiscussionLine(comments.comments[i]);
      }

      if (comments.comments.length === 0) {
        return self.errors('There is no discussion going on here :-)');
      }
    });
  });
}

CLI.prototype.abandon = function (task) {
  var self = this;
  this.ralio.setTaskState(this.fetchID(task), {state: 'Defined', own: false, blocked: false, pair: ""}, function (error, task) {
    self.errors(error);
    self.printTaskLine(task, {quickid: false});
  });
};

CLI.prototype.block = function (task) {
  var self = this;
  this.ralio.setTaskState(this.fetchID(task), {blocked: true}, function (error, task) {
    self.errors(error);
    self.printTaskLine(task, {quickid: false});
  });
};

CLI.prototype.unblock = function (task) {
  var self = this;
  this.ralio.setTaskState(this.fetchID(task), {blocked: false}, function (error, task) {
    self.errors(error);
    self.printTaskLine(task, {quickid: false});
  });
};

CLI.prototype.point = function (story, points) {
  var self = this;
  this.ralio.point(this.fetchID(story), points, function (error, story) {
    self.errors(error);
    self.printStoryLine(story, {quickid: false});
  });
};

CLI.prototype.time = function (task, hours) {
    var self = this;
    this.ralio.time(this.fetchID(task), hours, function(error, data) {
        self.errors(error);
        if (data.Hours) {
          console.log("Task " + task.yellow + " timesheet entry is now " + data.Hours.toString().yellow + " hours.");
        } else {
          console.log(data);
        }
    });
}

CLI.prototype.task = function (option, story, opts) {
  var self = this,
      team = self.config.team,
      project = opts.project || self.config.project,
      projectName = project || team,
      tags = opts.tags ? opts.tags.split(',') : [];

  this.ralio.task(option, projectName, this.fetchID(story), opts.name, tags, function (error, option, story, taskname) {
    self.errors(error);
    self.printStoryLine(story, {quickid: false});
  });
};

CLI.prototype.errors = function (error) {
  if (error !== null) {
    error = typeof error === "object" ? error.message : error
    console.log(error.red);
    process.exit(1);
  }
};

CLI.prototype.isInProgress = function(artifact){
  return (artifact.State == "In-Progress" || artifact.ScheduleState === "In-Progress");
};

CLI.prototype.printStoryLine = function (story, options) {
  var defaults = {state: true, owners: false, tags: true, quickid: true};
  options = _.extend({}, defaults, options || {});
  var fields = [];
  if (options.quickid) {
    fields.push(this.quickID(story));
  }
  fields.push(story.FormattedID.yellow);
  fields.push((story.PlanEstimate || '-').toString().blue);
  if (options.state) {
    var state = STATES[story.ScheduleState];
    if (typeof state === "string") {
      fields.push(story.Blocked ? state.red : state.green);
    }
  }
  fields.push(story.Name);
  if (options.owners) {
    var owners = [], cli = this;
    story.Tasks.forEach(function (task) {
      if (cli.isInProgress(task) && task.Owner !== null) {
        owners.push(task.Owner._refObjectName);
      }
    });
    fields.push(_.compact(owners).join(', ').blue);
  }
  
  if (options.tags) {
    var tags = _.map(_.compact(story.Tags), function (t) { return t.Name }).join(', ');
    fields.push(this.isInProgress(story) ? tags.magenta : tags.grey);
  }

  console.log(fields.join(' '));
};

CLI.prototype.printTaskLine = function (task, options) {
  var defaults = {quickid: true, tab: true, tags: true, points: false};
  options = _.extend({}, defaults, options || {});
  
  var taskState = TASKSTATES[task.ScheduleState || task.State] || '.',
      owner = task.Owner !== null ? "❙ " + task.Owner._refObjectName : '',
      fields = [],
      pair = (task.Pair && task.Pair !== "" && task.Pair !== "true" && task.Pair !== null) ? "& " + task.Pair : null;

  if (options.quickid)
    fields.push(this.quickID(task));
  
  if (options.tab)
    fields.push(' ');

  fields.push(task.FormattedID.yellow);
  if (options.points)
    fields.push((task.PlanEstimate || '-').toString().blue);

  fields.push(task.Blocked ? taskState.red : taskState.green);
  fields.push(task.Name);
  
  fields.push(this.isInProgress(task) ? owner.blue : owner);  
  
  if (pair) {
    fields.push(this.isInProgress(task) ? pair.blue : pair);
  }

  if (options.tags) {
    var tags = _.map(_.compact(task.Tags), function (t) { return t.Name }).join(', ');
    fields.push(this.isInProgress(task) ? tags.magenta : tags.grey);
  }

  console.log(fields.join(' '));
};

CLI.prototype.printDiscussionLine = function (comment, options) {
  var defaults = {number: true, tab: true};
  options = _.extend({}, defaults, options || {});

  var owner = comment.User !== null ? comment.User._refObjectName + " ❙" : '',
      post = '' + (comment.PostNumber + 1),
      fields = [];

  if (options.tab)
    fields.push(' ');

  if (options.number)
    fields.push(post.yellow);

  fields.push(owner.blue);
  fields.push(comment.Text);

  console.log(fields.join(' '));
};

CLI.prototype.quickID = function (task_or_story) {
  if (this.task_ids === undefined) {
    this.task_ids = [];
  }
  this.task_ids.push(task_or_story.FormattedID);
  return this.rpad(this.task_ids.length, 3);
};

CLI.prototype.rpad = function (num, cols) {
  var ret = num.toString();
  while (ret.length < cols) {
    ret = ' ' + ret;
  }
  return ret;
};

CLI.prototype.saveQuickIDs = function () {
  if(this.task_ids !== undefined) {
    fs.writeFileSync(
      path.join(process.env.HOME, '.ralio_recent'),
      JSON.stringify(this.task_ids)
    );
  }
};

CLI.prototype.fetchID = function (id) {
  if (id.match(/^\d+$/)) {
    var quick_idx = parseInt(id, 10);
    if (this.quickIDs !== undefined && quick_idx <= this.quickIDs.length) {
      return this.quickIDs[quick_idx - 1];
    }
    else {
      console.error(('No quick ID ' + id + ' found').red);
      process.exit(1);
    }
  }
  return id;
}

var STATES = {
  "Suggestion": "?",
  "Defined": "·",
  "Submitted": "·",
  "In-Progress": "▸",
  "Completed": "✔",
  "Fixed": "✔",
  "Closed": "✔",
  "Accepted": "✩",
  "Released": "✮",
  "Grooming": "⌚"
};

var TASKSTATES = _.extend(STATES, {
  "Submitted": "·",
  "Defined": "·",
  "Open": "·",
  "In-Progress": "▸",
  "Completed": "✔",
  "Fixed": "✔",
  "Closed": "✔",
});

module.exports = CLI;


<|start_filename|>test/ralio_test.js<|end_filename|>
var assert = require('assert'),
    nock = require('nock'),
    sinon = require('sinon'),
    Ralio = require('../lib/ralio');

require('sinon-mocha').enhance(sinon);

var RALLY_HOST = "rally1.rallydev.com",
    RALLY_SERVER = "https://" + RALLY_HOST,
    RALLY_BULK_PATH = "/slm/webservice/1.42/adhoc.js";

describe('Ralio', function () {

  before(function () {
    Ralio.test = true;
  });

  beforeEach(function () {
    this.ralio = new Ralio(RALLY_SERVER, 'user1', '<PASSWORD>');
  });

  afterEach(function () {
    nock.cleanAll();
  });

  describe('#prefixOf', function () {
    it('should match single character prefixes', function () {
      assert.equal(
        this.ralio.prefixOf('S12345'),
        'S'
      );
    });

    it('should match two character prefixes', function () {
      assert.equal(
        this.ralio.prefixOf('US12345'),
        'US'
      );
    });
  });

  describe('#rallyWsapiUrl', function () {
     it('should insert auth credentials', function () {
      var url = 'https://user1:password1@' + RALLY_HOST;
      var len = url.length;
       assert.equal(
        this.ralio.rallyWsapiUrl().substring(0, len),
        url);
    });
  });

  describe('#artifact', function () {
    it('should fetch the artifact with the given ForamttedID', function (done) {
      var domainObject = sinon.stub(this.ralio, 'domainObject').yields(null, {
        FormattedID: 'US4321', Name: 'Test Story'
      });

      this.ralio.artifact('US4321', {}, {}, function (error, artifact) {
        assert.equal(error, null);
        assert.deepEqual(artifact, {FormattedID: 'US4321', Name: 'Test Story'});
        done();
      });
     });

    it('should fetch the artifact with the given ForamttedID and its typedef', function (done) {
      var domainObject = sinon.stub(this.ralio, 'domainObject').yields(null, {
        FormattedID: 'US4321', Name: 'Test Story'
      });

      var typedef = sinon.stub(this.ralio, 'typedef').yields(null, {
        type: 'hierarchicalrequirement'
      });

      this.ralio.artifact('US4321', {}, {typeDefinition: true}, function (error, artifact) {
        assert.equal(error, null);
        assert.deepEqual(artifact, {FormattedID: 'US4321', Name: 'Test Story', _typeDefinition:{type:'hierarchicalrequirement'}});
        done();
      });
    });

    it('should yield an error when no artifact is found with the given ForamttedID', function (done) {
      var domainObject = sinon.stub(this.ralio, 'domainObject').yields('Not found.');

      this.ralio.artifact('US4321', {}, {}, function (error, artifact) {
        assert.equal(error, 'Not found.');
        done();
      });
    });

   });

  describe('#bulk', function () {
    it('should send bulk queries', function (done) {
      var request = {
        foo: {},
        bar: '/bar',
        baz: {bap: '1,2,3', bop: 'bop'}
      };
      var expectedBody = {
        foo: '/foo?',
        bar: '/bar',
        baz: '/baz?bap=1%2C2%2C3&bop=bop'
      };
      nock(RALLY_SERVER)
        .post(RALLY_BULK_PATH, expectedBody)
        .reply(200, {foo: {bar: 'baz'}});
      this.ralio.bulk(request, function (error, data) {
        assert.equal(error, null);
        assert.deepEqual(data, {foo: {bar: 'baz'}});
        done();
      });
    });

    it('should catch HTTP errors', function (done) {
      var request = {foo: {}};
      var expectedBody = {foo: '/foo?'};
      nock(RALLY_SERVER)
        .post(RALLY_BULK_PATH, expectedBody)
        .reply(500, 'OMG BAD THINGS');
      this.ralio.bulk(request, function (error, data) {
        assert.equal(error, 'OMG BAD THINGS');
        done();
      });
    });
  });

  describe('#update', function () {
    it('should send updates', function (done) {
      nock('https://example.com')
        .post('/ref', {name: 'foo', desc: 'bar'})
        .reply(200, {OperationResult: {Errors: []}});
      this.ralio.update(
        'https://example.com/ref',
        {name: 'foo', desc: 'bar'},
        function (error) {
          assert.equal(error, null);
          done();
        }
      );
    });

    it('should catch HTTP errors', function (done) {
      nock('https://example.com')
        .post('/ref', {name: 'foo', desc: 'bar'})
        .reply(400, "IT WENT WRONG");
      this.ralio.update(
        'https://example.com/ref',
        {name: 'foo', desc: 'bar'},
        function (error) {
          assert.equal(error, "IT WENT WRONG");
          done();
        }
      );
    });

    it('should catch Rally operation errors', function (done) {
      nock('https://example.com')
        .post('/ref', {name: 'foo', desc: 'bar'})
        .reply(200, {OperationResult: {Errors: ['Bad romance']}});
      this.ralio.update(
        'https://example.com/ref',
        {name: 'foo', desc: 'bar'},
        function (error) {
          assert.deepEqual(error, ["Bad romance"]);
          done();
        }
      );
    });
  });

  describe('#backlog', function () {
    it('should fetch the backlog stories for a given project', function (done) {
      var query = {
        fetch: 'Name,FormattedID,Rank,PlanEstimate,Tags',
        order: 'Rank',
        query: '((Project.Name = "project1") AND (Iteration = NULL))',
        pagesize: 16
      };
      var query_result = {
        hierarchicalrequirement: {
          Results: [
            { FormattedID: 'US0000', Rank: 50 },
            { FormattedID: 'US0001', Rank: 52 },
            { FormattedID: 'US0002', Rank: 48 },
            { FormattedID: 'US0003', Rank: 54 },
            { FormattedID: 'US0004', Rank: 46 },
            { FormattedID: 'US0005', Rank: 56 }
          ]
        },
        defect: {
          Results: [
            { FormattedID: 'DE0000', Rank: 51 },
            { FormattedID: 'DE0001', Rank: 53 },
            { FormattedID: 'DE0002', Rank: 49 },
            { FormattedID: 'DE0003', Rank: 55 },
            { FormattedID: 'DE0004', Rank: 47 },
            { FormattedID: 'DE0005', Rank: 57 }
          ]
        }
      }
      var ex = sinon.mock(this.ralio).expects('bulk').once()
        .withArgs({ hierarchicalrequirement: query, defect: query });

      this.ralio.backlog({projectName: 'project1', pagesize: 16}, function (error, stories) {
        assert.equal(error, null);
        assert.deepEqual(stories, [
          { FormattedID: 'US0004', Rank: 46 },
          { FormattedID: 'DE0004', Rank: 47 },
          { FormattedID: 'US0002', Rank: 48 },
          { FormattedID: 'DE0002', Rank: 49 },
          { FormattedID: 'US0000', Rank: 50 },
          { FormattedID: 'DE0000', Rank: 51 },
          { FormattedID: 'US0001', Rank: 52 },
          { FormattedID: 'DE0001', Rank: 53 },
          { FormattedID: 'US0003', Rank: 54 },
          { FormattedID: 'DE0003', Rank: 55 },
          { FormattedID: 'US0005', Rank: 56 },
          { FormattedID: 'DE0005', Rank: 57 }
        ])
        done();
      });

      ex.yield(null, query_result);
    });

    it('should fetch the backlog stories for the current project filtered by a tag', function (done) {
      var query = {
        fetch: 'Name,FormattedID,Rank,PlanEstimate,Tags',
        order: 'Rank',
        query: '((Project.Name = "project1") AND ((Iteration = NULL) AND (Tags.Name = "tag1")))',
        pagesize: 16
      };
      var query_result = {
        hierarchicalrequirement: { Results: [] },
        defect: { Results: [] }
      }
      var ex = sinon.mock(this.ralio).expects('bulk').once()
        .withArgs({ hierarchicalrequirement: query, defect: query });

      this.ralio.backlog({projectName: 'project1', pagesize: 16, tag: 'tag1'}, function (error, stories) {
        assert.equal(error, null);
        assert.deepEqual(stories, [])
        done();
      });

      ex.yield(null, query_result);
    });
  });

  describe('#sprint', function () {
    it('should fetch the sprint stories for a given project and a given filter', function (done) {
      var query = {
        fetch: 'Name,FormattedID,Rank,PlanEstimate,ScheduleState,Tasks,Tags,Pair,Defects,State,Owner,TaskIndex,Blocked',
        order: 'Rank',
        query: '((Project.Name = "project2") AND ((Iteration.StartDate <= "1970-01-01") AND ((Iteration.EndDate >= "1970-01-01") AND (Name contains "teste"))))',
        pagesize: 100
      };
      var result = {
        hierarchicalrequirement: {
          Results: [
            { FormattedID: 'US0000', Rank: 50, Tasks: [
                { FormattedID: 'TA0000', TaskIndex: 3 },
                { FormattedID: 'TA0001', TaskIndex: 1 },
                { FormattedID: 'TA0002', TaskIndex: 2 },
              ] },
            { FormattedID: 'US0001', Rank: 52, Tasks: [] },
          ]
        },
        defect: {
          Results: [
            { FormattedID: 'DE0000', Rank: 51, Tasks: [
                { FormattedID: 'TA0003', TaskIndex: 1 },
                { FormattedID: 'TA0004', TaskIndex: 11 },
              ] },
            { FormattedID: 'DE0001', Rank: 53, Tasks: [
                { FormattedID: 'TA0005', TaskIndex: 0 }
              ] },
          ]
        }
      }
      var mock = sinon.mock(this.ralio);
      mock.expects('date').once().returns('1970-01-01');
      var ex = mock.expects('bulk').once()
        .withArgs({hierarchicalrequirement: query, defect: query});

      this.ralio.sprint('project2', {find: "teste"}, function (error, stories) {
        assert.equal(error, null);
        assert.deepEqual(stories, [
          { FormattedID: 'US0000', Rank: 50, Tasks: [
              { FormattedID: 'TA0001', TaskIndex: 1 },
              { FormattedID: 'TA0002', TaskIndex: 2 },
              { FormattedID: 'TA0000', TaskIndex: 3 }
            ] },
          { FormattedID: 'DE0000', Rank: 51, Tasks: [
              { FormattedID: 'TA0003', TaskIndex: 1 },
              { FormattedID: 'TA0004', TaskIndex: 11 }
            ] },
          { FormattedID: 'US0001', Rank: 52, Tasks: [] },
          { FormattedID: 'DE0001', Rank: 53, Tasks: [
              { FormattedID: 'TA0005', TaskIndex: 0 }
            ] },
        ])
        done();
      });

      ex.yield(null, result);
    });
    it('should fetch the sprint stories for a given project', function (done) {
      var query = {
        fetch: 'Name,FormattedID,Rank,PlanEstimate,ScheduleState,Tasks,Tags,Pair,Defects,State,Owner,TaskIndex,Blocked',
        order: 'Rank',
        query: '((Project.Name = "project2") AND ((Iteration.StartDate <= "1970-01-01") AND (Iteration.EndDate >= "1970-01-01")))',
        pagesize: 100
      };
      var result = {
        hierarchicalrequirement: {
          Results: [
            { FormattedID: 'US0000', Rank: 50, Tasks: [
                { FormattedID: 'TA0000', TaskIndex: 3 },
                { FormattedID: 'TA0001', TaskIndex: 1 },
                { FormattedID: 'TA0002', TaskIndex: 2 },
              ] },
            { FormattedID: 'US0001', Rank: 52, Tasks: [] },
          ]
        },
        defect: {
          Results: [
            { FormattedID: 'DE0000', Rank: 51, Tasks: [
                { FormattedID: 'TA0003', TaskIndex: 1 },
                { FormattedID: 'TA0004', TaskIndex: 11 },
              ] },
            { FormattedID: 'DE0001', Rank: 53, Tasks: [
                { FormattedID: 'TA0005', TaskIndex: 0 }
              ] },
          ]
        }
      }
      var mock = sinon.mock(this.ralio);
      mock.expects('date').once().returns('1970-01-01');
      var ex = mock.expects('bulk').once()
        .withArgs({hierarchicalrequirement: query, defect: query});

      this.ralio.sprint('project2', {}, function (error, stories) {
        assert.equal(error, null);
        assert.deepEqual(stories, [
          { FormattedID: 'US0000', Rank: 50, Tasks: [
              { FormattedID: 'TA0001', TaskIndex: 1 },
              { FormattedID: 'TA0002', TaskIndex: 2 },
              { FormattedID: 'TA0000', TaskIndex: 3 }
            ] },
          { FormattedID: 'DE0000', Rank: 51, Tasks: [
              { FormattedID: 'TA0003', TaskIndex: 1 },
              { FormattedID: 'TA0004', TaskIndex: 11 }
            ] },
          { FormattedID: 'US0001', Rank: 52, Tasks: [] },
          { FormattedID: 'DE0001', Rank: 53, Tasks: [
              { FormattedID: 'TA0005', TaskIndex: 0 }
            ] },
        ])
        done();
      });

      ex.yield(null, result);
    });
  });

  describe('#story', function () {
    it('should return a error message when anything different of US or DE were passed', function(){
       this.ralio.story('XD321', function (error, story) {
          assert.equal(error, "Ralio's can only show detailed information of a defect(DE), story(US) or tasks(TA).");
       });
    });

    it('should fetch the defect with the given ID', function (done) {
      var ex = sinon.mock(this.ralio).expects('bulk').once()
        .withArgs({ defect: {
          fetch: 'Name,FormattedID,PlanEstimate,ScheduleState,Tasks,Tags,Pair,Defects,State,Owner,TaskIndex,Blocked,Project,ObjectID,Description',
          query: '(FormattedID = "DE4321")'
        }});

      this.ralio.story('DE4321', function (error, story) {
        assert.equal(error, null);
        assert.deepEqual(story, {
          FormattedID: 'DE4321', Name: 'Test Story', Tasks: [
            { FormattedID: 'TA0001', TaskIndex: 1 },
            { FormattedID: 'TA0002', TaskIndex: 2 },
            { FormattedID: 'TA0000', TaskIndex: 3 }
          ]
        });
        done();
      });

      ex.yield(null, {
        defect: {
          Results: [
            { FormattedID: 'DE4321', Name: 'Test Story', Tasks: [
                { FormattedID: 'TA0001', TaskIndex: 1 },
                { FormattedID: 'TA0002', TaskIndex: 2 },
                { FormattedID: 'TA0000', TaskIndex: 3 }
              ] }
          ]
        }
      });
    });

    it('should fetch the user story with the given ID', function (done) {
      var ex = sinon.mock(this.ralio).expects('bulk').once()
        .withArgs({ hierarchicalrequirement: {
          fetch: 'Name,FormattedID,PlanEstimate,ScheduleState,Tasks,Tags,Pair,Defects,State,Owner,TaskIndex,Blocked,Project,ObjectID,Description',
          query: '(FormattedID = "US4321")'
        }});

      this.ralio.story('US4321', function (error, story) {
        assert.equal(error, null);
        assert.deepEqual(story, {
          FormattedID: 'US4321', Name: 'Test Story', Tasks: [
            { FormattedID: 'TA0001', TaskIndex: 1 },
            { FormattedID: 'TA0002', TaskIndex: 2 },
            { FormattedID: 'TA0000', TaskIndex: 3 }
          ]
        });
        done();
      });

      ex.yield(null, {
        hierarchicalrequirement: {
          Results: [
            { FormattedID: 'US4321', Name: 'Test Story', Tasks: [
                { FormattedID: 'TA0001', TaskIndex: 1 },
                { FormattedID: 'TA0002', TaskIndex: 2 },
                { FormattedID: 'TA0000', TaskIndex: 3 }
              ] }
          ]
        }
      });
    });

    it('should fetch the user task with the given ID', function (done) {
      var ex = sinon.mock(this.ralio).expects('bulk').once()
        .withArgs({ task: {
          fetch: 'Name,FormattedID,PlanEstimate,ScheduleState,Tasks,Tags,Pair,Defects,State,Owner,TaskIndex,Blocked,Project,ObjectID,Description',
          query: '(FormattedID = "TA4321")'
        }});

      this.ralio.story('TA4321', function (error, story) {
        assert.equal(error, null);
        assert.deepEqual(story, {
          FormattedID: 'TA4321', Name: '<NAME>', Tasks: []
        });
        done();
      });

      ex.yield(null, {
        task: {
          Results: [
            { FormattedID: 'TA4321', Name: '<NAME>', Tasks: []}
          ]
        }
      });
    });
  });

  describe('#setTaskState', function () {
    describe('should update the task with the given ID', function () {
      it('should update the owner ID if the flag is set', function (done) {
        var ralio_mock = sinon.mock(this.ralio);
        var bulk1 = ralio_mock.expects('bulk').withArgs({
          task: {fetch: true, query: '(FormattedID = "TA00001")'},
          user: {}
        });

        var update = ralio_mock.expects('update').withArgs('https://example.com/task', {
            Task: {
              Owner: 'https://example.com/user',
              State: 'In-Progress',
              ToDo: 1.0,
              Estimate: 1.0,
              _ref: 'https://example.com/task'
            }
          });

        var bulk2 = ralio_mock.expects('bulk').withArgs({
          task: {fetch: true, query: '(FormattedID = "TA00001")'},
          user: {}
        });

        this.ralio.setTaskState('TA00001', {state: 'In-Progress', own: true}, function (error, task) {
          assert.equal(error, null);
          assert.deepEqual(task, { 'FormattedID': 'TA00001' });
          done();
        });

        bulk1.yield(null, {
          user: { _ref: 'https://example.com/user' },
          task: { Results: [{ _ref: 'https://example.com/task' }] }
        });

        update.yield(null);
        bulk2.yield(null, { task: { Results: [{ 'FormattedID': 'TA00001' }] } });
      });

      it('should not update the owner ID if the flag is not set', function (done) {
        var ralio_mock = sinon.mock(this.ralio);
        var bulk1 = ralio_mock.expects('bulk').withArgs({
          user: {},
          task: {fetch: true, query: '(FormattedID = "TA00001")'}
        });

        var update = ralio_mock.expects('update').withArgs('https://example.com/task', {
            Task: {
              Owner: null,
              State: 'In-Progress',
              ToDo: 1.0,
              Estimate: 1.0,
              _ref: 'https://example.com/task'
            }
          });

        var bulk2 = ralio_mock.expects('bulk').withArgs({
          task: {fetch: true, query: '(FormattedID = "TA00001")'},
          user: {}
        });

        this.ralio.setTaskState('TA00001', {state: 'In-Progress'}, function (error, task) {
          assert.equal(error, null);
          assert.deepEqual(task, { 'FormattedID': 'TA00001' });
          done();
        });

        bulk1.yield(null, {
          user: { _ref: 'https://example.com/user' },
          task: { Results: [{ _ref: 'https://example.com/task' }] }
        });
        update.yield(null);
        bulk2.yield(null, { task: { Results: [{ 'FormattedID': 'TA00001' }] } });
      });

      it('should set the ToDo time to zero when the task state is Completed', function (done) {
        var ralio_mock = sinon.mock(this.ralio);
        var bulk1 = ralio_mock.expects('bulk').withArgs({
          user: {},
          task: {fetch: true, query: '(FormattedID = "TA00001")'}
        });

        var update = ralio_mock.expects('update').withArgs('https://example.com/task', {
            Task: {
              Owner: null,
              State: 'Completed',
              ToDo: 0.0,
              _ref: 'https://example.com/task'
            }
          });

        var bulk2 = ralio_mock.expects('bulk').withArgs({
          task: {fetch: true, query: '(FormattedID = "TA00001")'},
          user: {}
        });

        this.ralio.setTaskState('TA00001', {state: 'Completed'}, function (error, task) {
          assert.equal(error, null);
          assert.deepEqual(task, { 'FormattedID': 'TA00001' });
          done();
        });

        bulk1.yield(null, {
          user: { _ref: 'https://example.com/user' },
          task: { Results: [{ _ref: 'https://example.com/task' }] }
        });
        update.yield(null);
        bulk2.yield(null, { task: { Results: [{ 'FormattedID': 'TA00001' }] } });
      });

      it('should set the ToDo time to zero when the defect state is Fixed/Completed', function (done) {
        var ralio_mock = sinon.mock(this.ralio);
        var bulk1 = ralio_mock.expects('bulk').withArgs({
          user: {},
          defect: {fetch: true, query: '(FormattedID = "DE00001")'}
        });

        var update = ralio_mock.expects('update').withArgs('https://example.com/defect', {
            Defect: {
              Owner: null,
              ScheduleState: 'Completed',
              State: 'Fixed',
              ToDo: 0.0,
              _ref: 'https://example.com/defect'
            }
          });

        var bulk2 = ralio_mock.expects('bulk').withArgs({
          defect: {fetch: true, query: '(FormattedID = "DE00001")'},
          user: {}
        });

        this.ralio.setTaskState('DE00001', {state: 'Completed'}, function (error, defect) {
          assert.equal(error, null);
          assert.deepEqual(defect, { 'FormattedID': 'DE00001' });
          done();
        });

        bulk1.yield(null, {
          user: { _ref: 'https://example.com/user' },
          defect: { Results: [{ _ref: 'https://example.com/defect' }] }
        });

        update.yield(null);
        bulk2.yield(null, { defect: { Results: [{ 'FormattedID': 'DE00001' }] } });
      });

      it('should set the ToDo time to zero when the story state is Completed', function (done) {
        var ralio_mock = sinon.mock(this.ralio);
        var bulk1 = ralio_mock.expects('bulk').withArgs({
          user: {},
          hierarchicalrequirement: {fetch: true, query: '(FormattedID = "US0001")'}
        });

        var update = ralio_mock.expects('update').withArgs('https://example.com/story', {
            HierarchicalRequirement: {
              Owner: null,
              ScheduleState: 'Completed',
              ToDo: 0.0,
              _ref: 'https://example.com/story'
            }
          });

        var bulk2 = ralio_mock.expects('bulk').withArgs({
          hierarchicalrequirement: {fetch: true, query: '(FormattedID = "US0001")'},
          user: {}
        });

        this.ralio.setTaskState('US0001', {state: 'Completed'}, function (error, story) {
          assert.equal(error, null);
          assert.deepEqual(story, { 'FormattedID': 'US0001' });
          done();
        });

        bulk1.yield(null, {
          user: { _ref: 'https://example.com/user' },
          hierarchicalrequirement: { Results: [{ _ref: 'https://example.com/story' }] }
        });

        update.yield(null);
        bulk2.yield(null, { hierarchicalrequirement: { Results: [{ 'FormattedID': 'US0001' }] } });
      });

      it('should set the ToDo time to 1 when the story state is In-Progress', function (done) {
        var ralio_mock = sinon.mock(this.ralio);
        var bulk1 = ralio_mock.expects('bulk').withArgs({
          user: {},
          hierarchicalrequirement: {fetch: true, query: '(FormattedID = "US0001")'}
        });

        var update = ralio_mock.expects('update').withArgs('https://example.com/story', {
            HierarchicalRequirement: {
              Owner: null,
              ScheduleState: 'In-Progress',
              ToDo: 1.0,
              Estimate: 1.0,
              _ref: 'https://example.com/story'
            }
          });

        var bulk2 = ralio_mock.expects('bulk').withArgs({
          hierarchicalrequirement: {fetch: true, query: '(FormattedID = "US0001")'},
          user: {}
        });

        this.ralio.setTaskState('US0001', {state: 'In-Progress'}, function (error, story) {
          assert.equal(error, null);
          assert.deepEqual(story, { 'FormattedID': 'US0001' });
          done();
        });

        bulk1.yield(null, {
          user: { _ref: 'https://example.com/user' },
          hierarchicalrequirement: { Results: [{ _ref: 'https://example.com/story' }] }
        });

        update.yield(null);
        bulk2.yield(null, { hierarchicalrequirement: { Results: [{ 'FormattedID': 'US0001' }] } });
      });

      it('should set the ToDo time to 1 when the defect state is Open/Defined', function (done) {
        var ralio_mock = sinon.mock(this.ralio);
        var bulk1 = ralio_mock.expects('bulk').withArgs({
          user: {},
          defect: {fetch: true, query: '(FormattedID = "DE00001")'}
        });

        var update = ralio_mock.expects('update').withArgs('https://example.com/defect', {
            Defect: {
              Owner: null,
              ScheduleState: "In-Progress",
              State: 'Open',
              ToDo: 1.0,
              Estimate: 1.0,
              _ref: 'https://example.com/defect'
            }
          });

        var bulk2 = ralio_mock.expects('bulk').withArgs({
          defect: {fetch: true, query: '(FormattedID = "DE00001")'},
          user: {}
        });

        this.ralio.setTaskState('DE00001', {state: 'In-Progress'}, function (error, defect) {
          assert.equal(error, null);
          assert.deepEqual(defect, { 'FormattedID': 'DE00001' });
          done();
        });

        bulk1.yield(null, {
          user: { _ref: 'https://example.com/user' },
          defect: { Results: [{ _ref: 'https://example.com/defect' }] }
        });

        update.yield(null);
        bulk2.yield(null, { defect: { Results: [{ 'FormattedID': 'DE00001' }] } });
      });

      it('should set the ToDo time to 1 when the task state is Completed', function (done) {
        var ralio_mock = sinon.mock(this.ralio);
        var bulk1 = ralio_mock.expects('bulk').withArgs({
          user: {},
          task: {fetch: true, query: '(FormattedID = "TA00001")'}
        });

        var update = ralio_mock.expects('update').withArgs('https://example.com/task', {
            Task: {
              Owner: null,
              State: 'In-Progress',
              ToDo: 1.0,
              Estimate: 1.0,
              _ref: 'https://example.com/task'
            }
          });

        var bulk2 = ralio_mock.expects('bulk').withArgs({
          task: {fetch: true, query: '(FormattedID = "TA00001")'},
          user: {}
        });

        this.ralio.setTaskState('TA00001', {state: 'In-Progress'}, function (error, task) {
          assert.equal(error, null);
          assert.deepEqual(task, { 'FormattedID': 'TA00001' });
          done();
        });

        bulk1.yield(null, {
          user: { _ref: 'https://example.com/user' },
          task: { Results: [{ _ref: 'https://example.com/task' }] }
        });
        update.yield(null);
        bulk2.yield(null, { task: { Results: [{ 'FormattedID': 'TA00001' }] } });
      });

    });

    it('should block story when block param is true', function (done) {
      var ralio_mock = sinon.mock(this.ralio);
      var bulk1 = ralio_mock.expects('bulk').withArgs({
        user: {},
        hierarchicalrequirement: {fetch: true, query: '(FormattedID = "US0001")'}
      });

      var update = ralio_mock.expects('update').withArgs('https://example.com/story', {
          HierarchicalRequirement: {
            Owner: null,
            ScheduleState: 'In-Progress',
            ToDo: 1.0,
            Estimate: 1.0,
            Blocked: true,
            _ref: 'https://example.com/story'
          }
        });

      var bulk2 = ralio_mock.expects('bulk').withArgs({
        hierarchicalrequirement: {fetch: true, query: '(FormattedID = "US0001")'},
        user: {}
      });

      this.ralio.setTaskState('US0001', {state: 'In-Progress', blocked: true}, function (error, story) {
        assert.equal(error, null);
        assert.deepEqual(story, { 'FormattedID': 'US0001' });
        done();
      });

      bulk1.yield(null, {
        user: { _ref: 'https://example.com/user' },
        hierarchicalrequirement: { Results: [{ _ref: 'https://example.com/story' }] }
      });

      update.yield(null);
      bulk2.yield(null, { hierarchicalrequirement: { Results: [{ 'FormattedID': 'US0001' }] } });
    });

    it('should block defect when block param is true', function (done) {
      var ralio_mock = sinon.mock(this.ralio);
      var bulk1 = ralio_mock.expects('bulk').withArgs({
        user: {},
        defect: {fetch: true, query: '(FormattedID = "DE00001")'}
      });

      var update = ralio_mock.expects('update').withArgs('https://example.com/defect', {
          Defect: {
            Owner: null,
            ScheduleState: "In-Progress",
            State: 'Open',
            ToDo: 1.0,
            Estimate: 1.0,
            Blocked: true,
            _ref: 'https://example.com/defect'
          }
        });

      var bulk2 = ralio_mock.expects('bulk').withArgs({
        defect: {fetch: true, query: '(FormattedID = "DE00001")'},
        user: {}
      });

      this.ralio.setTaskState('DE00001', {state: 'In-Progress', blocked: true}, function (error, defect) {
        assert.equal(error, null);
        assert.deepEqual(defect, { 'FormattedID': 'DE00001' });
        done();
      });

      bulk1.yield(null, {
        user: { _ref: 'https://example.com/user' },
        defect: { Results: [{ _ref: 'https://example.com/defect' }] }
      });

      update.yield(null);
      bulk2.yield(null, { defect: { Results: [{ 'FormattedID': 'DE00001' }] } });
    });

    it('should block task when block param is true', function (done) {
      var ralio_mock = sinon.mock(this.ralio);
      var bulk1 = ralio_mock.expects('bulk').withArgs({
        user: {},
        task: {fetch: true, query: '(FormattedID = "TA00001")'}
      });

      var update = ralio_mock.expects('update').withArgs('https://example.com/task', {
          Task: {
            Owner: null,
            State: 'In-Progress',
            ToDo: 1.0,
            Estimate: 1.0,
            Blocked: true,
            _ref: 'https://example.com/task'
          }
        });

      var bulk2 = ralio_mock.expects('bulk').withArgs({
        task: {fetch: true, query: '(FormattedID = "TA00001")'},
        user: {}
      });

      this.ralio.setTaskState('TA00001', {state: 'In-Progress', blocked: true}, function (error, task) {
        assert.equal(error, null);
        assert.deepEqual(task, { 'FormattedID': 'TA00001' });
        done();
      });

      bulk1.yield(null, {
        user: { _ref: 'https://example.com/user' },
        task: { Results: [{ _ref: 'https://example.com/task' }] }
      });
      update.yield(null);
      bulk2.yield(null, { task: { Results: [{ 'FormattedID': 'TA00001' }] } });
    });

    it('should unblock story when block param is false', function (done) {
      var ralio_mock = sinon.mock(this.ralio);
      var bulk1 = ralio_mock.expects('bulk').withArgs({
        user: {},
        hierarchicalrequirement: {fetch: true, query: '(FormattedID = "US0001")'}
      });

      var update = ralio_mock.expects('update').withArgs('https://example.com/story', {
          HierarchicalRequirement: {
            Owner: null,
            ScheduleState: 'In-Progress',
            ToDo: 1.0,
            Estimate: 1.0,
            Blocked: false,
            _ref: 'https://example.com/story'
          }
        });

      var bulk2 = ralio_mock.expects('bulk').withArgs({
        hierarchicalrequirement: {fetch: true, query: '(FormattedID = "US0001")'},
        user: {}
      });

      this.ralio.setTaskState('US0001', {state: 'In-Progress', blocked: false}, function (error, story) {
        assert.equal(error, null);
        assert.deepEqual(story, { 'FormattedID': 'US0001' });
        done();
      });

      bulk1.yield(null, {
        user: { _ref: 'https://example.com/user' },
        hierarchicalrequirement: { Results: [{ _ref: 'https://example.com/story' }] }
      });

      update.yield(null);
      bulk2.yield(null, { hierarchicalrequirement: { Results: [{ 'FormattedID': 'US0001' }] } });
    });

    it('should unblock defect when block param is false', function (done) {
      var ralio_mock = sinon.mock(this.ralio);
      var bulk1 = ralio_mock.expects('bulk').withArgs({
        user: {},
        defect: {fetch: true, query: '(FormattedID = "DE00001")'}
      });

      var update = ralio_mock.expects('update').withArgs('https://example.com/defect', {
          Defect: {
            Owner: null,
            ScheduleState: 'In-Progress',
            State: 'Open',
            ToDo: 1.0,
            Estimate: 1.0,
            Blocked: false,
            _ref: 'https://example.com/defect'
          }
        });

      var bulk2 = ralio_mock.expects('bulk').withArgs({
        defect: {fetch: true, query: '(FormattedID = "DE00001")'},
        user: {}
      });

      this.ralio.setTaskState('DE00001', {state: 'In-Progress', blocked: false}, function (error, defect) {
        assert.equal(error, null);
        assert.deepEqual(defect, { 'FormattedID': 'DE00001' });
        done();
      });

      bulk1.yield(null, {
        user: { _ref: 'https://example.com/user' },
        defect: { Results: [{ _ref: 'https://example.com/defect' }] }
      });

      update.yield(null);
      bulk2.yield(null, { defect: { Results: [{ 'FormattedID': 'DE00001' }] } });
    });

    it('should unblock task when block param is false', function (done) {
      var ralio_mock = sinon.mock(this.ralio);
      var bulk1 = ralio_mock.expects('bulk').withArgs({
        user: {},
        task: {fetch: true, query: '(FormattedID = "TA00001")'}
      });

      var update = ralio_mock.expects('update').withArgs('https://example.com/task', {
          Task: {
            Owner: null,
            State: 'In-Progress',
            ToDo: 1.0,
            Estimate: 1.0,
            Blocked: false,
            _ref: 'https://example.com/task'
          }
        });

      var bulk2 = ralio_mock.expects('bulk').withArgs({
        task: {fetch: true, query: '(FormattedID = "TA00001")'},
        user: {}
      });

      this.ralio.setTaskState('TA00001', {state: 'In-Progress', blocked: false}, function (error, task) {
        assert.equal(error, null);
        assert.deepEqual(task, { 'FormattedID': 'TA00001' });
        done();
      });

      bulk1.yield(null, {
        user: { _ref: 'https://example.com/user' },
        task: { Results: [{ _ref: 'https://example.com/task' }] }
      });
      update.yield(null);
      bulk2.yield(null, { task: { Results: [{ 'FormattedID': 'TA00001' }] } });
    });

    it('should set root cause and resolution when is passed on a defect', function (done) {
      var ralio_mock = sinon.mock(this.ralio);
      var bulk1 = ralio_mock.expects('bulk').withArgs({
        user: {},
        defect: {fetch: true, query: '(FormattedID = "DE00001")'}
      });

      var update = ralio_mock.expects('update').withArgs('https://example.com/defect', {
          Defect: {
            Owner: null,
            ScheduleState: 'In-Progress',
            State: 'Open',
            ToDo: 1.0,
            Estimate: 1.0,
            RootCause: "Code Design/Error",
            Resolution: "Code Change",
            _ref: 'https://example.com/defect'
          }
        });

      var bulk2 = ralio_mock.expects('bulk').withArgs({
        defect: {fetch: true, query: '(FormattedID = "DE00001")'},
        user: {}
      });

      this.ralio.setTaskState('DE00001', {state: 'In-Progress', rootcause: "Code Design/Error", resolution: "Code Change"}, function (error, defect) {
        assert.equal(error, null);
        assert.deepEqual(defect, { 'FormattedID': 'DE00001' });
        done();
      });

      bulk1.yield(null, {
        user: { _ref: 'https://example.com/user' },
        defect: { Results: [{ _ref: 'https://example.com/defect' }] }
      });

      update.yield(null);
      bulk2.yield(null, { defect: { Results: [{ 'FormattedID': 'DE00001' }] } });
    });
  });

  describe('#current', function () {
    it('should fetch all the stories that the current user is working on', function (done) {
      var query = {
        fetch: 'Name,FormattedID,Rank,PlanEstimate,ScheduleState,Tasks,Tags,Pair,State,Owner,TaskIndex,Blocked',
        order: 'Rank',
        query: '((Project.Name = "project3") AND ((Iteration.StartDate <= "1970-01-01") AND (Iteration.EndDate >= "1970-01-01")))',
        pagesize: 100
      };
      var result = {
        user: { _ref: 'https://example.com/user' },
        hierarchicalrequirement: {
          Results: [
            { FormattedID: 'US0000', Rank: 50, Tasks: [
                { FormattedID: 'TA0000', TaskIndex: 3, State: 'Completed', Owner: { _ref: 'https://example.com/user' } },
                { FormattedID: 'TA0001', TaskIndex: 1, State: 'Completed', Owner: { _ref: 'https://example.com/user' } },
                { FormattedID: 'TA0002', TaskIndex: 2, State: 'Completed', Owner: { _ref: 'https://example.com/user' } },
              ] },
            { FormattedID: 'US0001', Rank: 52, Tasks: [] },
          ]
        },
        defect: {
          Results: [
            { FormattedID: 'DE0000', Rank: 51, State: 'Open', Owner: null, Tasks: [
                { FormattedID: 'TA0003', TaskIndex: 1, State: 'In-Progress', Owner: null },
                { FormattedID: 'TA0004', TaskIndex: 11, State: 'Defined', Owner: null },
              ] },
            { FormattedID: 'DE0001', Rank: 53, State: 'Open', Owner: { _ref: 'https://example.com/user' }, Tasks: [
                { FormattedID: 'TA0005', TaskIndex: 0, State: 'In-Progress', Owner: { _ref: 'https://example.com/user' } }
              ] },
          ]
        }
      }
      var mock = sinon.mock(this.ralio);
      mock.expects('date').once().returns('1970-01-01');
      var ex = mock.expects('bulk').once().withArgs({ user: {}, hierarchicalrequirement: query, defect: query });

      this.ralio.current('project3', function (error, stories) {
        assert.equal(error, null);

        assert.deepEqual(stories, [
          { FormattedID: 'DE0001', Rank: 53, State: 'Open', Owner: { _ref: 'https://example.com/user' }, Tasks: [
              { FormattedID: 'TA0005', TaskIndex: 0, State: 'In-Progress', Owner: { _ref: 'https://example.com/user' } }
            ] },
        ])
        done();
      });

      ex.yield(null, result);
    });
  });

  describe('#block', function () {
    it('should block the given task if the block flag is true');
    it('should unblock the given task if the block flag is false');
  });

  describe('#point', function () {
    it('should return an error when anything different then US or DE', function(){
      this.ralio.point('XD321', 5, function (error, story) {
          assert.equal(error, "You can only point a Defect (DE) or a Story (US)");
       });
    });

    it('should update the given defect with the given points value', function (done) {
      var ralio_mock = sinon.mock(this.ralio);
      var bulk1 = ralio_mock.expects('bulk').withArgs({
        defect: {fetch: true, query: '(FormattedID = "DE0001")'}, user: {}
      });

      var update = ralio_mock.expects('update').withArgs('https://example.com/defect', {
        Defect: {
          PlanEstimate: 5,
          _ref: 'https://example.com/defect'
        }
      });

      var bulk2 = ralio_mock.expects('bulk').withArgs({
        defect: {fetch: true, query: '(FormattedID = "DE0001")'}, user: {}
      });

      this.ralio.point('DE0001', 5, function (error, story) {
        assert.equal(error, null);
        assert.deepEqual(story, { 'FormattedID': 'DE0001' });
        done();
      });

      bulk1.yield(null, {
        defect: { Results: [{ _ref: 'https://example.com/defect' }]}
      });
      update.yield(null);
      bulk2.yield(null, { defect: { Results: [{ 'FormattedID': 'DE0001' }] } });
    });

    it('should update the given story with the given points value', function (done) {
      var ralio_mock = sinon.mock(this.ralio);
      var bulk1 = ralio_mock.expects('bulk').withArgs({
        hierarchicalrequirement: {fetch: true, query: '(FormattedID = "US0001")'}, user: {}
      });

      var update = ralio_mock.expects('update').withArgs('https://example.com/story', {
        HierarchicalRequirement: {
          PlanEstimate: 5,
          _ref: 'https://example.com/story'
        }
      });

      var bulk2 = ralio_mock.expects('bulk').withArgs({
        hierarchicalrequirement: {fetch: true, query: '(FormattedID = "US0001")'}, user: {}
      });

      this.ralio.point('US0001', 5, function (error, story) {
        assert.equal(error, null);
        assert.deepEqual(story, { 'FormattedID': 'US0001' });
        done();
      });

      bulk1.yield(null, {
        hierarchicalrequirement: { Results: [{ _ref: 'https://example.com/story' }]}
      });
      update.yield(null);
      bulk2.yield(null, { hierarchicalrequirement: { Results: [{ 'FormattedID': 'US0001' }] } });
    });
  });

describe('#task', function() {
  describe('#getTag', function(){
    it('should return a tag object', function(done){
      var ralio_mock = sinon.mock(this.ralio);

       var options = {
          tag: {fetch: true, query: '(Name = "BLUE TASK")'}
        };

       var mock_request = ralio_mock.expects('bulk').withArgs(options);

       this.ralio.getTag('BLUE TASK', function(error, resource) {
        assert.equal(error, null);
        assert.deepEqual(resource, {'_ref': 'https://example.com/task'});
        done();
       });

       // mocked returns
       mock_request.yield(null, {'tag': {'TotalResultCount': 1, 'Results': [{'_ref': 'https://example.com/task'}]}});

    });

    it('should return tag not found', function(done){
      var ralio_mock = sinon.mock(this.ralio);

       var options = {
          tag: {fetch: true, query: '(Name = "UNKNOW TASK")'}
        };

       var mock_request = ralio_mock.expects('bulk').withArgs(options);

       this.ralio.getTag('UNKNOW TASK', function(error, resource) {
        assert.equal(error, 'Tag UNKNOW TASK Not Found!');
        assert.deepEqual(resource, undefined);
        done();
       });

       // mocked returns
       mock_request.yield(null, {'tag': {'TotalResultCount': 0, 'Results': []}});

    });
  });

  describe('getTask Method', function(){
    it('should return task object when a task id was given', function(done) {
       var ralio_mock = sinon.mock(this.ralio);

       var options = {
          user: {},
          task: {query: '(FormattedID = "TA1234")'}
        }

       var mock_request = ralio_mock.expects('bulk').withArgs(options);

       this.ralio.getTask('TA1234', function(error, resource) {
        assert.equal(error, null);
        assert.deepEqual(resource, {'_ref': 'https://example.com/task'});
        done();
       });

       // mocked returns
       mock_request.yield(null, {'task': {'TotalResultCount': 1, 'Results': [{'_ref': 'https://example.com/task'}]}});
    });

    it('should return task not found if TotalResultCount is zero', function(done) {
      var ralio_mock = sinon.mock(this.ralio);

      var options = {
          user: {},
          task: {query: '(FormattedID = "TA1234")'}
        }

       var mock_request = ralio_mock.expects('bulk').withArgs(options);

       this.ralio.getTask('TA1234', function(error, resource) {
        assert.equal(error, 'Task Not Found!');
        assert.deepEqual(resource, null);
        done();
       });

       // mocked returns
       mock_request.yield(null, {'task': {'TotalResultCount': 0, 'Results': []}});
    });
    describe('createTask Method', function(){
      it('should create a task with success without tags', function(done){
        var clock = sinon.useFakeTimers();
        var ralio_mock = sinon.mock(this.ralio);
        var tag_request = ralio_mock.expects('getTags').withArgs([]);
        var story_request = ralio_mock.expects('story').withArgs('US1234');

        var options = {
          url: this.ralio.bulkUrl({"pathname":"/slm/webservice/1.42/task/create.js"}),
          method: 'POST',
          json: {
            Task: {
              Name: "<NAME>",
              Project: "https://example.com/project",
              WorkProduct: "https://example.com/story",
              ToDo: 1.0,
              Estimate: 1.0,
              Tags: []
            }
          },
          strictSSL: !Ralio.test
        };

        var mock_request = ralio_mock.expects('request').withArgs(options);

        this.ralio.createTask('hokage', 'US1234', 'my task name', [], function(var1, var2, data, taskName){
          assert.deepEqual(data, {});
          assert.equal(taskName, '<NAME> name');
        });

        clock.tick(300);

        done();
        tag_request.yield(null, []);
        story_request.yield(null, {'_ref':'https://example.com/story', 'Project': {'_ref': 'https://example.com/project'}});
        mock_request.yield(null, {'CreateResult':{'Object': {}}});
      });

      it('should create a task with success with tags', function(done){
        var clock = sinon.useFakeTimers();

        var ralio_mock = sinon.mock(this.ralio);
        var tag_request = ralio_mock.expects('getTags').withArgs(['BLUE TASK']);
        var story_request = ralio_mock.expects('story').withArgs('US1234');

        var options = {
          url: this.ralio.bulkUrl({"pathname":"/slm/webservice/1.42/task/create.js"}),
          method: 'POST',
          json: {
            Task: {
              Name: "<NAME>",
              Project: "https://example.com/project",
              WorkProduct: "https://example.com/story",
              ToDo: 1.0,
              Estimate: 1.0,
              Tags: [{'_ref':'https://example.com/tag/12312312.js'}]
            }
          },
          strictSSL: !Ralio.test
        };

        var mock_request = ralio_mock.expects('request').withArgs(options);

        this.ralio.createTask('hokage', 'US1234', 'my task name', ['BLUE TASK'], function(var1, var2, data, taskName){
          assert.deepEqual(data, {});
          assert.equal(taskName, 'my task name');
        });

        clock.tick(600);

        done();
        tag_request.yield(null, {'_ref':'https://example.com/tag/12312312.js'});
        story_request.yield(null, {'_ref':'https://example.com/story', 'Project': {'_ref': 'https://example.com/project'}});
        mock_request.yield(null, {'CreateResult':{'Object': {}}});
      });
    });

    describe('deleteTask Method', function(){
      it('should delete a task with success', function(done) {
        var ralio_mock = sinon.mock(this.ralio);
        var get_task = ralio_mock.expects('getTask').withArgs('TA1234');

        var options = {
          url: "https://user1:password1@rally1.rallydev.com/TA1234.js",
          method: 'DELETE',
          json: {},
          strictSSL: !Ralio.test
        };

        var mock_request = ralio_mock.expects('request').withArgs(options);

        this.ralio.deleteTask('hokage', 'TA1234', function(task) {
          assert(task, 'TA1234 deleted');
        });

        done();

        get_task.yield(null, {'_ref':'https://example.com/TA1234.js'})
        mock_request.yield(null, {});
      });
    });
  });

  describe('creating tasks', function(){
    it('should call the createTask method', function(){
      var ralio_mock = sinon.mock(this.ralio);
      var mock_request = ralio_mock.expects('createTask').withArgs('hokage', 'US1234', 'my new task');

      this.ralio.task('create', 'hokage', 'US1234', 'my new task', [], function(){});

      mock_request.yield(null);
    });

    it('should call the deleteTask method', function(){
      var ralio_mock = sinon.mock(this.ralio);
      var mock_request = ralio_mock.expects('deleteTask').withArgs('hokage', 'TA1234');

      this.ralio.task('delete', 'hokage', 'TA1234', null, [], function(){});

      mock_request.yield(null);
    });
  });
});

describe('#editor', function () {
  it('should callback with {success: true} when $EDITOR exit code is 0', function (done) {
    var _editor = sinon.stub(this.ralio, '_editor').yields(0, 0);

    this.ralio.editor("a phrase to edit", {}, function(result) {
      assert.equal(result.success, true);
      done();
    });
  });

  it('should callback with {success: false} when $EDITOR exit code is non-0', function (done) {
    var _editor = sinon.stub(this.ralio, '_editor').yields(-1, 0);

    this.ralio.editor("a phrase to edit", {}, function(result) {
      assert.equal(result.success, false);
      done();
    });
  });

});

});
