docbrown/spec/requests/comments_spec.rb
Daniel Uber 14caad86a7
handle deleted article's comments page with a 404 response (#15351)
* Add a failing test for comments index of deleted article

After https://github.com/forem/forem/pull/15052 removed the (also
broken) deleted commentable template and the redirect, requests for
comments from deleted articles raise no method errors (initially
trying to set the page title to "Comments for commentable.title" but
the assumption that @commentable is non-nil is present in several
other places.

This test currently fails (by design). Either we want to update the
controller so that comments from deleted articles are no longer
viewable (returning not found, as we did prior to
https://github.com/forem/forem/pull/5199 or making the view safe for
the case where @commentable is nil and no @root_comment is present.

These requests are happening quite frequently (@maestromac and I
suspect the sitemap may contain these pages and crawlers are visiting
the site from a published link), as evidenced by
https://app.honeybadger.io/projects/66984/faults/81994265

* Add request spec for deleted article scenario

Additionally, relabel the legacy spec (updated during #15052) to
clarify we do not render the deleted_commentable_comment view (this is
testing the comment.path, rather than the article.path/comments index

The scenario with comment.path sets `@root_comment` in the controller,
so does not trigger errors on the `@commentable` method calls.

* Update view

pass 1: get the errors to stop (need to check the rendered page is
also usable, the comment tree is not shown when commentable is nil and
this might be a huge usability/correctness issue).

* extract logic from comments index to methods and update tests

We no longer expect deleted article's comments index to render (should
return 404), only direct links to comments of deleted articles.

Only assign @article in the index for the view if it is in fact an
article (not a podcast episode, it's possible `@root_comment` was for
a podcast episode).

* remove unneeded (and soon to be incorrect spec)

No longer want or need to test for the case where root comment is nil
and so is commentable. This was an exploratory spec (scaffolding) and
can be removed.

* Undo nil safety changes to comments index

and fix typo in comments spec
2021-11-11 12:56:14 -06:00

391 lines
13 KiB
Ruby

require "rails_helper"
require "requests/shared_examples/comment_hide_or_unhide_request"
RSpec.describe "Comments", type: :request do
let(:user) { create(:user) }
let(:article) { create(:article, user: user) }
let(:podcast) { create(:podcast) }
let(:podcast_episode) { create(:podcast_episode, podcast_id: podcast.id) }
let!(:comment) { create(:comment, commentable: article, user: user) }
describe "GET comment index" do
it "returns 200" do
get comment.path
expect(response).to have_http_status(:ok)
end
it "displays a comment" do
get comment.path
expect(response.body).to include(comment.processed_html)
end
it "renders user payment pointer if set" do
article.user.update_column(:payment_pointer, "test-pointer-for-comments")
get "#{article.path}/comments"
expect(response.body).to include "author-payment-pointer"
expect(response.body).to include "test-pointer-for-comments"
end
it "does not render payment pointer if not set" do
get "#{article.path}/comments"
expect(response.body).not_to include "author-payment-pointer"
end
context "when the comment is a root" do
it "displays the comment hidden message if the comment is hidden" do
comment.update(hidden_by_commentable_user: true)
get comment.path
hidden_comment_message = "Comment hidden by post author - thread only visible in this permalink"
expect(response.body).to include(hidden_comment_message)
end
it "displays the comment anyway if it is hidden" do
comment.update(hidden_by_commentable_user: true)
get comment.path
expect(response.body).to include(comment.processed_html)
end
it "displays noindex if comment has score of less than 0" do
comment.update_column(:score, -5)
get comment.path
expect(response.body).to include('<meta name="googlebot" content="noindex">')
end
it "displays does not display noindex if comment has 0 or more score" do
get comment.path
expect(response.body).not_to include('<meta name="googlebot" content="noindex">')
end
it "displays noindex if commentable has score of less than 0" do
comment.commentable.update_column(:score, -5)
get comment.path
expect(response.body).to include('<meta name="googlebot" content="noindex">')
end
end
context "when the comment is a child comment" do
let(:child) { create(:comment, parent: comment, commentable: article, user: user) }
it "displays proper button and text for child comment" do
get child.path
expect(response.body).to include(CGI.escapeHTML(comment.title(150)))
expect(response.body).to include(child.processed_html)
end
end
context "when the comment is two levels nested and hidden" do # child of a child
let(:child) { create(:comment, parent: comment, commentable: article, user: user) }
let(:child_of_child) do
create(:comment, parent_id: child.id, commentable: article, user: user, hidden_by_commentable_user: true)
end
it "does not display the hidden comment in the child's permalink" do
get child.path
expect(response.body).not_to include(child_of_child.processed_html)
end
it "does not display the hidden comment in the article's comments section" do
get "#{article.path}/comments"
expect(response.body).not_to include(child_of_child.processed_html)
end
end
context "when the comment is a sibling of a child comment and is hidden" do
let(:child) { create(:comment, parent: comment, commentable: article, user: user) }
let(:sibling) do
create(:comment, parent: comment, commentable: article, user: user, hidden_by_commentable_user: true)
end
it "does not display the hidden comment in the article's comments section" do
get "#{article.path}/comments"
expect(response.body).not_to include(sibling.processed_html)
end
it "shows the hidden comments message in the comment's permalink" do
get sibling.path
hidden_comment_message = "Comment hidden by post author - thread only visible in this permalink"
expect(response.body).to include(hidden_comment_message)
end
it "does not show the sibling comment in the child's comment permalink" do
get child.path
expect(response.body).not_to include(sibling.processed_html)
end
it "shows the comment in the permalink" do
get sibling.path
expect(response.body).to include(sibling.processed_html)
end
end
context "when the comment is three levels nested and hidden" do # child of a child of a child
let(:child) { create(:comment, parent: comment, commentable: article, user: user) }
let(:second_level_child) { create(:comment, parent: child, commentable: article, user: user) }
let(:third_level_child) do
create(:comment, parent: second_level_child, commentable: article, user: user, hidden_by_commentable_user: true)
end
let(:fourth_level_child) do
create(:comment, parent_id: third_level_child.id, commentable: article, user: user)
end
it "does not show the hidden comment in the article's comments section" do
get "#{article.path}/comments"
expect(response.body).not_to include(third_level_child.processed_html)
end
it "does not show the hidden comment in its parent's permalink" do
get second_level_child.path
expect(response.body).not_to include(third_level_child.processed_html)
end
it "shows the comment in the permalink" do
get third_level_child.path
expect(response.body).to include(third_level_child.processed_html)
end
it "shows the fourth level child in the hidden comment's permalink" do
fourth_level_child
get third_level_child.path
expect(response.body).to include(fourth_level_child.processed_html)
end
end
context "when the comment is for a podcast's episode" do
it "works" do
podcast_comment = create(:comment, commentable: podcast_episode, user: user)
get podcast_comment.path
expect(response).to have_http_status(:ok)
end
end
context "when the article is unpublished" do
before do
new_markdown = article.body_markdown.gsub("published: true", "published: false")
comment
article.update(body_markdown: new_markdown)
end
it "raises a Not Found error" do
expect { get comment.path }.to raise_error("Not Found")
end
end
context "when the article is deleted" do
it "raises not found when listing article comments" do
path = "#{article.path}/comments"
article.destroy
expect { get path }.to raise_error(ActiveRecord::RecordNotFound)
end
it "shows comment from a deleted post" do
article.destroy
get comment.path
expect(response.body).to include("Comment from a deleted post")
end
end
context "when the podcast episode is deleted" do
it "renders deleted_commentable_comment view" do
podcast_comment = create(:comment, commentable: podcast_episode)
podcast_episode.destroy
get podcast_comment.path
expect(response.body).to include("Comment from a deleted post")
end
end
end
describe "GET /:username/:slug/comments/:id_code/edit" do
context "when not logged-in" do
it "returns unauthorized error" do
expect do
get "/#{user.username}/#{article.slug}/comments/#{comment.id_code_generated}/edit"
end.to raise_error(Pundit::NotAuthorizedError)
end
end
context "when logged-in" do
before do
sign_in user
end
it "returns 200" do
get "/#{user.username}/#{article.slug}/comments/#{comment.id_code_generated}/edit"
expect(response).to have_http_status(:ok)
end
it "returns the comment" do
get "/#{user.username}/#{article.slug}/comments/#{comment.id_code_generated}/edit"
expect(response.body).to include CGI.escapeHTML(comment.body_markdown)
end
end
context "when the article is deleted" do
before do
sign_in user
end
it "edit action returns 200" do
article = create(:article, user: user)
comment = create(:comment, commentable: article, user: user)
article.destroy
get "/#{user.username}/#{article.slug}/comments/#{comment.id_code_generated}/edit"
expect(response).to have_http_status(:ok)
end
end
end
describe "PUT /comments/:id" do
before do
sign_in user
end
it "does not raise a StandardError for invalid liquid tags" do
put "/comments/#{comment.id}",
params: { comment: { body_markdown: "{% gist flsnjfklsd %}" } }
expect(response).to have_http_status(:ok)
expect(flash[:error]).not_to be_nil
end
context "when the article is deleted" do
it "updates body markdown" do
article = create(:article, user: user)
comment = create(:comment, commentable: article, user: user)
article.destroy
params = { comment: { body_markdown: "{edited comment}" } }
put "/comments/#{comment.id}", params: params
comment.reload
expect(comment.processed_html).to include("edited comment")
end
end
end
describe "POST /comments/preview" do
it "returns 401 if user is not logged in" do
post "/comments/preview",
params: { comment: { body_markdown: "hi" } },
headers: { HTTP_ACCEPT: "application/json" }
expect(response).to have_http_status(:unauthorized)
end
context "when logged-in" do
before do
sign_in user
post "/comments/preview",
params: { comment: { body_markdown: "hi" } },
headers: { HTTP_ACCEPT: "application/json" }
end
it "returns 200 on good request" do
expect(response).to have_http_status(:ok)
end
it "returns json" do
expect(response.media_type).to eq("application/json")
end
end
end
describe "POST /comments" do
let(:base_comment_params) do
{
comment: {
commentable_id: article.id,
commentable_type: "Article",
user: user,
body_markdown: "New comment #{rand(10)}"
}
}
end
context "when part of field test" do
before do
sign_in user
allow(Users::RecordFieldTestEventWorker).to receive(:perform_async)
end
it "converts field test" do
post "/comments", params: base_comment_params
expected_args = [user.id, "user_creates_comment"]
expect(Users::RecordFieldTestEventWorker).to have_received(:perform_async).with(*expected_args)
end
end
context "when not part of field test" do
before do
sign_in user
allow(FieldTest).to receive(:config).and_return({ "experiments" => nil })
allow(Users::RecordFieldTestEventWorker).to receive(:perform_async)
end
it "converts field test" do
post "/comments", params: base_comment_params
expect(Users::RecordFieldTestEventWorker).not_to have_received(:perform_async)
end
end
end
describe "PATCH /comments/:comment_id/hide" do
include_examples "PATCH /comments/:comment_id/hide or unhide", path: "hide", hidden: "true"
context "with notifications" do
let(:user2) { create(:user) }
let(:article) { create(:article, :with_notification_subscription, user: user) }
let(:comment) { create(:comment, commentable: article, user: user2) }
before do
sign_in user
Notification.send_new_comment_notifications_without_delay(comment)
end
it "Delete notification when comment is hidden" do
notification = user.notifications.last
patch "/comments/#{comment.id}/hide", headers: { HTTP_ACCEPT: "application/json" }
expect(Notification.exists?(id: notification.id)).to eq(false)
end
end
end
describe "PATCH /comments/:comment_id/unhide" do
include_examples "PATCH /comments/:comment_id/hide or unhide", path: "unhide", hidden: "false"
end
describe "DELETE /comments/:comment_id" do
# we're using local article and comments, to avoid removing data used by other tests,
# which will incur in ordering issues
let!(:article) { create(:article, user: user) }
let!(:comment) { create(:comment, commentable: article, user: user) }
before { sign_in user }
it "deletes a comment if the article is still present" do
delete "/comments/#{comment.id}"
expect(Comment.find_by(id: comment.id)).to be_nil
expect(response).to redirect_to(comment.commentable.path)
expect(flash[:notice]).to eq("Comment was successfully deleted.")
end
it "deletes a comment if the article has been deleted" do
article.destroy!
delete "/comments/#{comment.id}"
expect(Comment.find_by(id: comment.id)).to be_nil
expect(response).to redirect_to(user_path(user))
expect(flash[:notice]).to eq("Comment was successfully deleted.")
end
end
end