Commit 413246bc by jimabramson

add lots of test coverage.

parent 03153453
......@@ -48,6 +48,7 @@ group :test do
gem 'rack-test', :require => "rack/test"
gem 'guard'
gem 'guard-unicorn'
gem 'simplecov', :require => false
end
gem 'newrelic_rpm'
......
......@@ -127,6 +127,10 @@ GEM
rspec-expectations (2.11.2)
diff-lcs (~> 1.1.3)
rspec-mocks (2.11.2)
simplecov (0.7.1)
multi_json (~> 1.0)
simplecov-html (~> 0.7.1)
simplecov-html (0.7.1)
sinatra (1.3.3)
rack (~> 1.3, >= 1.3.6)
rack-protection (~> 1.2)
......@@ -186,6 +190,7 @@ DEPENDENCIES
rdiscount
rest-client
rspec
simplecov
sinatra
tire
tire-contrib
......
......@@ -247,6 +247,43 @@ class CommentThread < Content
end
def to_hash(params={})
# to_hash returns the following model for each thread
# title body course_id anonymous anonymous_to_peers commentable_id
# created_at updated_at at_position_list closed
# (all the above direct from the original document)
# id
# from doc._id
# user_id
# from doc.author_id
# username
# from doc.author_username
# votes
# from subdocument votes - {count, up_count, down_count, point}
# abuse_flaggers
# from original document
# tags
# from orig doc tags_array
# type
# hardcoded "thread"
# group_id
# from orig doc
# pinned
# from orig doc
# endorsed
# if any comment within the thread is endorsed, see above (endorsed?)
# children (if recursive=true)
# will do a separate query for all the children
# unread_comments_count
# assuming this is being called on behalf of a specific user U, this counts the
# number of comments in this thread not authored by U AND whose updated_at
# is greater than (U[last_read_dates][this._id] or 0)
# comments_count
# count across all comments
# read
# currently this checks doc.updated_at (which is kept up to date by callbacks)
# iow, any comment or the thread itself have been updated since i last read
doc = as_document.slice(*%w[title body course_id anonymous anonymous_to_peers commentable_id created_at updated_at at_position_list closed])
.merge("id" => _id, "user_id" => author_id,
"username" => author.username,
......
......@@ -57,13 +57,13 @@ describe "app" do
it "create a new comment thread for the commentable object" do
post '/api/v1/question_1/threads', default_params
last_response.should be_ok
CommentThread.count.should == 3
CommentThread.count.should == 4
CommentThread.where(title: "Interesting question").first.should_not be_nil
end
it "allows anonymous thread" do
post '/api/v1/question_1/threads', default_params.merge(anonymous: true)
last_response.should be_ok
CommentThread.count.should == 3
CommentThread.count.should == 4
c = CommentThread.where(title: "Interesting question").first
c.should_not be_nil
c["anonymous"].should be_true
......@@ -101,7 +101,7 @@ describe "app" do
it "create a new comment thread with tag" do
post '/api/v1/question_1/threads', default_params.merge(tags: "a, b, c")
last_response.should be_ok
CommentThread.count.should == 3
CommentThread.count.should == 4
thread = CommentThread.where(title: "Interesting question").first
thread.tags_array.length.should == 3
thread.tags_array.should include "a"
......@@ -111,7 +111,7 @@ describe "app" do
it "strip spaces in tags" do
post '/api/v1/question_1/threads', default_params.merge(tags: " a, b ,c ")
last_response.should be_ok
CommentThread.count.should == 3
CommentThread.count.should == 4
thread = CommentThread.where(title: "Interesting question").first
thread.tags_array.length.should == 3
thread.tags_array.should include "a"
......@@ -121,7 +121,7 @@ describe "app" do
it "accepts [a-z 0-9 + # - .]words, numbers, dashes, spaces but no underscores in tags" do
post '/api/v1/question_1/threads', default_params.merge(tags: "artificial-intelligence, machine-learning, 7-is-a-lucky-number, interesting problem, interesting problems in c++")
last_response.should be_ok
CommentThread.count.should == 3
CommentThread.count.should == 4
thread = CommentThread.where(title: "Interesting question").first
thread.tags_array.length.should == 5
end
......
......@@ -46,5 +46,174 @@ describe "app" do
last_response.status.should == 400
end
end
describe "GET /api/v1/users/:user_id/active_threads" do
before(:each) do
User.all.delete
Content.all.delete
@users = {}
@threads = {}
@comments = {}
10.times do |i|
author = create_test_user(i+100)
@users["u#{i+100}"] = author
thread = make_thread(author, "t#{i}", "xyz", "pdq")
@threads["t#{i}"] = thread
5.times do |j|
comment = make_comment(author, thread, "t#{i} c#{j}")
@comments["t#{i} c#{j}"] = comment
end
end
@natural_order = 10.times.map {|i| "t#{i}"}
end
def thread_result(user_id, params)
get "/api/v1/users/#{user_id}/active_threads", params
last_response.should be_ok
parse(last_response.body)["collection"]
end
it "requires that a course id be passed" do
get "/api/v1/users/100/active_threads"
# this is silly, but it is the legacy behavior
last_response.should be_ok
last_response.body.should == "{}"
end
it "only returns threads with activity from the specified user" do
rs = thread_result 100, course_id: "xyz"
rs.length.should == 1
check_thread_result(@users["u100"], @threads["t0"], rs.first, true)
rs.first["children"].length.should == 5
end
it "does not include anonymous threads" do
@comments["t0 c4"].anonymous = true
@comments["t0 c4"].save!
rs = thread_result 100, course_id: "xyz"
rs.length.should == 1
check_thread_result(@users["100"], @threads["t0"], rs.first, false)
rs.first["children"].length.should == 4
end
it "does not include anonymous-to-peers threads" do
@comments["t0 c3"].anonymous_to_peers = true
@comments["t0 c3"].save!
rs = thread_result 100, course_id: "xyz"
rs.length.should == 1
check_thread_result(@users["100"], @threads["t0"], rs.first, false)
rs.first["children"].length.should == 4
end
it "only returns threads from the specified course" do
@threads.each do |k, v|
v.author = @users["u100"]
v.save!
end
@threads["t9"].course_id = "zzz"
@threads["t9"].save!
rs = thread_result 100, course_id: "xyz"
rs.length.should == 9
end
it "correctly orders results by most recently updated" do
@threads.each do |k, v|
v.author = @users["u100"]
v.save!
end
@threads["t5"].updated_at = DateTime.now
@threads["t5"].save!
expected_order = @threads.keys.reverse.select{|k| k!="t5"}.insert(0, "t5")
rs = thread_result 100, course_id: "xyz"
actual_order = rs.map {|v| v["title"]}
actual_order.should == expected_order
end
it "only returns content authored by the specified user, and ancestors of that content" do
# by default, number of comments returned by u100 would be 5
@comments["t0 c2"].author = @users["u101"]
# now 4
make_comment(@users["u100"], @comments["t0 c2"], "should see me")
# now 5
make_comment(@users["u101"], @comments["t0 c2"], "should not see me")
make_comment(@users["u100"], @threads["t1"], "should see me")
# now 6
make_comment(@users["u101"], @threads["t1"], "should not see me")
rs = thread_result 100, course_id: "xyz"
rs.length.should == 2
# the leaf of every subtree in the rs must have author==u100
# and the comment count should match our expectation
expected_comment_count = 6
@actual_comment_count = 0
def check_leaf(result)
if !result["children"] or result["children"].length == 0
result["username"].should == "user100"
@actual_comment_count += 1
else
result["children"].each do |child|
check_leaf(child)
end
end
end
rs.each do |r|
check_leaf(r)
end
@actual_comment_count.should == expected_comment_count
end
# FIXME note the checks on result["num_pages"] are disabled.
# turns out there is a bug in GET "#{APIPREFIX}/users/:user_id/active_threads
# and this value is often wrong. however, since these tests are being
# created as a precursor to refactoring, the bug will not be fixed, and the
# checks will stay disabled.
context "pagination" do
def thread_result_page (page, per_page)
get "/api/v1/users/100/active_threads", course_id: "xyz", page: page, per_page: per_page
last_response.should be_ok
parse(last_response.body)
end
before(:each) do
@threads.each do |k, v|
@comments["#{k} c4"].author = @users["u100"]
@comments["#{k} c4"].save!
end
end
it "returns single page" do
result = thread_result_page(1, 20)
result["collection"].length.should == 10
#result["num_pages"].should == 1
result["page"].should == 1
end
it "returns multiple pages" do
result = thread_result_page(1, 5)
result["collection"].length.should == 5
#result["num_pages"].should == 2
result["page"].should == 1
result = thread_result_page(2, 5)
result["collection"].length.should == 5
#result["num_pages"].should == 2
result["page"].should == 2
end
it "orders correctly across pages" do
expected_order = @threads.keys.reverse
actual_order = []
pp = 3 # per page
4.times do |i|
n = i + 1
result = thread_result_page(n, pp)
result["collection"].length.should == [n * pp, 10].min - ((n - 1) * pp)
#result["num_pages"].should == 4
result["page"].should == n
actual_order += result["collection"].map {|v| v["title"]}
end
actual_order.should == expected_order
end
end
end
end
end
ENV["SINATRA_ENV"] = "test"
require 'simplecov'
SimpleCov.start
require File.join(File.dirname(__FILE__), '..', 'app')
......@@ -47,7 +49,8 @@ def init_without_subscriptions
commentable = Commentable.new("question_1")
user = create_test_user(1)
users = (1..10).map{|id| create_test_user(id)}
user = users.first
thread = CommentThread.new(title: "I can't solve this problem", body: "can anyone help me?", course_id: "1", commentable_id: commentable.id)
thread.author = user
......@@ -94,16 +97,23 @@ def init_without_subscriptions
comment1.comment_thread = thread
comment1.save!
users = (2..10).map{|id| create_test_user(id)}
thread = CommentThread.new(title: "I don't know what to say", body: "lol", course_id: "2", commentable_id: "something else")
thread.author = users[1]
thread.save!
comment = thread.comments.new(body: "i am objectionable", course_id: "2")
comment.author = users[2]
comment.abuse_flaggers = [users[3]._id]
comment.save!
Comment.all.each do |c|
user.vote(c, :up) # make the first user always vote up for convenience
users.each {|user| user.vote(c, [:up, :down].sample)}
users[2,9].each {|user| user.vote(c, [:up, :down].sample)}
end
CommentThread.all.each do |c|
user.vote(c, :up) # make the first user always vote up for convenience
users.each {|user| user.vote(c, [:up, :down].sample)}
users[2,9].each {|user| user.vote(c, [:up, :down].sample)}
end
Content.mongo_session[:blocked_hash].insert(hash: Digest::MD5.hexdigest("blocked post"))
......@@ -156,3 +166,88 @@ def init_with_subscriptions
thread.author = user1
thread.save!
end
# this method is used to test results produced using the helper function handle_threads_query
# which is used in multiple areas of the API
def check_thread_result(user, thread, json_response, check_comments=false)
json_response["title"].should == thread.title
json_response["body"].should == thread.body
json_response["course_id"].should == thread.course_id
json_response["anonymous"].should == thread.anonymous
json_response["anonymous_to_peers"].should == thread.anonymous_to_peers
json_response["commentable_id"].should == thread.commentable_id
json_response["created_at"].should == thread.created_at.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
json_response["updated_at"].should == thread.updated_at.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
json_response["at_position_list"].should == thread.at_position_list
json_response["closed"].should == thread.closed
json_response["id"].should == thread._id.to_s
json_response["user_id"].should == thread.author.id
json_response["username"].should == thread.author.username
json_response["votes"]["point"].should == thread.votes["point"]
json_response["votes"]["count"].should == thread.votes["count"]
json_response["votes"]["up_count"].should == thread.votes["up_count"]
json_response["votes"]["down_count"].should == thread.votes["down_count"]
json_response["abuse_flaggers"].should == thread.abuse_flaggers
json_response["tags"].should == thread.tags_array
json_response["type"].should == "thread"
json_response["group_id"].should == thread.group_id
json_response["pinned"].should == thread.pinned?
json_response["endorsed"].should == (thread.endorsed? or thread.comments.any? {|c| c.endorsed?})
if check_comments
# warning - this only checks top-level comments and may not handle all possible sorting scenarios
# it also does not check for author-only results (e.g. user active threads view)
root_comments = thread.root_comments.sort(_id:1).to_a
json_response["children"].should_not be_nil
json_response["children"].length.should == root_comments.length
json_response["children"].each_with_index { |v, i|
v["body"].should == root_comments[i].body
v["user_id"].should == root_comments[i].author_id
v["username"].should == root_comments[i].author_username
}
end
json_response["comments_count"].should == thread.comments.length
if user.nil?
json_response["unread_comments_count"].should == thread.comments.length
json_response["read"].should == false
else
expected_unread_cnt = thread.comments.length # initially assume nothing has been read
read_states = user.read_states.where(course_id: thread.course_id)
if read_states.length == 1
read_date = read_states.first.last_read_times[thread.id]
if read_date
thread.comments.each do |c|
if c.author != user and c.updated_at < read_date
expected_unread_cnt -= 1
end
end
end
end
json_response["unread_comments_count"].should == expected_unread_cnt
json_response["read"].should == (expected_unread_cnt == 0)
end
end
# general purpose factory helpers
def make_thread(author, text, course_id, commentable_id)
thread = CommentThread.new(title: text, body: text, course_id: course_id, commentable_id: commentable_id)
thread.author = author
thread.save!
thread
end
def make_comment(author, obj, text)
if obj.is_a?(CommentThread)
coll = obj.comments
thread = obj
else
coll = obj.children
thread = obj.comment_thread
end
comment = coll.new(body: text, course_id: obj.course_id)
comment.author = author
comment.comment_thread = thread
comment.save!
comment
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment