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,
......
......@@ -2,19 +2,254 @@ require 'spec_helper'
describe "app" do
describe "comment threads" do
before(:each) { init_without_subscriptions }
describe "GET /api/v1/threads" do
before(:each) do
User.all.delete
Content.all.delete
@threads = {}
@comments = {}
10.times do |i|
author = create_test_user(i+100)
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(params)
get "/api/v1/threads", params
last_response.should be_ok
parse(last_response.body)["collection"]
end
context "when filtering by course" do
it "returns only threads with matching course id" do
[@threads["t1"], @threads["t2"]].each do |t|
t.course_id = "abc"
t.save!
end
rs = thread_result course_id: "abc"
rs.length.should == 2
rs.each_with_index { |res, i|
check_thread_result(nil, @threads["t#{i+1}"], res)
res["course_id"].should == "abc"
}
end
it "returns only threads where course id and group id match" do
@threads["t1"].course_id = "omg"
@threads["t1"].group_id = 100
@threads["t1"].save!
@threads["t2"].course_id = "omg"
@threads["t2"].group_id = 101
@threads["t2"].save!
rs = thread_result course_id: "omg", group_id: 100
rs.length.should == 1
check_thread_result(nil, @threads["t1"], rs.first)
end
it "returns only threads where course id and group id match or group id is nil" do
@threads["t1"].course_id = "omg"
@threads["t1"].group_id = 100
@threads["t1"].save!
@threads["t2"].course_id = "omg"
@threads["t2"].group_id = nil
@threads["t2"].save!
@threads["t3"].group_id = 100
@threads["t3"].save!
rs = thread_result course_id: "omg", group_id: 100
rs.length.should == 2
rs.each_with_index { |res, i|
check_thread_result(nil, @threads["t#{i+1}"], res)
res["course_id"].should == "omg"
}
end
it "returns an empty result when no threads match course_id" do
rs = thread_result course_id: 99
rs.length.should == 0
end
context "when filtering flagged posts" do
it "returns threads that are flagged" do
@threads["t1"].abuse_flaggers = [1]
@threads["t1"].save!
rs = thread_result course_id: "xyz", flagged: true
rs.length.should == 1
check_thread_result(nil, @threads["t1"], rs.first)
end
it "returns threads that have flagged comments" do
@comments["t2 c3"].abuse_flaggers = [1]
@comments["t2 c3"].save!
rs = thread_result course_id: "xyz", flagged: true
rs.length.should == 1
check_thread_result(nil, @threads["t2"], rs.first)
end
it "returns an empty result when no posts were flagged" do
rs = thread_result course_id: "xyz", flagged: true
rs.length.should == 0
end
end
it "correctly considers read state" do
user = create_test_user(123)
[@threads["t1"], @threads["t2"]].each do |t|
t.course_id = "abc"
t.save!
end
rs = thread_result course_id: "abc", user_id: "123"
rs.length.should == 2
rs.each_with_index { |result, i|
check_thread_result(user, @threads["t#{i+1}"], result)
result["course_id"].should == "abc"
result["unread_comments_count"].should == 5
result["read"].should == false
}
user.mark_as_read(@threads["t1"])
rs = thread_result course_id: "abc", user_id: "123"
rs.length.should == 2
rs.each_with_index { |result, i|
check_thread_result(user, @threads["t#{i+1}"], result)
}
rs[0]["unread_comments_count"].should == 0
rs[0]["read"].should == true
rs[1]["unread_comments_count"].should == 5
rs[1]["read"].should == false
end
context "sorting" do
def thread_result_order (sort_key, sort_order)
get "/api/v1/threads", course_id: "xyz", sort_key: sort_key, sort_order: sort_order
last_response.should be_ok
results = parse(last_response.body)["collection"]
results.length.should == 10
results.map {|t| t["title"]}
end
def move_to_end(ary, val)
ary.select {|v| v!=val } << val
end
def move_to_front(ary, val)
ary.select {|v| v!=val }.insert(0, val)
end
it "sorts using create date / ascending" do
actual_order = thread_result_order("date", "asc")
expected_order = @natural_order
actual_order.should == expected_order
end
it "sorts using create date / descending" do
actual_order = thread_result_order("date", "desc")
expected_order = @natural_order.reverse
actual_order.should == expected_order
end
it "sorts using last activity / ascending" do
t5c = @threads["t5"].comments.first
t5c.update(body: "changed!")
t5c.save!
actual_order = thread_result_order("activity", "asc")
expected_order = move_to_end(@natural_order, "t5")
actual_order.should == expected_order
end
it "sorts using vote count" do
user = User.all.first
t5c = @threads["t5"].comments.first
user.vote(t5c, :up)
t5c.save!
actual_order = thread_result_order("activity", "asc")
expected_order = move_to_end(@natural_order, "t5")
actual_order.should == expected_order
end
it "sorts using comment count" do
make_comment(@threads["t5"].author, @threads["t5"], "extra comment")
actual_order = thread_result_order("comments", "asc")
expected_order = move_to_end(@natural_order, "t5")
actual_order.should == expected_order
end
it "sorts pinned items first" do
make_comment(@threads["t5"].author, @threads["t5"], "extra comment")
@threads["t7"].pinned = true
@threads["t7"].save!
actual_order = thread_result_order("comments", "asc")
expected_order = move_to_front(move_to_end(@natural_order, "t5"), "t7")
actual_order.should == expected_order
end
context "pagination" do
def thread_result_page (sort_key, sort_order, page, per_page)
get "/api/v1/threads", course_id: "xyz", sort_key: sort_key, sort_order: sort_order, page: page, per_page: per_page
last_response.should be_ok
parse(last_response.body)
end
it "returns single page" do
result = thread_result_page("date", "desc", 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("date", "desc", 1, 5)
result["collection"].length.should == 5
result["num_pages"].should == 2
result["page"].should == 1
result = thread_result_page("date", "desc", 2, 5)
result["collection"].length.should == 5
result["num_pages"].should == 2
result["page"].should == 2
end
it "orders correctly across pages" do
make_comment(@threads["t5"].author, @threads["t5"], "extra comment")
@threads["t7"].pinned = true
@threads["t7"].save!
expected_order = move_to_front(move_to_end(@natural_order, "t5"), "t7")
actual_order = []
pp = 3 # per page
4.times do |i|
n = i + 1
result = thread_result_page("comments", "asc", 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
describe "GET /api/v1/threads/:thread_id" do
before(:each) { init_without_subscriptions }
it "get information of a single comment thread" do
thread = CommentThread.first
get "/api/v1/threads/#{thread.id}"
last_response.should be_ok
response_thread = parse last_response.body
thread.title.should == response_thread["title"]
thread.body.should == response_thread["body"]
thread.course_id.should == response_thread["course_id"]
thread.votes_point.should == response_thread["votes"]["point"]
thread.commentable_id.should == response_thread["commentable_id"]
response_thread["children"].should be_nil
check_thread_result(nil, thread, response_thread)
end
it "computes endorsed? correctly" do
thread = CommentThread.first
comment = thread.root_comments[1]
comment.endorsed = true
comment.save!
get "/api/v1/threads/#{thread.id}"
last_response.should be_ok
response_thread = parse last_response.body
response_thread["endorsed"].should == true
check_thread_result(nil, thread, response_thread)
end
# This is a test to ensure that the username is included even if the
......@@ -42,14 +277,7 @@ describe "app" do
thread = CommentThread.first
get "/api/v1/threads/#{thread.id}", recursive: true
last_response.should be_ok
response_thread = parse last_response.body
thread.title.should == response_thread["title"]
thread.body.should == response_thread["body"]
thread.course_id.should == response_thread["course_id"]
thread.votes_point.should == response_thread["votes"]["point"]
response_thread["children"].should_not be_nil
response_thread["children"].length.should == thread.root_comments.length
response_thread["children"].index{|c| c["body"] == thread.root_comments.first.body}.should_not be_nil
check_thread_result(nil, thread, parse(last_response.body), true)
end
it "returns 400 when the thread does not exist" do
......@@ -71,6 +299,7 @@ describe "app" do
get "/api/v1/threads/#{thread.id}"
last_response.should be_ok
response_thread = parse last_response.body
check_thread_result(nil, thread, response_thread)
response_thread["tags"].length.should == 3
response_thread["tags"].should include "taga"
response_thread["tags"].should include "tagb"
......@@ -78,6 +307,9 @@ describe "app" do
end
end
describe "PUT /api/v1/threads/:thread_id" do
before(:each) { init_without_subscriptions }
it "update information of comment thread" do
thread = CommentThread.first
put "/api/v1/threads/#{thread.id}", body: "new body", title: "new title", commentable_id: "new_commentable_id"
......@@ -86,6 +318,7 @@ describe "app" do
changed_thread.body.should == "new body"
changed_thread.title.should == "new title"
changed_thread.commentable_id.should == "new_commentable_id"
check_thread_result(nil, changed_thread, parse(last_response.body))
end
it "returns 400 when the thread does not exist" do
put "/api/v1/threads/does_not_exist", body: "new body", title: "new title"
......@@ -116,6 +349,9 @@ describe "app" do
end
end
describe "POST /api/v1/threads/:thread_id/comments" do
before(:each) { init_without_subscriptions }
let :default_params do
{body: "new comment", course_id: "1", user_id: User.first.id}
end
......
......@@ -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