Introduced the state_machine gem to the project. State transitions are
now more controllable through declarative statements rather than through
tracking flags.
Refactored all unit and functional tests to accomodate the new state
machine.
Added new actions to handle moving sprints between the various states.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/report_controller.rb | 10 +-
app/controllers/sprints_controller.rb | 95 ++++++++---
app/models/sprint.rb | 103 +++++++------
app/models/task.rb | 2 +-
app/views/sprints/_edit.html.erb | 4 +-
app/views/sprints/index.html.erb | 8 +-
app/views/sprints/show.html.erb | 40 ++---
config/environment.rb | 1 +
config/routes.rb | 5 +-
db/migrate/011_create_sprints.rb | 4 +-
doc/ChangeLog | 1 +
test/fixtures/backlog_items.yml | 10 +-
test/fixtures/product_roles.yml | 7 +
test/fixtures/products.yml | 10 +-
test/fixtures/sprints.yml | 79 ++++++---
test/functional/items_controller_test.rb | 10 +-
test/functional/report_controller_test.rb | 10 +-
test/functional/sprints_controller_test.rb | 236 ++++++++++++++++++++--------
test/functional/user_items_test.rb | 16 +-
test/unit/backlog_item_test.rb | 22 ++--
test/unit/sprint_member_test.rb | 2 +-
test/unit/sprint_test.rb | 217 +++++++++++++-------------
test/unit/task_test.rb | 6 +-
23 files changed, 541 insertions(+), 357 deletions(-)
diff --git a/app/controllers/report_controller.rb b/app/controllers/report_controller.rb
index be9a85a..c70e895 100644
--- a/app/controllers/report_controller.rb
+++ b/app/controllers/report_controller.rb
@@ -49,8 +49,8 @@ class ReportController < ApplicationController
:all,
:conditions => ['(primary_id = ? or backup_id = ?) and
backlog_item_id in
- (select bi.id from backlog_items bi where bi.sprint_id in (select id from sprints
where status = ?))',
- @this_user.id, @this_user.id, Sprint::STATUS_ACTIVE],
+ (select bi.id from backlog_items bi where bi.sprint_id in (select id from sprints
where state = ?))',
+ @this_user.id, @this_user.id, 'active'],
:order => 'backlog_item_id')
end
@@ -70,9 +70,9 @@ class ReportController < ApplicationController
g.data("Remaining hours", @data)
g.data("Target", @goal)
g.labels= {
- 0 => @sprint.start.to_s(:date),
- (@sprint.duration / 2) => (@sprint.start + (@sprint.duration /
2)).to_s(:date),
- (@sprint.duration - 1) => (@sprint.start + (@sprint.duration -
1)).to_s(:date)}
+ 0 => @sprint.starts.to_s(:date),
+ (@sprint.duration / 2) => (@sprint.starts + (@sprint.duration /
2)).to_s(:date),
+ (@sprint.duration - 1) => (@sprint.starts + (@sprint.duration -
1)).to_s(:date)}
send_data(g.to_blob, :disposition => 'inline', :type =>
'image/png', :filename => "site-stats.png")
else
diff --git a/app/controllers/sprints_controller.rb
b/app/controllers/sprints_controller.rb
index 85e5097..409c546 100644
--- a/app/controllers/sprints_controller.rb
+++ b/app/controllers/sprints_controller.rb
@@ -20,12 +20,12 @@ class SprintsController < ApplicationController
before_filter :authenticated, :except => [:index, :show, :members]
before_filter :load_product, :only => [:new, :create]
before_filter :load_sprint, :except => [:index, :new, :create]
- before_filter :get_status, :only => [:status]
- before_filter :verify_can_edit, :only => [:edit, :update, :status]
+ before_filter :verify_can_edit, :only => [:edit, :update]
before_filter :verify_sprint_is_pending, :only => [:plan, :populate]
+ before_filter :verify_can_change_state, :only => [:planning, :start, :complete,
:cancel]
before_filter :prepare_for_edit, :only => [:new, :create, :edit, :update,
:plan, :populate]
before_filter :path_to_list, :only => [:index, :new, :create]
- before_filter :path_to_one, :only => [:show, :edit, :update, :status,
:plan, :populate, :members]
+ before_filter :path_to_one, :only => [:show, :edit, :update, :plan,
:populate, :members]
# GET /sprints
def index
@@ -111,23 +111,6 @@ class SprintsController < ApplicationController
end
end
- # PUT /sprints/1/status
- def status
- if @sprint.allowed_status?(@status)
- respond_to do |format|
- Sprint.transaction do
- @sprint.status = @status
- @sprint.save!
-
- flash[:message] = "Sprint moved to #{(a)sprint.status_text}."
- format.html { redirect_to sprint_path(@sprint) }
- end
- end
- else
- report_error "You cannot move the sprint to #{(a)sprint.status_text}."
- end
- end
-
# DELETE /sprints/1/destroy
def destroy
if @sprint.can_delete?(@user)
@@ -144,6 +127,66 @@ class SprintsController < ApplicationController
end
end
+ # PUT /sprints/1/planning
+ def planning
+ Sprint.transaction do
+ if @sprint.can_plan?
+ @sprint.plan
+ respond_to do |format|
+ flash[:message] = "The sprint is now moved to the planning state."
+ format.html {redirect_to sprint_path(@sprint)}
+ end
+ else
+ report_error "This sprint cannot be moved to the planning state."
+ end
+ end
+ end
+
+ # PUT /sprints/1/start
+ def start
+ Sprint.transaction do
+ if @sprint.can_start?
+ @sprint.start
+ respond_to do |format|
+ flash[:message] = "This sprint is now started."
+ format.html {redirect_to sprint_path(@sprint)}
+ end
+ else
+ report_error "This sprint cannot be moved to the active state."
+ end
+ end
+ end
+
+ # PUT /sprints/1/complete
+ def complete
+ Sprint.transaction do
+ if @sprint.can_complete?
+ @sprint.complete
+ respond_to do |format|
+ flash[:message] = "This sprint is now completed."
+ format.html {redirect_to sprint_path(@sprint)}
+ end
+ else
+ report_error "This sprint cannot be moved to the completed state."
+ end
+ end
+ end
+
+ # PUT /sprints/1/cancel
+ def cancel
+ Sprint.transaction do
+ if @sprint.can_cancel?
+ @sprint.cancel
+ respond_to do |format|
+ flash[:message] = "This sprint has been cancelled."
+ format.html {redirect_to sprint_path(@sprint)}
+ end
+ else
+ report_error "This sprint cannot be cancelled."
+ end
+ end
+ end
+
# GET /sprints/1/plan
def plan
add_breadcrumb "Plan"
@@ -226,18 +269,16 @@ class SprintsController < ApplicationController
report_error "Missing or invalid sprint" unless @sprint
end
- def get_status
- @status = params[:status].to_i
-
- report_error "Missing or invalid status." unless @status &&
(0...Sprint::STATUS_TEXT.size).include?(@status)
- end
-
def verify_can_edit
report_error "You are not allowed to edit this sprint." unless
@sprint.can_edit?(@user)
end
def verify_sprint_is_pending
- report_error "This can only be done for a pending sprint." unless
@sprint.pending?
+ report_error "This can only be done for a pending sprint." unless
@sprint.planning?
+ end
+
+ def verify_can_change_state
+ report_error "You are not allowed to change this sprint's state."
unless @sprint.can_change_state?(@user)
end
def prepare_for_edit
diff --git a/app/models/sprint.rb b/app/models/sprint.rb
index f6d808f..b735e68 100644
--- a/app/models/sprint.rb
+++ b/app/models/sprint.rb
@@ -37,25 +37,13 @@
# sprint is considered healthy.
#
class Sprint < ActiveRecord::Base
- STATUS_PLANNED = 0
- STATUS_ACTIVE = 1
- STATUS_CLOSED = 2
- STATUS_CANCELED = 3
- STATUS_TEXT =
- {
- 'Planned' => STATUS_PLANNED,
- 'Active' => STATUS_ACTIVE,
- 'Closed' => STATUS_CLOSED,
- 'Canceled' => STATUS_CANCELED
- }.sort_by { |k,v| v }
-
validates_presence_of :product_id,
:message => 'Sprints must be associated with a product.'
validates_presence_of :title,
:message => 'Please give the sprint a title.'
- validates_presence_of :start,
+ validates_presence_of :starts,
:message => 'Please set the starting date for the sprint.'
validates_presence_of :duration,
@@ -89,16 +77,48 @@ class Sprint < ActiveRecord::Base
named_scope :for_product, lambda { |product_id|
{ :conditions => product_id ? ["product_id = ?", product_id]: []}
}
- named_scope :planned, {:conditions => ['status = ?', STATUS_PLANNED]}
+ named_scope :planned, {:conditions => "state = 'planning'"}
+ named_scope :active, {:conditions => "state = 'active'"}
+
+ STATES = {:planning => "Planning", :active => "Active",
:completed => "Completed", :cancelled => "Cancelled"}
+
+ state_machine :initial => :planning do
+ event :start do
+ transition :planning => :active
+
+ def can_fire?(sprint)
+ sprint.planning? && Sprint.for_product(sprint.product).active.empty?
+ end
+ end
+
+ event :plan do
+ transition :active => :planning
- # Returns the text for the status.
- def status_text
- STATUS_TEXT[status][0]
+ def can_fire?(sprint)
+ sprint.active? && Sprint.for_product(sprint.product).planned.empty?
+ end
+ end
+
+ event :complete do
+ transition :active => :completed
+ end
+
+ event :cancel do
+ transition :active => :cancelled
+ end
+
+ event :restart do
+ transition [:completed, :cancelled] => :active
+
+ def can_fire?(sprint)
+ (sprint.completed? || sprint.cancelled?) &&
Sprint.for_product(sprint.product).active.empty?
+ end
+ end
end
# Returns the end date for the sprint.
def end_date
- self.start + (self.duration - 1)
+ self.starts + (self.duration - 1)
end
# Returns the total estimated hours.
@@ -127,12 +147,17 @@ class Sprint < ActiveRecord::Base
# Returns the number of hours remaining for the specified day.
def remaining_hours_for_day(day)
- backlog_items.inject(0) { |sum, item| sum + item.remaining_hours_for_date(start +
day) }
+ backlog_items.inject(0) { |sum, item| sum + item.remaining_hours_for_date(starts +
day) }
end
# Returns whether the specified user can modify this sprint.
def can_edit?(user)
- is_product_owner(user) || is_team_lead(user)
+ (active? || planning?) && (is_product_owner(user) || is_team_lead(user))
+ end
+
+ # returns whether the user can change this sprint's state.
+ def can_change_state?(user)
+ (is_product_owner(user) || is_team_lead(user))
end
# Returns whether the user can delete the current sprint.
@@ -142,49 +167,33 @@ class Sprint < ActiveRecord::Base
# Returns whether the specified user is allowed to populate this sprint.
def can_populate?(user)
- (is_product_owner(user) || is_team_lead(user)) && pending?
- end
-
- # Returns whether the sprint can be moved to the given status.
- def allowed_status?(status)
- case self.status
- when STATUS_PLANNED:
- return true if status == STATUS_ACTIVE
- when STATUS_ACTIVE:
- return true if (status == STATUS_PLANNED && actual_hours == 0 &&
Sprint.for_product(product).planned.empty?)
- return true if [STATUS_CANCELED, STATUS_CLOSED].include?(status)
-
- when STATUS_CLOSED: return true if status == STATUS_ACTIVE
- when STATUS_CANCELED: return true if status == STATUS_ACTIVE
- end
-
- return false
+ planning? && (is_product_owner(user) || is_team_lead(user))
end
# Returns whether the sprint is pending.
- def pending?
- status == STATUS_PLANNED
+ def planning?
+ state == 'planning'
end
# Returns whether the sprint is currently active or not.
def active?
- status == STATUS_ACTIVE
+ state == 'active'
end
- # Returns whether the sprint is current closed or not.
- def closed?
- status == STATUS_CLOSED
+ # Returns whether the sprint is current completed.
+ def completed?
+ state == 'completed'
end
# Returns whether a burndown chart can be viewed for this sprint.
def can_view_burndown?
- status != STATUS_PLANNED
+ !planning?
end
# Rethers whether the sprint can have backlog items added, and whether the
# specified user can add one.
def can_add_backlog_items?(user)
- (is_product_owner(user) || is_team_lead(user)) && status == STATUS_ACTIVE
+ (is_product_owner(user) || is_team_lead(user)) && active?
end
# Returns the data as of the given day into the sprint.
@@ -198,7 +207,7 @@ class Sprint < ActiveRecord::Base
# Returns whether the sprint is in a healthy state or not.
def healthy?
- days_into_sprint = Date.today - start
+ days_into_sprint = Date.today - starts
if (0..duration).include? days_into_sprint
@@ -234,7 +243,7 @@ class Sprint < ActiveRecord::Base
# When the user close a sprint, every user stories are close if the related
# backlog item are completed.
def close_user_stories
- return unless status == STATUS_CLOSED
+ return unless state == 'closed'
BacklogItem.transaction do
self.backlog_items.each do |backlog_item|
diff --git a/app/models/task.rb b/app/models/task.rb
index 75b2451..697bc21 100644
--- a/app/models/task.rb
+++ b/app/models/task.rb
@@ -70,7 +70,7 @@ class Task < ActiveRecord::Base
errors.add('backup_id',"The backup developer cannot also be
#{primary.display_name}.")
end
if backlog_item
- unless (backlog_item.sprint.start...(backlog_item.sprint.start +
backlog_item.sprint.duration)).include? self.when_entered
+ unless (backlog_item.sprint.starts...(backlog_item.sprint.starts +
backlog_item.sprint.duration)).include? self.when_entered
errors.add('when_entered', 'Task must be done during the
sprint.')
end
end
diff --git a/app/views/sprints/_edit.html.erb b/app/views/sprints/_edit.html.erb
index fc518e7..5761eac 100644
--- a/app/views/sprints/_edit.html.erb
+++ b/app/views/sprints/_edit.html.erb
@@ -24,8 +24,8 @@
<tr>
<td class="label-required">Starts on:</td>
<td class="value">
- <%= form.date_select :start %>
- <%= error_message_on(:sprint, :start) %>
+ <%= form.date_select :starts %>
+ <%= error_message_on(:sprint, :starts) %>
</td>
</tr>
<tr>
diff --git a/app/views/sprints/index.html.erb b/app/views/sprints/index.html.erb
index bd5b84b..ade1a68 100644
--- a/app/views/sprints/index.html.erb
+++ b/app/views/sprints/index.html.erb
@@ -12,17 +12,16 @@
<th scope="col">Team Lead</th>
<th scope="col">Items</th>
<th scope="col">Members</th>
- <th scope="col">Status</th>
<th scope="col">Starts</th>
</tr>
</thead>
<tbody>
<% @sprints.each do |sprint| %>
- <tr class="<%= "status-#{sprint.status}" %>">
+ <tr class="<%= "status-#{sprint.state}" %>">
<td><%= sprint.id %></td>
<td class="name">
- <%= link_to "#{sprint.title}
(#{Sprint::STATUS_TEXT[sprint.status][0]})",
+ <%= link_to "#{sprint.title} (#{Sprint::STATES[sprint.state]})",
sprint_path(sprint) %>
<%= "Hours: #{show_hours_as_ear(sprint)}" %>
<%= RedCloth.new(get_first_sentence(sprint.goals)).to_html %>
@@ -36,8 +35,7 @@
</td>
<td><%= link_to "#{sprint.backlog_items.size}", items_path(:sprint
=> sprint) %></td>
<td><%= link_to "#{sprint.members.size}",
members_sprint_path(sprint) %></td>
- <td><%= sprint.status_text %></td>
- <td><%= "#{show_date(sprint.start)} (#{sprint.duration} days)"
%></td>
+ <td><%= "#{show_date(sprint.starts)} (#{sprint.duration} days)"
%></td>
</tr>
<% end %>
</tbody>
diff --git a/app/views/sprints/show.html.erb b/app/views/sprints/show.html.erb
index ce58061..fb54101 100644
--- a/app/views/sprints/show.html.erb
+++ b/app/views/sprints/show.html.erb
@@ -1,7 +1,7 @@
<div id="content">
<div id="page-details">
<dl>
- <dt><%= "#{(a)sprint.title} (#{(a)sprint.status_text})"
%></dt>
+ <dt><%= "#{(a)sprint.title} (#{(a)sprint.state})" %></dt>
<dd><%= RedCloth.new((a)sprint.goals).to_html %></dd>
<dt>Team Lead</dt>
<dd>
@@ -10,8 +10,8 @@
</dd>
<dt>Sprint Dates</dt>
<dd>
- Starts on <%= show_date((a)sprint.start) %> and continues until <%=
show_date((a)sprint.end_date) %>.
- This sprint is currently in the <%= @sprint.status_text %> state.
+ Starts on <%= show_date((a)sprint.starts) %> and continues until <%=
show_date((a)sprint.end_date) %>.
+ This sprint is currently in the <%= @sprint.state %> state.
</dd>
</dl>
</div>
@@ -41,33 +41,27 @@
<div class="header"><span class="title">Change Sprint
Status</span></div>
- <% if @sprint.allowed_status?(Sprint::STATUS_PLANNED) %>
- <%= link_to "Move sprint back to planning...",
- status_sprint_path(@sprint, :status => Sprint::STATUS_PLANNED),
- :method => :put, :confirm => "Move to planning? Are you sure?",
- :class => "command" %>
+ <% if @sprint.can_change_state?(@user) %>
+ <% if @sprint.can_plan? %>
+ <%= link_to "Move sprint back to planning...",
planning_sprint_path(@sprint),
+ :method => :put, :confirm => "Move to planning? Are you sure?",
:class => "command" %>
<% end %>
- <% if @sprint.allowed_status?(Sprint::STATUS_ACTIVE) %>
- <%= link_to "Start this sprint...",
- status_sprint_path(@sprint, :status => Sprint::STATUS_ACTIVE),
- :method => :put, :confirm => "Start this sprint? Are you sure?",
- :class => "command" %>
+ <% if @sprint.can_start? %>
+ <%= link_to "Start this sprint...", start_sprint_path(@sprint),
+ :method => :put, :confirm => "Start this sprint? Are you sure?",
:class => "command" %>
<% end %>
- <% if @sprint.allowed_status?(Sprint::STATUS_CLOSED) %>
- <%= link_to "Close this sprint...",
- status_sprint_path(@sprint, :status => Sprint::STATUS_CLOSED),
- :method => :put, :confirm => "Close this sprint? Are you sure?",
- :class => "command" %>
+ <% if @sprint.can_complete? %>
+ <%= link_to "Close this sprint...", complete_sprint_path(@sprint),
+ :method => :put, :confirm => "Close this sprint? Are you sure?",
:class => "command" %>
<% end %>
- <% if @sprint.allowed_status?(Sprint::STATUS_CANCELED) %>
- <%= link_to "Cancel this sprint...",
- status_sprint_path(@sprint, :status => Sprint::STATUS_CANCELED),
- :method => :put, :confirm => "Cancel this sprint? Are you sure?",
- :class => "command" %>
+ <% if @sprint.can_cancel? %>
+ <%= link_to "Cancel this sprint...", cancel_sprint_path(@sprint),
+ :method => :put, :confirm => "Cancel this sprint? Are you sure?",
:class => "command" %>
<% end %>
<% end %>
+ <% end %>
<% end %>
diff --git a/config/environment.rb b/config/environment.rb
index 464c8bf..3848e63 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -33,6 +33,7 @@ Rails::Initializer.run do |config|
:secret =>
'bb5e22a8065951948272bccbeff6c1e1fa1eff29e85dabcefe7577b68795d9e42cfc9a18426f84aee6722bb4859bf8b144a98fa58891e6c1f215dc3b6f38d2b3'
}
+ config.gem 'state_machine'
config.gem 'rufus-scheduler'
config.gem "gruff"
config.gem "RedCloth"
diff --git a/config/routes.rb b/config/routes.rb
index 9738de7..1d4c143 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -48,9 +48,12 @@ ActionController::Routing::Routes.draw do |map|
map.resources :sprints, :member =>
{
+ :planning => :put,
+ :start => :put,
+ :complete => :put,
+ :cancel => :put,
:plan => :get,
:populate => :post,
- :status => :put,
:members => :get
}
diff --git a/db/migrate/011_create_sprints.rb b/db/migrate/011_create_sprints.rb
index 6222b1c..1c1b27b 100644
--- a/db/migrate/011_create_sprints.rb
+++ b/db/migrate/011_create_sprints.rb
@@ -20,9 +20,9 @@ class CreateSprints < ActiveRecord::Migration
t.integer :product_id, :null => false
t.integer :team_lead_id, :null => false
t.string :title, :null => false, :limit => 100
- t.date :start, :null => false
+ t.date :starts, :null => false
t.integer :duration, :null => false, :precision => 2, :scale => 0
- t.integer :status, :null => false, :default => Sprint::STATUS_PLANNED
+ t.string :state, :null => false, :default => "planning"
t.string :goals, :null => false, :limit => 1000
t.timestamps
diff --git a/doc/ChangeLog b/doc/ChangeLog
index da61547..1252467 100644
--- a/doc/ChangeLog
+++ b/doc/ChangeLog
@@ -12,6 +12,7 @@ Change Log (0.3.0):
* #182 - The member count on the sprint list goes to the member list page.
* #183 - Unown items have an accept link on the sprint backlog page.
* #184 - Only one planned sprint is allowed for a product.
+ * #185 - Only one active sprint is allowed for a product.
* #192 - Backlog items should not be reopened when the sprint is not active. (BUG)
* #193 - After writing an epic, the user should be returned to the epics list. (BUG)
* #195 - Product details page does not show number of members. (BUG)
diff --git a/test/fixtures/backlog_items.yml b/test/fixtures/backlog_items.yml
index 1b0cc23..f7b3c50 100644
--- a/test/fixtures/backlog_items.yml
+++ b/test/fixtures/backlog_items.yml
@@ -18,21 +18,21 @@ closed_backlog_item:
estimated_hours: 4.0
state: <%= BacklogItem::STATE_COMPLETED %>
-inactive_sprint_backlog_item:
- sprint_id: <%= Fixtures.identify(:inactive_sprint) %>
+planned_sprint_backlog_item:
+ sprint_id: <%= Fixtures.identify(:planned_sprint) %>
user_story_id: <%= Fixtures.identify(:create_update_profile) %>
estimated_hours: 3.5
state: <%= BacklogItem::STATE_PENDING %>
-owned_inactive_sprint_backlog_item:
- sprint_id: <%= Fixtures.identify(:inactive_sprint) %>
+owned_planned_sprint_backlog_item:
+ sprint_id: <%= Fixtures.identify(:planned_sprint) %>
user_story_id: <%= Fixtures.identify(:email_forgotten_password) %>
owner_id: <%= Fixtures.identify(:mcpierce) %>
estimated_hours: 3.5
state: <%= BacklogItem::STATE_PENDING %>
closed_sprint_backlog_item:
- sprint_id: <%= Fixtures.identify(:closed_sprint) %>
+ sprint_id: <%= Fixtures.identify(:completed_sprint) %>
user_story_id: <%= Fixtures.identify(:email_forgotten_password) %>
owner_id: <%= Fixtures.identify(:mcpierce) %>
estimated_hours: 4.5
diff --git a/test/fixtures/product_roles.yml b/test/fixtures/product_roles.yml
index 9078564..a5acfed 100644
--- a/test/fixtures/product_roles.yml
+++ b/test/fixtures/product_roles.yml
@@ -19,6 +19,13 @@ team_lead_projxp:
pending: false
is_approved: true
+team_lead_projxp_web_services:
+ product_id: <%= Fixtures.identify(:projxp_web_services) %>
+ user_id: <%= Fixtures.identify(:team_lead) %>
+ role_id: <%= Fixtures.identify(:developer) %>
+ pending: false
+ is_approved: true
+
verified_user_projxp:
product_id: <%= Fixtures.identify(:projxp_web) %>
user_id: <%= Fixtures.identify(:verified_user) %>
diff --git a/test/fixtures/products.yml b/test/fixtures/products.yml
index fae6dc4..18d457e 100644
--- a/test/fixtures/products.yml
+++ b/test/fixtures/products.yml
@@ -1,16 +1,22 @@
projxp_web:
project_id: <%= Fixtures.identify(:projxp) %>
owner_id: <%= Fixtures.identify(:projxp_owner) %>
- name: Cairo web application.
+ name: ProjXP web application
description: The Ruby on Rails application.
logo_name: projxp_web.png
projxp_web_services:
project_id: <%= Fixtures.identify(:projxp) %>
owner_id: <%= Fixtures.identify(:projxp_owner) %>
- name: Cairo web services.
+ name: ProjXP web services.
description: A set of web service APIs to interface with the RoR app.
+projxp_common_libraries:
+ project_id: <%= Fixtures.identify(:projxp) %>
+ owner_id: <%= Fixtures.identify(:projxp_owner) %>
+ name: ProjXP common libraries.
+ description: Shared components between all ProjXP elements.
+
teatime_midp:
project_id: <%= Fixtures.identify(:teatime) %>
owner_id: <%= Fixtures.identify(:teatime_owner) %>
diff --git a/test/fixtures/sprints.yml b/test/fixtures/sprints.yml
index 049d5f6..3af57e9 100644
--- a/test/fixtures/sprints.yml
+++ b/test/fixtures/sprints.yml
@@ -1,44 +1,71 @@
+planned_sprint:
+ product_id: <%= Fixtures.identify(:projxp_web) %>
+ title: Stuff we're going to do next.
+ starts: <%= DateTime.now.to_s(:db) %>
+ duration: 28
+ goals: Get things done.
+ state: planning
+ team_lead_id: <%= Fixtures.identify(:team_lead) %>
+
active_sprint:
product_id: <%= Fixtures.identify(:projxp_web) %>
title: Begin development for the web product.
- start: <%= DateTime.now.to_s(:db) %>
+ starts: <%= DateTime.now.to_s(:db) %>
duration: 28
goals: Get things done.
- status: <%= Sprint::STATUS_ACTIVE %>
+ state: active
+ team_lead_id: <%= Fixtures.identify(:team_lead) %>
+
+completed_sprint:
+ product_id: <%= Fixtures.identify(:projxp_web) %>
+ title: This sprint is completed.
+ starts: <%= DateTime.now.to_s(:db) %>
+ duration: 28
+ goals: Get more stuff done.
+ state: completed
team_lead_id: <%= Fixtures.identify(:team_lead) %>
-closed_sprint:
+cancelled_sprint:
product_id: <%= Fixtures.identify(:projxp_web) %>
- title: This sprint is closed.
- start: <%= DateTime.now.to_s(:db) %>
+ title: This sprint is cancelled.
+ starts: <%= DateTime.now.to_s(:db) %>
duration: 28
goals: Get more stuff done.
- status: <%= Sprint::STATUS_CLOSED %>
+ state: cancelled
team_lead_id: <%= Fixtures.identify(:team_lead) %>
-inactive_sprint:
+startable_sprint:
product_id: <%= Fixtures.identify(:projxp_web_services) %>
- title: This is the planned sprint.
- start: <%= (DateTime.now + 28).to_s(:db) %>
+ title: This sprint can be started.
+ starts: <%= DateTime.now.to_s(:db) %>
duration: 28
- goals: Get some web services written.
- status: <%= Sprint::STATUS_PLANNED %>
- team_lead_id: <%= Fixtures.identify(:mcpierce) %>
+ goals: Get some stuff done.
+ state: planning
+ team_lead_id: <%= Fixtures.identify(:team_lead) %>
-projxp_web_services_active_sprint:
+restartable_completed_sprint:
product_id: <%= Fixtures.identify(:projxp_web_services) %>
- title: This is the active sprint.
- start: <%= DateTime.now.to_s(:db) %>
+ title: This sprint can be started.
+ starts: <%= DateTime.now.to_s(:db) %>
duration: 28
- goals: Get some web services written.
- status: <%= Sprint::STATUS_ACTIVE %>
- team_lead_id: <%= Fixtures.identify(:mcpierce) %>
+ goals: Got some stuff done.
+ state: completed
+ team_lead_id: <%= Fixtures.identify(:team_lead) %>
-teatime_iphone_active_sprint:
- product_id: <%= Fixtures.identify(:teatime_iphone) %>
- title: This is an active sprint.
- start: <%= DateTime.now.to_s(:db) %>
- duration: 14
- goals: Get some iphone stuff done.
- status: <%= Sprint::STATUS_ACTIVE %>
- team_lead_id: <%= Fixtures.identify(:mcpierce) %>
+restartable_cancelled_sprint:
+ product_id: <%= Fixtures.identify(:projxp_web_services) %>
+ title: This sprint can be started.
+ starts: <%= DateTime.now.to_s(:db) %>
+ duration: 28
+ goals: Didn't get any stuff done.
+ state: cancelled
+ team_lead_id: <%= Fixtures.identify(:team_lead) %>
+
+plannable_sprint:
+ product_id: <%= Fixtures.identify(:projxp_common_libraries) %>
+ title: Stuff we're going to do next.
+ starts: <%= DateTime.now.to_s(:db) %>
+ duration: 28
+ goals: Get things done.
+ state: active
+ team_lead_id: <%= Fixtures.identify(:team_lead) %>
diff --git a/test/functional/items_controller_test.rb
b/test/functional/items_controller_test.rb
index 3cb35ed..54eed34 100644
--- a/test/functional/items_controller_test.rb
+++ b/test/functional/items_controller_test.rb
@@ -44,13 +44,13 @@ class ItemsControllerTest < ActionController::TestCase
@non_team_lead = users(:jdonuts)
raise "Non-team lead cannot be the team lead!" if @team_lead.id ==
@non_team_lead.id
- @pending_sprint = sprints(:inactive_sprint)
- raise "Sprint should be pending!" unless @pending_sprint.pending?
+ @pending_sprint = sprints(:planned_sprint)
+ raise "Sprint should be pending!" unless @pending_sprint.planning?
- @pending_item = backlog_items(:inactive_sprint_backlog_item)
+ @pending_item = backlog_items(:planned_sprint_backlog_item)
raise "Pending item should be on a pending sprint." unless
@pending_item.sprint_id == @pending_sprint.id
- @other_sprint = sprints(:inactive_sprint)
+ @other_sprint = sprints(:planned_sprint)
raise "Product and sprint are mismatched!" unless @product.id ==
@sprint.product_id
@item = backlog_items(:owned_backlog_item)
@@ -242,7 +242,7 @@ class ItemsControllerTest < ActionController::TestCase
end
# Ensures that the sprint must be active for an estimate to be updated.
- def test_estimate_on_inactive_sprint
+ def test_estimate_on_planned_sprint
post :estimate, {:id => @pending_item.id}, {:user_id => @item_owner.id}
assert_redirected_to error_path
diff --git a/test/functional/report_controller_test.rb
b/test/functional/report_controller_test.rb
index be5ad3e..ed52087 100644
--- a/test/functional/report_controller_test.rb
+++ b/test/functional/report_controller_test.rb
@@ -22,7 +22,7 @@ class ReportControllerTest < ActionController::TestCase
fixtures :users
def setup
- @pending_sprint = sprints(:inactive_sprint)
+ @planned_sprint = sprints(:planned_sprint)
@sprint = sprints(:active_sprint)
@user = users(:mcpierce)
end
@@ -35,12 +35,12 @@ class ReportControllerTest < ActionController::TestCase
assert_redirected_to error_url
end
- # Ensures that trying to print a burndown report for a pending sprint fails.
+ # Ensures that trying to print a burndown report for a planned sprint fails.
#
- def test_burndown_graphic_fails_for_pending_sprint
- get :burndown_graphic, {:id => @pending_sprint.id}
+ def test_burndown_graphic_fails_for_planned_sprint
+ get :burndown_graphic, {:id => @planned_sprint.id}
- assert_redirected_to sprint_path(@pending_sprint)
+ assert_redirected_to sprint_path(@planned_sprint)
end
# Ensures that a burndown chart works as expected.
diff --git a/test/functional/sprints_controller_test.rb
b/test/functional/sprints_controller_test.rb
index 3802af0..3247a5f 100644
--- a/test/functional/sprints_controller_test.rb
+++ b/test/functional/sprints_controller_test.rb
@@ -25,13 +25,21 @@ class SprintsControllerTest < ActionController::TestCase
def setup
@active_sprint = sprints(:active_sprint)
raise "An active sprint must be active!" unless @active_sprint.active?
+ raise "Product must have a planned sprint!" if
Sprint.for_product((a)active_sprint.product).planned.empty?
+
+ @plannable_sprint = sprints(:plannable_sprint)
+ raise "Sprint must be in a non-planning state!" if
@plannable_sprint.planning?
@member = @active_sprint.members.first
raise "The active sprint has to have members!" unless @member
- @pending_sprint = sprints(:inactive_sprint)
- raise "Sprint must be pending!" unless @pending_sprint.pending?
- raise "Products must have the same owner!" unless
@pending_sprint.product.owner_id == @active_sprint.product.owner_id
+ @planned_sprint = sprints(:planned_sprint)
+ raise "Sprint must be pending!" unless @planned_sprint.planning?
+ raise "Products must have the same owner!" unless
@planned_sprint.product.owner_id == @active_sprint.product.owner_id
+
+ @startable_sprint = sprints(:startable_sprint)
+ raise "Sprint must be in the planned start!" unless
@startable_sprint.planning?
+ raise "Product cannot have an active sprint!" unless
Sprint.for_product((a)startable_sprint).active.empty?
@product = @active_sprint.product
@other_product = products(:projxp_web_services)
@@ -52,12 +60,15 @@ class SprintsControllerTest < ActionController::TestCase
raise "Product cannot have anys sprints!" unless
@product_with_no_sprints.sprints.empty?
raise "Member must be on product team!" unless
@product_with_no_sprints.is_member?(@member)
- raise "Nonowner is the product owner!" if @pending_sprint.product.owner_id
== @nonowner.id
- raise "Nonwner is the team lead!" if @pending_sprint.team_lead_id ==
@nonowner.id
+ raise "Nonowner is the product owner!" if @planned_sprint.product.owner_id
== @nonowner.id
+ raise "Nonwner is the team lead!" if @planned_sprint.team_lead_id ==
@nonowner.id
+
+ @non_team_lead = users(:mcpierce)
+ raise "Non-team lead cannot be a team lead!" if [@active_sprint.id,
@plannable_sprint.id].include? @non_team_lead.id
@new_sprint = {
:title => "New Sprint",
- :start => Date.today,
+ :starts => Date.today,
:duration => 28,
:goals => "Get lots of things done!",
:team_lead_id => @product.owner_id
@@ -309,6 +320,138 @@ class SprintsControllerTest < ActionController::TestCase
"All backlog items should have been deleted for this sprint."
end
+ # Ensures that an anonymous user cannot move sprints to the planning state.
+ def test_planning_as_anonymous
+ put :planning
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that a valid sprint is required.
+ def test_planning_with_invalid_sprint
+ put :planning, {}, {:user_id => @plannable_sprint.team_lead_id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that only team leads can move a sprint to the planning state.
+ def test_planning_as_non_team_lead
+ put :planning, {:id => @plannable_sprint.id}, {:user_id => @non_team_lead.id}
+ end
+
+ # Ensures that a sprint that cannot be planned fails.
+ def test_planning_with_state_failure
+ put :planning, {:id => @active_sprint.id}, {:user_id =>
@active_sprint.team_lead_id}
+
+ assert_redirected_to error_path
+ result = Sprint.find_by_id((a)active_sprint.id)
+ assert !result.planning?, "Sprint should not have been put into the planning
state."
+ end
+
+ # Ensures that a sprint can be moved to the planned state.
+ def test_planning
+ put :planning, {:id => @plannable_sprint.id}, {:user_id =>
@plannable_sprint.team_lead_id}
+
+ assert_redirected_to sprint_path(@plannable_sprint)
+ result = Sprint.find_by_id((a)plannable_sprint.id)
+ assert result.planning?, "Sprint should have been put into the planning
state."
+ end
+
+ # Ensures that anonymous users can't start a sprint.
+ def test_start_as_anonymous
+ put :start
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that a valid sprint id is required.
+ def test_start_with_invalid_id
+ put :start, {}, {:user_id => @planned_sprint.team_lead_id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that only the team lead can start a sprint.
+ def test_start_as_non_team_lead
+ put :start, {:id => @planned_sprint.id}, {:user_id => @non_team_lead.id }
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that a sprint that can't be started results in an error.
+ def test_start_with_nonstarter_sprint
+ put :start, {:id => @plannable_sprint.id },{:user_id =>
@plannable_sprint.team_lead_id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that starting a sprint work.s
+ def test_start
+ put :start, {:id => @startable_sprint.id}, {:user_id =>
@startable_sprint.team_lead_id}
+
+ assert_redirected_to sprint_path(@startable_sprint)
+ result = Sprint.find_by_id((a)startable_sprint.id)
+ assert result.active?, "Sprint should have been started."
+ end
+
+ # Ensures that an anonymous user cannot complete a sprint.
+ def test_complete_as_anonymous
+ put :complete
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that a valid sprint id is required.
+ def test_complete_with_invalid_sprint
+ put :complete, {}, {:user_id => @active_sprint.team_lead_id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that only the team lead can complete a sprint.
+ def test_complete_as_non_team_lead
+ put :complete, {:id => @active_sprint.id}, {:user_id => @non_team_lead.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that a sprint that cannot be completed results in an error.
+ def test_complete_for_noncompletable_sprint
+ put :complete, {:id => @planned_sprint.id}, {:user_id =>
@planned_sprint.team_lead_id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that a sprint can be completed.
+ def test_complete
+ put :complete, {:id => @active_sprint.id}, {:user_id =>
@active_sprint.team_lead_id}
+
+ assert_redirected_to sprint_path(@active_sprint)
+ result = Sprint.find_by_id((a)active_sprint.id)
+ assert result.completed?, "Sprint was not moved to the completed state."
+ end
+
+ # Ensures that anonymous users can't cancel a sprint.
+ def test_cancel_as_anonymous
+ put :cancel
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that a valid sprint id is required.
+ def test_cancel_with_invalid_sprint
+ put :cancel, {}, {:user_id => @active_sprint.team_lead_id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that only the team lead can cancel a sprint.
+ def test_cancel_as_non_team_lead
+ put :cancel, {:id => @active_sprint.id}, {:user_id => @non_team_lead.id}
+
+ assert_redirected_to error_path
+ end
+
# Ensures that anonymous users can't plan.
def test_plan_as_anonymous
get :plan
@@ -316,6 +459,15 @@ class SprintsControllerTest < ActionController::TestCase
assert_redirected_to login_path
end
+ # Ensures that cancelling a sprint works.
+ def test_cancel
+ put :cancel, {:id => @active_sprint.id}, {:user_id =>
@active_sprint.team_lead_id}
+
+ assert_redirected_to sprint_path(@active_sprint)
+ result = Sprint.find_by_id((a)active_sprint.id)
+ assert result.cancelled?, "Sprint should have been marked cancelled."
+ end
+
# Ensures that a valid sprint is required.
def test_plan_with_invalid_sprint
get :plan, { }, {:user_id => @product.owner_id}
@@ -332,14 +484,14 @@ class SprintsControllerTest < ActionController::TestCase
# Ensures that only the owner can plan a sprint.
def test_plan_as_nonowner
- get :plan, {:id => @pending_sprint.id}, {:user_id => @nonowner.id}
+ get :plan, {:id => @planned_sprint.id}, {:user_id => @nonowner.id}
assert_redirected_to error_path
end
# Ensures that planning works.
def test_plan
- get :plan, { :id => @pending_sprint.id}, {:user_id =>
@pending_sprint.product.owner_id}
+ get :plan, { :id => @planned_sprint.id}, {:user_id =>
@planned_sprint.product.owner_id}
assert_response :success
assert assigns['user_stories'], "Failed to load any user stories."
@@ -370,83 +522,31 @@ class SprintsControllerTest < ActionController::TestCase
# Ensures that only the owner can populate a sprint.
def test_populate_as_nonowner
- post :populate, { :id => @pending_sprint.id, :estimates => @estimates,
:selected => @selected},
+ post :populate, { :id => @planned_sprint.id, :estimates => @estimates,
:selected => @selected},
{:user_id => @nonowner.id}
assert_redirected_to error_path
- assert
!BacklogItem.find_by_sprint_id_and_user_story_id(@pending_sprint.id,(a)user_story.id),
+ assert
!BacklogItem.find_by_sprint_id_and_user_story_id(@planned_sprint.id,(a)user_story.id),
"Backlog item should not have been created"
end
# Ensures that populating with no user stories is not an error. BUG#92
def test_populate_with_no_user_stories
- post :populate, { :id => @pending_sprint.id},{:user_id =>
@pending_sprint.product.owner_id}
+ post :populate, { :id => @planned_sprint.id},{:user_id =>
@planned_sprint.product.owner_id}
- assert_redirected_to sprint_path(@pending_sprint)
+ assert_redirected_to sprint_path(@planned_sprint)
end
# Ensures that populating works.
def test_populate
- post :populate, {:id => @pending_sprint.id,:estimates => @estimates, :selected
=> @selected},
- {:user_id => @pending_sprint.product.owner_id}
+ post :populate, {:id => @planned_sprint.id,:estimates => @estimates, :selected
=> @selected},
+ {:user_id => @planned_sprint.product.owner_id}
- assert_redirected_to sprint_path(@pending_sprint)
- assert
BacklogItem.find_by_sprint_id_and_user_story_id(@pending_sprint.id,(a)user_story.id),
+ assert_redirected_to sprint_path(@planned_sprint)
+ assert
BacklogItem.find_by_sprint_id_and_user_story_id(@planned_sprint.id,(a)user_story.id),
"Backlog item should have been created"
end
- # Ensures that an anonymous user can't change a sprint's status.
- def test_status_as_anonymous
- put :status
-
- assert_redirected_to login_path
- end
-
- # Ensures that a valid sprint is required.
- def test_status_with_invalid_sprint
- put :status, { }, {:user_id => @product.owner_id}
-
- assert_redirected_to error_path
- end
-
- # Ensures that non-product owners can't alter a sprint's status.
- def test_status_as_nonowner
- put :status, { :id => @active_sprint.id}, {:user_id => @nonowner.id}
-
- assert_redirected_to error_path
- end
-
- # Ensures that a status must be presented.
- def test_status_without_state
- put :status, { :id => @active_sprint.id}, {:user_id =>
@active_sprint.product.owner_id}
-
- assert_redirected_to error_path
- end
-
- # Ensures that if a status is not allowed then the sprint's not updated.
- def test_status_with_disallowed_status
- put :status, {:id => @active_sprint.id, :status => Sprint::STATUS_PLANNED},
{:user_id => @active_sprint.product.owner_id}
-
- assert_redirected_to error_path
- assert_equal @active_sprint.status, Sprint.find_by_id((a)active_sprint.id).status,
- "Sprint status should not have been updated."
- end
-
- # Ensures that a status can be updated as allowed.
- def test_status
- put :status, { :id => @active_sprint.id, :status => Sprint::STATUS_CLOSED},
{:user_id => @active_sprint.product.owner_id}
-
- assert_redirected_to sprint_path(@active_sprint)
- result = Sprint.find_by_id((a)active_sprint.id)
- assert result.closed?, "Sprint status was not updated."
- # ensure all backlog items were closed
- result.backlog_items.each do |item|
- if item.completed?
- assert item.user_story.closed?, "User story should have been closed."
- end
- end
- end
-
# Ensures that a valid product is required.
def test_members_with_invalid_product
get :members
diff --git a/test/functional/user_items_test.rb b/test/functional/user_items_test.rb
index d49bd44..f7f4da9 100644
--- a/test/functional/user_items_test.rb
+++ b/test/functional/user_items_test.rb
@@ -40,13 +40,13 @@ class UserItemsTest < ActionController::TestCase
@sprint = sprints(:active_sprint)
raise "Product and sprint are mismatched!" unless @product.id ==
@sprint.product_id
- @pending_sprint = sprints(:inactive_sprint)
- raise "Sprint should be pending!" unless @pending_sprint.pending?
+ @pending_sprint = sprints(:planned_sprint)
+ raise "Sprint should be pending!" unless @pending_sprint.planning?
- @pending_item = backlog_items(:inactive_sprint_backlog_item)
+ @pending_item = backlog_items(:planned_sprint_backlog_item)
raise "Pending item should be on a pending sprint." unless
@pending_item.sprint_id == @pending_sprint.id
- @other_sprint = sprints(:inactive_sprint)
+ @other_sprint = sprints(:planned_sprint)
raise "Product and sprint are mismatched!" unless @product.id ==
@sprint.product_id
@item = backlog_items(:owned_backlog_item)
@@ -113,7 +113,7 @@ class UserItemsTest < ActionController::TestCase
end
# Ensures that only active sprint items can be accepted.
- def test_accept_from_inactive
+ def test_accept_from_planned
get :accept, {:id => @pending_item.id}, {:user_id => @member.id}
assert_redirected_to error_path
@@ -148,7 +148,7 @@ class UserItemsTest < ActionController::TestCase
end
# Ensures that only active sprint items can be dropped.
- def test_drop_from_inactive
+ def test_drop_from_planned
get :accept, {:id => @pending_item.id},{:user_id => @member.id}
assert_redirected_to error_path
@@ -183,7 +183,7 @@ class UserItemsTest < ActionController::TestCase
end
# Ensures that only active sprint items can be completed.
- def test_complete_from_inactive
+ def test_complete_from_planned
get :complete, {:id => @pending_item.id},{:user_id => @member.id}
assert_redirected_to error_path
@@ -225,7 +225,7 @@ class UserItemsTest < ActionController::TestCase
end
# Ensures that only active sprint items can be reopened.
- def test_reopen_from_inactive
+ def test_reopen_from_planned
get :reopen,{:id => @pending_item.id},{:user_id => @member.id}
assert_redirected_to error_path
diff --git a/test/unit/backlog_item_test.rb b/test/unit/backlog_item_test.rb
index bb4f65c..3df3636 100644
--- a/test/unit/backlog_item_test.rb
+++ b/test/unit/backlog_item_test.rb
@@ -54,13 +54,13 @@ class BacklogItemTest < ActiveSupport::TestCase
raise "Nonmember must be on the product team!" unless
@product.is_member?(@nonmember)
raise "Nonmember cannot be the team lead!" if @sprint.team_lead_id ==
@nonmember.id
- @item_on_inactive_sprint = backlog_items(:inactive_sprint_backlog_item)
- raise "Item should not have an owner!" if @item_on_inactive_sprint.owner
- raise "That sprint must no be active!" if
@item_on_inactive_sprint.sprint.active?
+ @item_on_planned_sprint = backlog_items(:planned_sprint_backlog_item)
+ raise "Item should not have an owner!" if @item_on_planned_sprint.owner
+ raise "That sprint must no be active!" if
@item_on_planned_sprint.sprint.active?
@item_on_closed_sprint = backlog_items(:closed_sprint_backlog_item)
raise "Item must have an owner!" unless @item_on_closed_sprint.owner
- raise "That sprint must be closed!" unless
@item_on_closed_sprint.sprint.closed?
+ raise "That sprint must be closed!" unless
@item_on_closed_sprint.sprint.completed?
@deferrable_item = backlog_items(:owned_backlog_item)
end
@@ -118,9 +118,9 @@ class BacklogItemTest < ActiveSupport::TestCase
end
# Ensures that a user cannot accept an item on a non-active sprint.
- def test_accept_on_item_on_inactive_sprint
- assert !@item_on_inactive_sprint.can_accept?((a)owner),
- "Users must not be allowed to accept items on inactive sprints."
+ def test_accept_on_item_on_planned_sprint
+ assert !@item_on_planned_sprint.can_accept?((a)owner),
+ "Users must not be allowed to accept items on planned sprints."
end
# Ensures that a user cannot accept an already accepted backlog item.
@@ -172,7 +172,7 @@ class BacklogItemTest < ActiveSupport::TestCase
end
# Ensures that an item can only be reopened on an active sprint.
- def test_can_reopen_on_inactive_sprint
+ def test_can_reopen_on_planned_sprint
fail "You cannot reopen an item on a closed sprint." if
@item_on_closed_sprint.can_reopen?((a)item_on_closed_sprint.owner)
end
@@ -187,7 +187,7 @@ class BacklogItemTest < ActiveSupport::TestCase
end
# Ensures that an item must be on an active sprint to be marked as blocked.
- def test_mark_blocked_for_inactive_sprint
+ def test_mark_blocked_for_planned_sprint
fail "Only items on active sprints can be marked as blocked." if
@item_on_closed_sprint.can_mark_blocked?((a)item_on_closed_sprint.owner)
end
@@ -202,8 +202,8 @@ class BacklogItemTest < ActiveSupport::TestCase
end
# Ensures that an item not on an active sprint cannot be deferred.
- def test_defer_for_inactive_sprint_item
- fail "Defer is only for active sprints." if
@item_on_inactive_sprint.can_defer?((a)item_on_inactive_sprint.sprint.team_lead)
+ def test_defer_for_planned_sprint_item
+ fail "Defer is only for active sprints." if
@item_on_planned_sprint.can_defer?((a)item_on_planned_sprint.sprint.team_lead)
end
# Ensures that an owned item can be deferred
diff --git a/test/unit/sprint_member_test.rb b/test/unit/sprint_member_test.rb
index da4e3bc..fbefba7 100644
--- a/test/unit/sprint_member_test.rb
+++ b/test/unit/sprint_member_test.rb
@@ -22,7 +22,7 @@ class SprintTeamTest < ActiveSupport::TestCase
fixtures :users
def setup
- @sprint = sprints(:inactive_sprint)
+ @sprint = sprints(:planned_sprint)
@team_lead = @sprint.team_lead
@member = users(:mcpierce)
diff --git a/test/unit/sprint_test.rb b/test/unit/sprint_test.rb
index c01f1a0..cb8eeba 100644
--- a/test/unit/sprint_test.rb
+++ b/test/unit/sprint_test.rb
@@ -18,158 +18,155 @@
require File.dirname(__FILE__) + '/../test_helper'
class SprintTest < ActiveSupport::TestCase
- fixtures :sprints
- fixtures :products
- fixtures :users
-
def setup
- @product = products(:projxp_web)
- @new_sprint = Sprint.new(:product_id => @product.id,
- :title => 'Sprint1',
- :start => Date.today - 7,
- :duration => 14,
- :goals => "Things I'd like to
accomplish.",
- :team_lead_id => @product.owner.id)
- @developer = users(:mcpierce)
- raise "Developer is not a member of the product team." unless
@product.is_member?(@developer)
-
- @sprint = sprints(:active_sprint)
- @active_sprint = sprints(:projxp_web_services_active_sprint)
- raise "There needs to be a planned sprint!" if
Sprint.for_product((a)active_sprint.product).planned.empty?
-
- backlog_item = BacklogItem.new(:estimated_hours => 100)
- backlog_item.tasks << Task.new(:hours => 50)
- backlog_item.update_remaining_hours(50.0,@developer)
- @healthy_backlog = [ backlog_item ]
-
- backlog_item = BacklogItem.new(:estimated_hours => 100)
- backlog_item.tasks << Task.new(:hours => 25)
- @unhealthy_backlog = [ backlog_item ]
-
- @existing_sprint = sprints(:active_sprint)
- @team_lead = @existing_sprint.team_lead
- @owner = @product.owner
- raise "Team lead and product owner cannot be the same user!" if
@team_lead.id == @owner.id
-
- @closed_sprint = sprints(:closed_sprint)
- raise "Sprint must be closed!" unless @closed_sprint.closed?
-
- @active_sprint_only = sprints(:teatime_iphone_active_sprint)
- raise "Sprint must be in the active state!" unless
@active_sprint_only.active?
- raise "Product cannot have a planned sprint!" unless
Sprint.for_product((a)active_sprint_only).planned.empty?
- end
-
- # Ensures that a sprint has to have a product.
- def test_valid_fails_without_product
+ @new_sprint = Sprint.new(:product => products(:projxp_web),
+ :title => "Inaugural sprint",
+ :starts => Date.today,
+ :duration => 14,
+ :goals => "To get stuff done!",
+ :team_lead => users(:mcpierce)
+ )
+ @planned_sprint = sprints(:planned_sprint)
+ raise "Sprint must be in planning!" unless @planned_sprint.planning?
+ raise "Product must have an active sprint!" if
Sprint.for_product((a)planned_sprint.product).active.empty?
+
+ @active_sprint = sprints(:active_sprint)
+ raise "Sprint must be active!" unless @active_sprint.active?
+ raise "Product must have a planned sprint!" if
Sprint.for_product((a)active_sprint.product).planned.empty?
+
+ @completed_sprint = sprints(:completed_sprint)
+ raise "Sprint must be completed!" unless @completed_sprint.completed?
+ raise "Product must have an active sprint!" if
Sprint.for_product((a)completed_sprint.product).active.empty?
+
+ @cancelled_sprint = sprints(:cancelled_sprint)
+ raise "Sprint must be cancelled!" unless @cancelled_sprint.cancelled?
+
+ @startable_sprint = sprints(:startable_sprint)
+ raise "Sprint must be planned." unless @startable_sprint.planning?
+ raise "Product must not have an active sprint." unless
Sprint.for_product((a)startable_sprint).active.empty?
+
+ @restartable_completed_sprint = sprints(:restartable_completed_sprint)
+ raise "Sprint must be completed!" unless
@restartable_completed_sprint.completed?
+ raise "Product must not have an active sprint." unless
Sprint.for_product((a)restartable_completed_sprint.product).active.empty?
+
+ @restartable_cancelled_sprint = sprints(:restartable_cancelled_sprint)
+ raise "Sprint must be cancelled!" unless
@restartable_cancelled_sprint.cancelled?
+ raise "Product must not have an active sprint." unless
Sprint.for_product((a)restartable_cancelled_sprint.product).active.empty?
+ end
+
+ # Ensures a product id is required.
+ def test_valid_fails_without_product_id
@new_sprint.product_id = nil
-
- flunk 'A sprint must have a product.' if @new_sprint.valid?
+ flunk "A sprint must require a product." if @new_sprint.valid?
end
- # Ensures that a sprint has to have a title.
+ # Ensures a title is required.
def test_valid_fails_without_title
@new_sprint.title = nil
-
- flunk 'A sprint must have a title.' if @new_sprint.valid?
+ flunk "A sprint must require a title." if @new_sprint.valid?
end
- # Ensures that a sprint has to have a start date.
- def test_valid_fails_without_start
- @new_sprint.start = nil
+ # Ensures a title cannot be an empty string.
+ def test_valid_fails_for_empty_title
+ @new_sprint.title = ''
+ flunk "A sprint title cannot be empty." if @new_sprint.valid?
+ end
- flunk 'A sprint must have a start date.' if @new_sprint.valid?
+ # Ensures a start date is required.
+ def test_valid_fails_without_start_date
+ @new_sprint.starts = nil
+ flunk "A sprint must have a start date." if @new_sprint.valid?
end
- # Ensures that a sprint has a defined duration.
+ # Ensures a duration is required.
def test_valid_fails_without_duration
@new_sprint.duration = nil
+ flunk "A sprint must have a duration." if @new_sprint.valid?
+ end
- flunk 'A sprint must have a duration.' if @new_sprint.valid?
-
- @new_sprint.duration = 'a'
-
- flunk 'A sprint must have a numeric duration.' if @new_sprint.valid?
+ # Ensures a duration must be a positive integer.
+ def test_valid_fails_with_negative_duration
+ @new_sprint.duration = -1
+ flunk "A sprint must have a positive duration." if @new_sprint.valid?
end
- # Ensures that a sprint has goals.
+ # Ensures a sprint requires set of goals.
def test_valid_fails_without_goals
- @new_sprint.goals = ''
+ @new_sprint.goals = nil
+ flunk "A sprint must have goals." if @new_sprint.valid?
+ end
- flunk 'A sprint must have goals.' if @new_sprint.valid?
+ # Ensures that goals must have content.
+ def test_valid_fails_with_empty_goals
+ @new_sprint.goals = ''
+ flunk "A sprint must define goals." if @new_sprint.valid?
end
- # Ensures that a sprint must have a team lead.
- def test_valid_fails_without_team_lead
+ # Ensures that a team lead is defined.
+ def test_valid_fails_without_team_lead_id
@new_sprint.team_lead_id = nil
-
- flunk 'A sprint must have a team lead.' if @new_sprint.valid?
+ flunk "A sprint must have a team lead." if @new_sprint.valid?
end
- # Ensures that an unhealthy sprint returns the appropriate value.
- def test_health_for_unhealthy_sprint
- @new_sprint.backlog_items = @unhealthy_backlog
-
- assert_equal false, @new_sprint.healthy?, 'Sprint should not be considered
healthy.'
+ # Ensures that a well-formed sprint is valid.
+ def test_valid
+ unless @new_sprint.valid?
+ puts @new_sprint.errors.full_messages
+ flunk "There is a general validation error."
+ end
end
- # Ensures that a healthy sprint returns the appropriate value.
- def test_health_for_healthy_sprint
- @new_sprint.backlog_items = @healthy_backlog
-
- assert @new_sprint.healthy?, 'Sprint should be considered healthy.'
+ # Ensures that nobody can edit a completed sprint.
+ def test_can_edit_with_completed_sprint
+ flunk "Completed sprints must not be edited by the team lead." if
@completed_sprint.can_edit?((a)completed_sprint.team_lead)
+ flunk "Completed sprints must not be edited by the product owner." if
@completed_sprint.can_edit?((a)completed_sprint.product.owner)
end
- # Ensures the estimated hours for a sprint are accurate.
- def test_estimated_hours
- assert_equal 17.5,
- @existing_sprint.estimated_hours,
- 'The estimated hours are wrong.'
+ # Ensures that nobody can edit a cancelled sprint.
+ def test_can_edit_with_cancelled_sprint
+ flunk "Cancelled sprints must not be edited by the team lead." if
@cancelled_sprint.can_edit?((a)cancelled_sprint.team_lead)
+ flunk "Cancelled sprints must not be edited by the product owner." if
@cancelled_sprint.can_edit?((a)cancelled_sprint.product.owner)
end
- # Ensures the actual hours for a sprint are accurate.
- def test_actual_hours
- assert_equal 11.5,
- @existing_sprint.actual_hours,
- 'The actual hours are wrong.'
+ # Ensures the team lead can edit an active sprint.
+ def test_can_edit_as_team_lead
+ flunk "Team lead must be able to edit an active sprint." unless
@active_sprint.can_edit?((a)active_sprint.team_lead)
end
- # Ensures the remaining hours are correct.
- def test_remaining_hours
- assert_equal 13.0,
- @existing_sprint.remaining_hours.to_f,
- 'The remaining hours are wrong.'
+ # Ensures that only one planned sprint is allowed.
+ def test_plan_fails_if_there_is_a_planned_sprint
+ flunk "There's already a planning sprint." if @active_sprint.can_plan?
end
- # Ensures the end date calculation is accurate.
- def test_end_date
- # the sprint was set at 14 days, starting 7 days ago, so the end date should
- # be 6 days from now
- assert_equal @new_sprint.start + (@new_sprint.duration - 1), @new_sprint.end_date,
- "End date calculation is wrong."
+ # Ensures that only one sprint may be active.
+ def test_start_fails_if_there_is_an_active_sprint
+ flunk "There's already an active sprint." if
@planned_sprint.can_start?
+ flunk "There's already an active sprint." if
@completed_sprint.can_restart?
+ flunk "There's already an active sprint." if
@cancelled_sprint.can_restart?
end
- # Ensures that a team lead can edit a sprint.
- def test_edit_as_team_lead
- flunk "Team leads must be allowed to edit sprints." unless
@existing_sprint.can_edit?(@team_lead)
+ # Ensures that a planned sprint can be started.
+ def test_start
+ flunk "A sprint must be startable." unless @startable_sprint.can_start?
end
- # Ensures that a product owner can edit a sprint.
- def test_edit_as_product_owner
- flunk "Product owners must be allowed to edit sprints." unless
@existing_sprint.can_edit?(@owner)
+ # Ensures that an active sprint can be cancelled.
+ def test_cancel
+ flunk "A sprint must be cancellable." unless @active_sprint.can_cancel?
end
- # Ensures that a sprint that a sprint that's not active cannot add a backlog item.
- def test_can_add_backlog_items_for_inactive_sprint
- flunk "Inactive sprints cannot add backlog items." if
@closed_sprint.can_add_backlog_items?((a)closed_sprint.team_lead)
+ # Ensures that an active sprint can be completed.
+ def test_complete
+ flunk "A sprint must be completable." unless @active_sprint.can_complete?
end
- # Ensures that a sprint cannot be moved back to planned state if a planned sprint
exists.
- def test_allowed_state_planned_with_planned_sprint_planned
- flunk "Product cannot have two planned sprints." if
@active_sprint.allowed_status?(Sprint::STATUS_PLANNED)
+ # Ensures that a completed sprint can be restarted.
+ def test_restart_for_completed
+ flunk "A completed sprint must be restartable." unless
@restartable_completed_sprint.can_restart?
end
- # Ensures that a sprint can be moved back to the planned state.
- def test_allowed_state_planned
- flunk "A sprint can move back to the planned state." unless
@active_sprint_only.allowed_status?(Sprint::STATUS_PLANNED)
+ # Ensures that a cancelled sprint can be restarted.
+ def test_restart_for_cancelled
+ flunk "A cancelled sprint must be restartable." unless
@restartable_cancelled_sprint.can_restart?
end
end
diff --git a/test/unit/task_test.rb b/test/unit/task_test.rb
index 4afb061..39e1d2d 100644
--- a/test/unit/task_test.rb
+++ b/test/unit/task_test.rb
@@ -28,7 +28,7 @@ class TaskTest < ActiveSupport::TestCase
:primary_id => @backlog_item.owner_id,
:description => 'This is a task',
:hours => 5,
- :when_entered => @backlog_item.sprint.start + (@backlog_item.sprint.duration /
2))
+ :when_entered => @backlog_item.sprint.starts + (@backlog_item.sprint.duration /
2))
@new_task.backlog_item = @backlog_item
@backup = users(:jdonuts)
@@ -93,14 +93,14 @@ class TaskTest < ActiveSupport::TestCase
# Ensures that the date for a task can't be before the sprint starts.
def test_valid_fails_when_date_preceeds_sprint
- @new_task.when_entered = @backlog_item.sprint.start - 1
+ @new_task.when_entered = @backlog_item.sprint.starts - 1
flunk "Date cannot be before the sprint starts." if @new_task.valid?
end
# Ensures that the date for a task can't be after the sprint ends.
def test_valid_fail_when_date_exceeds_sprint
- @new_task.when_entered = @backlog_item.sprint.start + @backlog_item.sprint.duration
+ @new_task.when_entered = @backlog_item.sprint.starts + @backlog_item.sprint.duration
flunk "Date cannot be after the sprint ends." if @new_task.valid?
end
--
1.6.0.6