Added a new table, user_stories_messages, that maps a Message back to
the UserStory against which it was posted.
Also cleaned up the user_story.rb and user_story_test.rb files.
Added a new business rule check, UserStory.can_comment?, that returns
whether the specified user is allowed to comment on the given user
story.
Created a new view for editing a message. Also a new action
StoriesController::comment for posting comments.
Signed-off-by: Darryl L. Pierce <mcpierce(a)gmail.com>
---
app/controllers/application.rb | 4 +-
app/controllers/sprints_controller.rb | 4 +-
app/controllers/stories_controller.rb | 47 +++++++++--
app/models/message.rb | 19 +++--
app/models/product.rb | 11 +--
app/models/product_role.rb | 4 +
app/models/user_story.rb | 53 +++++++------
app/views/messages/_edit.html.erb | 21 +++++
app/views/messages/_list.html.erb | 19 +++++
app/views/sprints/_edit.html.erb | 2 +-
app/views/stories/show.html.erb | 8 ++
config/routes.rb | 5 +-
db/migrate/022_create_user_stories_messages.rb | 34 ++++++++
doc/ChangeLog | 1 +
test/fixtures/messages.yml | 5 +
test/fixtures/user_stories_messages.yml | 3 +
test/functional/stories_controller_test.rb | 102 ++++++++++++++++++------
test/unit/user_story_test.rb | 51 ++++++++----
18 files changed, 296 insertions(+), 97 deletions(-)
create mode 100644 app/views/messages/_edit.html.erb
create mode 100644 app/views/messages/_list.html.erb
create mode 100644 db/migrate/022_create_user_stories_messages.rb
create mode 100644 test/fixtures/user_stories_messages.yml
diff --git a/app/controllers/application.rb b/app/controllers/application.rb
index 899db21..dd60773 100644
--- a/app/controllers/application.rb
+++ b/app/controllers/application.rb
@@ -75,11 +75,11 @@ class ApplicationController < ActionController::Base
# Redirects the user to the error page and displays
# the provided message.
- def report_error(message)
+ def report_error(message, url=nil)
flash[:error] = message
respond_to do |format|
- format.html { redirect_to error_path }
+ format.html { redirect_to url ? url : error_path }
end
end
diff --git a/app/controllers/sprints_controller.rb
b/app/controllers/sprints_controller.rb
index 31c3d66..b2e4c52 100644
--- a/app/controllers/sprints_controller.rb
+++ b/app/controllers/sprints_controller.rb
@@ -289,7 +289,7 @@ class SprintsController < ApplicationController
def load_product
@product = Product.find_by_id(params[:product])
- @members = @product.users if @product
+ @members = @product.members if @product
@project = @product.project if @product
report_error "Missing or invalid product." unless @product
@@ -317,7 +317,7 @@ class SprintsController < ApplicationController
def prepare_for_edit
@selected = Array.new
- @members = @product.users
+ @members = @product.members
end
def add_users_to_sprint(selected)
diff --git a/app/controllers/stories_controller.rb
b/app/controllers/stories_controller.rb
index 8db78e4..5bf1b44 100644
--- a/app/controllers/stories_controller.rb
+++ b/app/controllers/stories_controller.rb
@@ -16,15 +16,17 @@
# +StoriesController+ performs CRUD operations for instances of +UserStory+.
class StoriesController < ApplicationController
- before_filter :authenticated, :except => [:index, :show]
- before_filter :load_product, :only => [:new, :create]
- before_filter :load_user_story, :except => [:index, :new, :create]
- before_filter :load_epics, :only => [:new, :edit, :create, :update]
- before_filter :verify_can_create, :only => [:new, :create]
- before_filter :verify_can_edit, :only => [:edit, :update]
- before_filter :verify_can_delete, :only => [:destroy]
- before_filter :path_to_list, :only => [:index, :new, :create]
- before_filter :path_to_one, :only => [:show, :edit, :update]
+ before_filter :authenticated, :except => [:index, :show]
+ before_filter :load_product, :only => [:new, :create]
+ before_filter :load_user_story, :except => [:index, :new, :create]
+ before_filter :load_epics, :only => [:new, :edit, :create, :update]
+ before_filter :verify_can_create, :only => [:new, :create]
+ before_filter :verify_can_edit, :only => [:edit, :update]
+ before_filter :verify_can_delete, :only => [:destroy]
+ before_filter :verify_can_comment, :only => [:comment]
+ before_filter :load_comment, :only => [:comment]
+ before_filter :path_to_list, :only => [:index, :new, :create]
+ before_filter :path_to_one, :only => [:show, :edit, :update]
# GET /stories
def index
@@ -39,6 +41,7 @@ class StoriesController < ApplicationController
# GET /stories/1
def show
@title = "User Story - #{(a)user_story.title}"
+ @comment = Message.new(:author => @user) if @user_story.can_comment?(@user)
respond_to do |format|
format.html
end
@@ -122,6 +125,24 @@ class StoriesController < ApplicationController
end
end
+ # POST /stories/1/comment
+ def comment
+ Message.transaction do
+ respond_to do |format|
+ @comment.author = @user
+ @comment.posted = Time.current
+ if @comment.valid?
+ @user_story.comments << @comment
+ @user_story.save!
+ flash[:message] = "Comment saved."
+ format.html {redirect_to story_path(@user_story)}
+ else
+ format.html {render :action => :show}
+ end
+ end
+ end
+ end
+
private
def load_product
@@ -151,6 +172,14 @@ class StoriesController < ApplicationController
report_error "You are not allowed to deslete this user story." unless
@user_story.can_delete?(@user)
end
+ def verify_can_comment
+ report_error("You are not allowed to comment on this user story.",
story_path(@user_story)) unless @user_story.can_comment?(@user)
+ end
+
+ def load_comment
+ @comment = Message.new(params[:message])
+ end
+
def load_epics
@epics = Epic.find_all_by_project_id((a)project.id, :order => 'priority',
:conditions => "closed = false")
end
diff --git a/app/models/message.rb b/app/models/message.rb
index 58966f4..ffb49f0 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -17,15 +17,16 @@
# A +Message+ represents a single text body written by a user.
class Message < ActiveRecord::Base
- validates_presence_of :author_id, :message => 'An author is required.'
-
- validates_presence_of :body, :message => 'A message body is required.'
-
- validates_length_of :body, :within => 5..1000,
- :too_long => 'Maximum message length is 1000 characters.',
- :too_short => 'Please enter at least 5 characters.'
-
- validates_presence_of :posted, :message => 'A posting date is required.'
+ validates_presence_of(:author_id,
+ :message => 'An author is required.')
+ validates_presence_of(:body,
+ :message => 'A message body is required.')
+ validates_length_of(:body,
+ :within => 5..1000,
+ :too_long => 'Maximum message length is 1000
characters.',
+ :too_short => 'Please enter at least 5 characters.')
+ validates_presence_of(:posted,
+ :message => 'A posting date is required.')
belongs_to :author, :class_name => 'User', :foreign_key => :author_id
diff --git a/app/models/product.rb b/app/models/product.rb
index 9abcb8d..cc238fa 100644
--- a/app/models/product.rb
+++ b/app/models/product.rb
@@ -34,12 +34,10 @@ class Product < ActiveRecord::Base
:message => 'Mailing list must be a valid email address.'
belongs_to :project
- belongs_to :owner,
- :class_name => 'User',
- :foreign_key => 'owner_id'
+ belongs_to :owner, :class_name => 'User', :foreign_key =>
'owner_id'
has_many :product_roles, :dependent => :destroy
- has_many :users, :through => :product_roles
+ has_many :members, :through => :product_roles, :source => :user
has_many :user_stories, :dependent => :destroy
has_many :sprints, :dependent => :destroy
has_and_belongs_to_many :rss_entries, :class_name => 'Feed', :join_table
=> :product_feeds, :order => 'created_at DESC'
@@ -106,10 +104,7 @@ class Product < ActiveRecord::Base
# Returns whether the specified user is a member of the product team.
def is_member?(user)
- if (user != nil)
- role = ProductRole.find_by_product_id_and_user_id(self.id, user.id)
- return role.approved if role
- end
+ user && !ProductRole.for_product(self).for_user(user).approved.empty?
end
# Returns whether the user has a pending membership.
diff --git a/app/models/product_role.rb b/app/models/product_role.rb
index 38164a3..fe77f43 100644
--- a/app/models/product_role.rb
+++ b/app/models/product_role.rb
@@ -32,6 +32,10 @@ class ProductRole < ActiveRecord::Base
belongs_to :user
belongs_to :role
+ named_scope :for_product, lambda {|product_id| {:conditions => ['product_id =
?', product_id]}}
+ named_scope :approved, {:conditions => 'is_approved = true and pending =
false'}
+ named_scope :for_user, lambda {|user_id| {:conditions => ['user_id = ?',
user_id]}}
+
# Sets the status for a pending role.
def approved=(status)
self.pending = false
diff --git a/app/models/user_story.rb b/app/models/user_story.rb
index 4a5f8a6..f1ad634 100644
--- a/app/models/user_story.rb
+++ b/app/models/user_story.rb
@@ -17,37 +17,35 @@
# +UserStory+ represents a single user story.
#
+# A user story can be related back to an +Epic+ story, which is a project level
+# feature. User stories are the expression of that epic within a single product.
+#
+# A user story can have one or more +BacklogItem+ objects that refer to it.
+#
class UserStory < ActiveRecord::Base
- validates_presence_of :product_id,
- :message => 'All user stories must belong to a product.'
-
- validates_presence_of :priority,
- :message => 'A user story must have a priority.'
- validates_numericality_of :priority,
- :message => 'The priority must be an integer value',
- :only_integer => true,
- :greater_than => 0
-
- validates_presence_of :title,
- :message => 'You must include a title for the user story.'
- validates_length_of :title,
- :message => 'The title must be at least 5 characters long.',
- :minimum => 1
+ validates_presence_of(:product_id,
+ :message => 'All user stories must belong to a
product.')
+ validates_presence_of(:priority,
+ :message => 'A user story must have a priority.')
+ validates_numericality_of(:priority,
+ :message => 'The priority must be an integer
value',
+ :only_integer => true,
+ :greater_than => 0)
+ validates_presence_of(:title,
+ :message => 'You must include a title for the user
story.')
+ validates_length_of(:title,
+ :message => 'The title must be at least 5 characters
long.',
+ :minimum => 1)
belongs_to :product
belongs_to :epic
has_many :backlog_items
+ has_and_belongs_to_many :comments, :join_table => :user_stories_messages,
:class_name => 'Message'
- named_scope :default, { :order => 'priority ASC' }
- named_scope :for_product, lambda { |product_id|
- { :conditions => product_id ? ["product_id = ?", product_id] : [] }
- }
- named_scope :for_epic, lambda { |epic_id|
- {:conditions => ['epic_id = ?', epic_id]}
- }
- named_scope :not_in_sprint, lambda { |sprint_id|
- {:conditions => ['id not in (select user_story_id from backlog_items where
sprint_id = ?)', sprint_id]}
- }
+ named_scope :default, {:order => 'priority ASC'}
+ named_scope :for_product, lambda {|product_id| {:conditions => product_id ?
["product_id = ?", product_id] : []}}
+ named_scope :for_epic, lambda {|epic_id| {:conditions => ['epic_id = ?',
epic_id]}}
+ named_scope :not_in_sprint, lambda {|sprint_id| {:conditions => ['id not in
(select user_story_id from backlog_items where sprint_id = ?)', sprint_id]}}
named_scope :open, {:conditions => 'closed = false'}
# Returns whether the user can edit this user story.
@@ -55,6 +53,11 @@ class UserStory < ActiveRecord::Base
user && (user.id == product.owner_id)
end
+ # Returns whether the user can comment on this user story.
+ def can_comment?(user)
+ product.is_member?(user)
+ end
+
# Returns whether the user can delete this user story.
def can_delete?(user)
user && (user.id == product.owner_id) && can_be_deleted?
diff --git a/app/views/messages/_edit.html.erb b/app/views/messages/_edit.html.erb
new file mode 100644
index 0000000..e49f442
--- /dev/null
+++ b/app/views/messages/_edit.html.erb
@@ -0,0 +1,21 @@
+<% form_for(:message, message, :url => url) do |form| %>
+
+<table class="edit">
+ <caption><%= "#{caption}" %></caption>
+ <tbody>
+ <tr>
+ <td>
+ <%= form.text_area :body, :rows => 10, :cols => 100 %>
+ <%= error_message_on(message, :body) %>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="buttons" colspan="2">
+ <%= submit_tag "Post" %>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<% end %>
diff --git a/app/views/messages/_list.html.erb b/app/views/messages/_list.html.erb
new file mode 100644
index 0000000..a08b514
--- /dev/null
+++ b/app/views/messages/_list.html.erb
@@ -0,0 +1,19 @@
+<table class="main-list">
+ <caption><%= caption %></caption>
+ <thead>
+ <tr>
+ <th span="col">#</th>
+ <th span="col" class="name"></th>
+ <th span="col">Posted</th>
+ </tr>
+ </thead>
+ <tbody>
+ <% messages.each do |message| %>
+ <tr class="<%= cycle('odd', 'even') %>">
+ <td><%= link_to "#{message.id}", message_path(message)
%></td>
+ <td><%= RedCloth.new(message.body).to_html %></td>
+ <td><%= show_date(message.posted, true) %></td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
diff --git a/app/views/sprints/_edit.html.erb b/app/views/sprints/_edit.html.erb
index 31b1231..31d60db 100644
--- a/app/views/sprints/_edit.html.erb
+++ b/app/views/sprints/_edit.html.erb
@@ -16,7 +16,7 @@
<tr>
<td class="label-required">Team lead:</td>
<td class="value">
- <%= collection_select :sprint, :team_lead_id, @product.users, :id,
+ <%= collection_select :sprint, :team_lead_id, @product.members, :id,
:display_name, {:include_blank => false} %>
<%= error_message_on(:sprint, :team_lead_id) %>
</td>
diff --git a/app/views/stories/show.html.erb b/app/views/stories/show.html.erb
index a2ca708..85afce2 100644
--- a/app/views/stories/show.html.erb
+++ b/app/views/stories/show.html.erb
@@ -5,6 +5,14 @@
<dd><%= RedCloth.new((a)user_story.description).to_html %></dd>
</dl>
</div>
+
+ <% if @user_story.can_comment?(@user) %>
+ <%= render :partial => 'messages/edit',
+ :locals => {:caption => "Comment On This Story", :url =>
comment_story_path(@user_story),:message => @comment} %>
+ <% end %>
+
+ <%= render :partial => 'messages/list', :locals => {:caption =>
"User Story Comments", :messages => @user_story.comments} %>
+
</div>
<% render :layout => 'home/sidebar', :locals => {:title => 'User
Story Commands'} do %>
diff --git a/config/routes.rb b/config/routes.rb
index 79f0cc1..4c62ef3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -40,7 +40,10 @@ ActionController::Routing::Routes.draw do |map|
:reject => :put
}
- map.resources :stories
+ map.resources :stories, :member =>
+ {
+ :comment => :post,
+ }
map.resources :products, :member =>
{
diff --git a/db/migrate/022_create_user_stories_messages.rb
b/db/migrate/022_create_user_stories_messages.rb
new file mode 100644
index 0000000..c773c4f
--- /dev/null
+++ b/db/migrate/022_create_user_stories_messages.rb
@@ -0,0 +1,34 @@
+# 022_create_user_stories_messages.rb
+# Copyright (C) 2009, Darryl L. Pierce <mcpierce(a)gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <
http://www.gnu.org/licenses/>.
+#
+
+class CreateUserStoriesMessages < ActiveRecord::Migration
+ def self.up
+ create_table :user_stories_messages do |t|
+ t.integer :user_story_id, :null => false
+ t.integer :message_id, :null => false
+ end
+
+ execute 'alter table user_stories_messages add constraint
fk_user_stories_messages_user_story
+ foreign key (user_story_id) references user_stories(id)'
+ execute 'alter table user_stories_messages add constraint
fk_user_stories_messages_message
+ foreign key (message_id) references messages(id)'
+ end
+
+ def self.down
+ drop_table :user_stories_messages
+ end
+end
diff --git a/doc/ChangeLog b/doc/ChangeLog
index 023e95e..2abdbaa 100644
--- a/doc/ChangeLog
+++ b/doc/ChangeLog
@@ -3,6 +3,7 @@ Change Log (0.3.0):
* #157 - Items can be marked completed when a task is added.
* #159 - Users can watch an RSS feed of product activity.
* #160 - A product's RSS feed is displayed on the product details page.
+ * #161 - Users can post comments against user stories.
* #167 - Blocker messages are included in the daily updates email.
* #173 - Backlog items can be dropped from an active sprint.
* #174 - Deferred backlog items can be re-added to the active sprint.
diff --git a/test/fixtures/messages.yml b/test/fixtures/messages.yml
index 01c1baf..55984a6 100644
--- a/test/fixtures/messages.yml
+++ b/test/fixtures/messages.yml
@@ -1,3 +1,8 @@
+user_story_message:
+ author_id: <%= Fixtures.identify(:mcpierce) %>
+ posted: <%= Time.current %>
+ body: This user story is awesome. It's my personal favorite!
+
blocker_message:
author_id: <%= Fixtures.identify(:mcpierce) %>
posted: <%= Time.current %>
diff --git a/test/fixtures/user_stories_messages.yml
b/test/fixtures/user_stories_messages.yml
new file mode 100644
index 0000000..3b8d014
--- /dev/null
+++ b/test/fixtures/user_stories_messages.yml
@@ -0,0 +1,3 @@
+create_user_story_message:
+ user_story_id: <%= Fixtures.identify(:create_user_story) %>
+ message_id: <%= Fixtures.identify(:user_story_message) %>
diff --git a/test/functional/stories_controller_test.rb
b/test/functional/stories_controller_test.rb
index 1f17bf1..7dad60b 100644
--- a/test/functional/stories_controller_test.rb
+++ b/test/functional/stories_controller_test.rb
@@ -30,9 +30,15 @@ class StoriesControllerTest < ActionController::TestCase
@nonowner = users(:mcpierce)
raise "Owner and nonowner can't be the same person." if @owner.id ==
@nonowner.id
- @existing_story = user_stories(:freerange_user_story)
- raise "Story must use the right product." unless @existing_story.product_id
== @product.id
- raise "Story must be deletable." unless @existing_story.can_be_deleted?
+ @story = user_stories(:freerange_user_story)
+ raise "Story must use the right product." unless @story.product_id ==
@product.id
+ raise "Story must be deletable." unless @story.can_be_deleted?
+
+ @member = ProductRole.for_product((a)story.product).approved.first.user
+ raise "No members were found!" unless @member
+
+ @nonmember = users(:celliot)
+ raise "Non-member cannot have a product role!" if
@story.product.is_member?(@nonmember)
@story_with_backlog_items = user_stories(:create_login)
raise "Story must use the right product." unless
@story_with_backlog_items.product_id == @product.id
@@ -42,6 +48,10 @@ class StoriesControllerTest < ActionController::TestCase
:title => 'This is a new user story',
:priority => 17
}
+
+ @new_comment = {
+ :body => "This is a comment."
+ }
end
# Ensures that all stories by product are viewable.
@@ -72,11 +82,11 @@ class StoriesControllerTest < ActionController::TestCase
# Ensures that showing a user story works.
def test_show
- get :show, {:id => @existing_story.id}
+ get :show, {:id => @story.id}
assert_response :success
assert assigns['user_story'], "Failed to load a user story."
- assert_equal @existing_story.id, assigns['user_story'].id,
+ assert_equal @story.id, assigns['user_story'].id,
"Failed to load the correct user story."
end
@@ -126,19 +136,19 @@ class StoriesControllerTest < ActionController::TestCase
# Ensures that only the product owner can edit a user story.
def test_edit_as_nonowner
- get :edit, {:id => @existing_story.id}, {:user_id => @nonowner.id}
+ get :edit, {:id => @story.id}, {:user_id => @nonowner.id}
assert_redirected_to error_path
end
# Ensures that editing a user story works as expected.
def test_edit
- get :edit, { :id => @existing_story.id}, {:user_id => @owner.id}
+ get :edit, { :id => @story.id}, {:user_id => @owner.id}
assert_response :success
assert assigns['user_story'], "Failed to load a user story to
edit."
assert assigns['epics'], "Failed to load the set of epics."
- assert_equal @existing_story.id, assigns['user_story'].id,
+ assert_equal @story.id, assigns['user_story'].id,
"Failed to load the correct user story."
end
@@ -213,39 +223,39 @@ class StoriesControllerTest < ActionController::TestCase
# Ensures that only the product owner can update a user story.
def test_update_as_nonowner
- put :update, { :id => @existing_story.id, :user_story => {:title =>
'Updated'}}, {:user_id => @nonowner.id}
+ put :update, { :id => @story.id, :user_story => {:title =>
'Updated'}}, {:user_id => @nonowner.id}
assert_redirected_to error_path
- result = UserStory.find_by_id((a)existing_story.id)
- assert_equal @existing_story.title, result.title, "User story should not have
been updated."
+ result = UserStory.find_by_id((a)story.id)
+ assert_equal @story.title, result.title, "User story should not have been
updated."
end
# Ensures that an invalid user story gets sent back for editing.
def test_update_with_invalid_user_story
- put :update, { :id => @existing_story.id, :user_story => {:title =>
''}}, {:user_id => @owner.id}
+ put :update, { :id => @story.id, :user_story => {:title => ''}},
{:user_id => @owner.id}
assert_response :success
- result = UserStory.find_by_id((a)existing_story.id)
- assert_equal @existing_story.title, result.title, "User story should not have
been updated."
+ result = UserStory.find_by_id((a)story.id)
+ assert_equal @story.title, result.title, "User story should not have been
updated."
end
# Ensures that updates work as expected.
def test_update
update = {:title => "This ia the new title for an existing story"}
- put :update, { :id => @existing_story.id, :user_story => update}, {:user_id
=> @owner.id}
+ put :update, { :id => @story.id, :user_story => update}, {:user_id =>
@owner.id}
assert_redirected_to stories_path(:product => @product)
- result = UserStory.find_by_id((a)existing_story.id)
+ result = UserStory.find_by_id((a)story.id)
assert_equal update[:title], result.title, "User story should have been
updated."
end
# Ensures that updates return to a url when one is supplied.
def test_update_with_source
update = {:title => "This ia the new title for an existing story"}
- put :update,{ :id => @existing_story.id, :user_story => update, :source =>
"/farkle"}, {:user_id => @owner.id}
+ put :update,{ :id => @story.id, :user_story => update, :source =>
"/farkle"}, {:user_id => @owner.id}
assert_redirected_to "/farkle"
- result = UserStory.find_by_id((a)existing_story.id)
+ result = UserStory.find_by_id((a)story.id)
assert_equal update[:title], result.title, "User story should have been
updated."
end
@@ -265,10 +275,10 @@ class StoriesControllerTest < ActionController::TestCase
# Ensures that only the owner can delete a user story.
def test_destroy_as_nonowner
- delete :destroy, { :id => @existing_story.id}, {:user_id => @nonowner.id}
+ delete :destroy, { :id => @story.id}, {:user_id => @nonowner.id}
assert_redirected_to error_path
- assert UserStory.find_by_id((a)existing_story.id), "User story should not have
been deleted."
+ assert UserStory.find_by_id((a)story.id), "User story should not have been
deleted."
end
# Ensures that user stories associated with backlog items can't be deleted.
@@ -281,17 +291,61 @@ class StoriesControllerTest < ActionController::TestCase
# Ensures that deleting a user story works as expected.
def test_destroy
- delete :destroy, {:id => @existing_story.id}, {:user_id => @owner.id}
+ delete :destroy, {:id => @story.id}, {:user_id => @owner.id}
assert_redirected_to stories_path(:product => @product)
- assert !UserStory.find_by_id((a)existing_story.id), "User story should have been
deleted."
+ assert !UserStory.find_by_id((a)story.id), "User story should have been
deleted."
end
# Ensures that deleting returns to the url if one is supplied.
def test_destroy_with_source
- delete :destroy,{ :id => @existing_story.id, :source => "/farkle"},
{:user_id => @owner.id}
+ delete :destroy,{ :id => @story.id, :source => "/farkle"}, {:user_id
=> @owner.id}
assert_redirected_to "/farkle"
- assert !UserStory.find_by_id((a)existing_story.id), "User story should have been
deleted."
+ assert !UserStory.find_by_id((a)story.id), "User story should have been
deleted."
+ end
+
+ # Ensures that an anonymous user cannot comment on a story.
+ def test_comment_as_anonymous
+ post :comment
+
+ assert_redirected_to login_path
+ end
+
+ # Ensures that a valid story is is required.
+ def test_comment_with_invalid_id
+ post :comment, {}, {:user_id => @member.id}
+
+ assert_redirected_to error_path
+ end
+
+ # Ensures that only product members can comment on stories.
+ def test_comment_as_nonmember
+ count = @story.comments.size
+ post :comment, {:id => @story.id}, {:user_id => @nonmember.id}
+
+ assert_redirected_to story_path(@story)
+ result = UserStory.find_by_id((a)story.id)
+ assert_equal count, result.comments.size, "Comment should not have been
posted."
+ end
+
+ # Ensures that a comment body is required.
+ def test_comment_without_text
+ count = @story.comments.size
+ post :comment, {:id => @story.id}, {:user_id => @member.id}
+
+ assert_template 'stories/show'
+ result = UserStory.find_by_id((a)story.id)
+ assert_equal count, result.comments.size, "Comment should not have been
posted."
+ end
+
+ # Ensures that a comment is posted properly.
+ def test_comment
+ count = @story.comments.size
+ post :comment, {:id => @story.id, :message => @new_comment}, {:user_id =>
@member.id}
+
+ assert_redirected_to story_path(@story)
+ result = UserStory.find_by_id((a)story.id)
+ assert_equal count + 1, result.comments.size, "Comment should have been
saved!"
end
end
diff --git a/test/unit/user_story_test.rb b/test/unit/user_story_test.rb
index d3db6bb..05c922e 100644
--- a/test/unit/user_story_test.rb
+++ b/test/unit/user_story_test.rb
@@ -21,57 +21,76 @@ class UserStoryTest < ActiveSupport::TestCase
fixtures :user_stories
def setup
- @user_story = UserStory.new(
- :product_id => 1,
- :priority => 1,
- :title => 'This is a new user story.')
+ @new_story = UserStory.new(:product_id => 1,
+ :priority => 1,
+ :title => 'This is a new user story.')
+ @story = user_stories(:create_user_story)
+ @nonmember = users(:celliot)
+ raise "Non-member cannot be on the product team!" if
@story.product.is_member?(@nonmember)
+ @member = ProductRole.for_product((a)story.product).approved.first.user
+ raise "No approved members were found!" unless @member
end
# Ensures that a product is required.
#
def test_valid_fails_without_product
- @user_story.product = nil
+ @new_story.product = nil
- flunk "A product must be required." if @user_story.valid?
+ flunk "A product must be required." if @new_story.valid?
end
# Ensures that a priority is required.
#
def test_valid_fails_without_priority
- @user_story.priority = nil
+ @new_story.priority = nil
- flunk "A priority is required." if @user_story.valid?
+ flunk "A priority is required." if @new_story.valid?
end
# Ensures that a priorty must be a numerical value.
#
def test_valid_fails_with_nonnumeric_priority
- @user_story.priority = 'abc'
+ @new_story.priority = 'abc'
- flunk "Priority must be a numeric value." if @user_story.valid?
+ flunk "Priority must be a numeric value." if @new_story.valid?
end
# Ensures that a priority must be a positive integer.
#
def test_valid_fails_with_negative_priority
- @user_story.priority = -1
+ @new_story.priority = -1
- flunk "Priority must not be negative." if @user_story.valid?
+ flunk "Priority must not be negative." if @new_story.valid?
end
# Ensures that the priority cannot be zero.
#
def test_valid_fails_with_zero_priority
- @user_story.priority = 0
+ @new_story.priority = 0
- flunk "Priority must be greater than zero." if @user_story.valid?
+ flunk "Priority must be greater than zero." if @new_story.valid?
end
# Ensures that a title must be supplied.
#
def test_valid_fails_without_title
- @user_story.title = ""
+ @new_story.title = ""
- flunk "A title must be supplied." if @user_story.valid?
+ flunk "A title must be supplied." if @new_story.valid?
+ end
+
+ # Ensures that a user story has messages.
+ def test_messages_is_not_empty
+ flunk "User story must have messages!" if @story.messages.empty?
+ end
+
+ # Ensures that only team members can comment on a user story.
+ def test_can_comment_for_nonmember
+ flunk "Non-members cannot comment on user stories." if
@story.can_comment?(@nonmember)
+ end
+
+ # Ensures comments can be posted.
+ def test_can_comment
+ flunk "Members must be able to comment." unless
@story.can_comment?(@member)
end
end
--
1.6.0.6