-
-```
-
-On element declarations whose attributes span over more than one line, align subsequent lines with the first
-attribute on the first line.
-
-```html
-
- ...
-
-```
-
-#### Semantic elements
-Prefer semantic elements where possible: use `` over `
diff --git a/app/views/application/dashboard.html.erb b/app/views/application/dashboard.html.erb
index 397e020fc..8fec9b4cf 100644
--- a/app/views/application/dashboard.html.erb
+++ b/app/views/application/dashboard.html.erb
@@ -30,7 +30,7 @@
<% end %>
<% end %>
- <% if current_user&.has_ability_on(c.id, 'edit_posts') %>
+ <% if current_user&.ability_on?(c.id, 'edit_posts') %>
<% sug_edits = @edits[cat.id] || 0 %>
<% if sug_edits > 0 %>
<%= link_to suggested_edits_queue_url(cat, host: c.host), class: 'widget--body-extra' do %>
diff --git a/app/views/categories/category_post_types.html.erb b/app/views/categories/category_post_types.html.erb
index 808630027..c9d53c8d9 100644
--- a/app/views/categories/category_post_types.html.erb
+++ b/app/views/categories/category_post_types.html.erb
@@ -1,8 +1,17 @@
-
- <%= link_to edit_category_path(@category) do %>
- « Back to category edit
- <% end %>
-
+<%#
+ View for managing allowed category post types
+
+ Parameters:
+ params[:no_return] : whether to suppress the return link
+%>
+
+<% unless params[:no_return] == '1' %>
+
+ <%= link_to edit_category_path(@category) do %>
+ « Back to category edit
+ <% end %>
+
+<% end %>
Allowed post types for <%= @category.name %>
Only post types listed here are allowed to be posted in this category. Not all will be displayed as available options
diff --git a/app/views/categories/index.html.erb b/app/views/categories/index.html.erb
index 2c507c1fe..6999cd10b 100644
--- a/app/views/categories/index.html.erb
+++ b/app/views/categories/index.html.erb
@@ -18,15 +18,15 @@
<%= link_to category_path(cat), class: 'button is-filled' do %>
See posts »
<% end %>
- <% if current_user&.is_admin %>
+ <% if current_user&.admin? %>
<%= link_to 'Edit', edit_category_path(cat), class: 'button is-outlined' %>
<% end %>
<% end %>
-<% if current_user&.is_admin %>
+<% if current_user&.admin? %>
<%= link_to new_category_path, class: 'button is-outlined' do %>
Add new category »
<% end %>
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/views/categories/post_types.html.erb b/app/views/categories/post_types.html.erb
index 3b594b327..3ab6bd43f 100644
--- a/app/views/categories/post_types.html.erb
+++ b/app/views/categories/post_types.html.erb
@@ -1,6 +1,12 @@
-
What kind of post?
+<% header_title = @post_types.any? ? 'What kind of post?' : 'No allowed post types'
+ header_subtitle = @post_types.any? \
+ ? 'This category has more than one type of post available. Pick a post type to get started.'
+ : 'This category does not have any post types available.'
+%>
+
+
<%= header_title %>
- This category has more than one type of post available. Pick a post type to get started.
+ <%= header_subtitle %>
<% @post_types.each do |pt| %>
diff --git a/app/views/categories/show.html.erb b/app/views/categories/show.html.erb
index 62a18a316..96e10ea2f 100644
--- a/app/views/categories/show.html.erb
+++ b/app/views/categories/show.html.erb
@@ -11,7 +11,7 @@
<% end %>
<% post_count = @posts.count %>
-
+
+ <% if inline && shown_comments_count < thread.reply_count %>
+
+ <% end %>
+
+ <% unless current_user.nil? %>
+
+ <% end %>
+ <% end %>
+<% end %>
+
+<% if current_user&.privilege?('flag_curate') %>
+ <%= render 'comments/thread_actions_modal', thread: thread, user: current_user %>
+<% end %>
diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb
index 4d8e4f2c2..41cabae0d 100644
--- a/app/views/comments/_comment.html.erb
+++ b/app/views/comments/_comment.html.erb
@@ -1,8 +1,10 @@
<%#
Single comment view.
+
Variables:
+ comment : Comment to display
? with_post_link : includes share labelled post
- ? pingable : ???
+ ? pingable : an Array of user IDs to ping
%>
<% with_post_link ||= false %>
@@ -38,29 +40,33 @@
<% end %>
- <%= user_link comment.user %> wrote
+ <%= comment_user_link(comment) %> wrote
<%= time_ago_in_words(comment.created_at) %> ago
<% if comment.updated_at > comment.created_at %>
· edited <%= time_ago_in_words(comment.updated_at) %> ago
<% end %>
- <%= link_to comment_link(comment), class: 'js-comment-permalink', role: 'button' do %>
+ <%= link_to comment_link(comment), class: 'js-comment-permalink nowrap', role: 'button' do %>
copy link
<% end %>
<% if with_post_link %>
<%= link_to 'post', generic_share_link(comment.post) %>
<% end %>
- <% if user_signed_in? && (comment.user == current_user || current_user.is_moderator) && params[:inline] != 'true' %>
- edit
+ <% if user_signed_in? && (comment.user == current_user || current_user.at_least_moderator?) %>
+ edit
<% if comment.deleted %>
- undelete
+ <%= form_tag undelete_comment_path(comment), method: :patch do %>
+ <%= submit_tag 'undelete', class: 'link is-red js-comment-undelete nowrap', aria: { label: 'Undelete comment' } %>
+ <% end %>
<% else %>
- delete
+ <%= form_tag delete_comment_path(comment), method: :delete do %>
+ <%= submit_tag 'delete', class: 'link is-red js-comment-delete nowrap', aria: { label: 'Delete comment' } %>
+ <% end %>
<% end %>
<% end %>
- <% if user_signed_in? && params[:inline] != 'true' %>
- flag
+ <% if user_signed_in? %>
+ flag
diff --git a/app/views/comments/_lock_thread_modal.html.erb b/app/views/comments/_lock_thread_modal.html.erb
new file mode 100644
index 000000000..ef756bf6a
--- /dev/null
+++ b/app/views/comments/_lock_thread_modal.html.erb
@@ -0,0 +1,31 @@
+<%#
+ Helper for rendering comment thread lock action modal
+
+ Variables:
+ thread : Comment thread to create the modal for
+%>
+
+
diff --git a/app/views/comments/_new_thread.html.erb b/app/views/comments/_new_thread.html.erb
new file mode 100644
index 000000000..94f0c59ed
--- /dev/null
+++ b/app/views/comments/_new_thread.html.erb
@@ -0,0 +1,40 @@
+<%#
+ Renders new comment thread button & modal
+
+ Variables:
+ post : post to create a new thread for
+ text : text to display on the button
+ user : user that will create a new thread
+%>
+
+<%
+ can_comment = user.can_comment_on?(post)
+ is_rate_limited, rate_limit_message = comment_rate_limited?(user, post, create_audit_log: false)
+ text ||= defined?(text) && text.present? ? text : t('comments.labels.create_new_thread')
+ is_exempt = !post.comments_allowed? && at_least_moderator?
+ exempt_text = is_exempt ? t('comments.errors.exempt_from_disabled') : nil
+ title = if !post.comments_allowed?
+ [comments_post_error_msg(post), exempt_text].compact.join(' ')
+ elsif is_rate_limited
+ rate_limit_message
+ else
+ ''
+ end
+%>
+
+
+ <%= link_to 'javascript:void(0)', class: 'button is-outlined is-small js-new-thread-link',
+ data: { post: post.id },
+ disabled: !can_comment,
+ role: 'button',
+ title: is_exempt ? '' : title do %>
+ <%= text %>
+ <% end %>
+ <% if is_exempt %>
+
+ <% end %>
+
+
+<% if can_comment %>
+ <%= render 'comments/new_thread_modal', post: post %>
+<% end %>
diff --git a/app/views/comments/_new_thread_modal.html.erb b/app/views/comments/_new_thread_modal.html.erb
index 8ea24575f..360a81148 100644
--- a/app/views/comments/_new_thread_modal.html.erb
+++ b/app/views/comments/_new_thread_modal.html.erb
@@ -2,6 +2,12 @@
Start new comment thread dialog.
%>
+<%
+ # TODO: make configurable
+ min_chars = 15
+ max_chars = 1000
+%>
+
Start a new comment thread
@@ -18,9 +24,15 @@
<%= label_tag :body, 'Your comment', class: 'form-element' %>
-<% end %>
\ No newline at end of file
diff --git a/app/views/comments/_rename_thread_modal.html.erb b/app/views/comments/_rename_thread_modal.html.erb
new file mode 100644
index 000000000..09417dcda
--- /dev/null
+++ b/app/views/comments/_rename_thread_modal.html.erb
@@ -0,0 +1,26 @@
+<%#
+ Helper for rendering comment thread rename action modal
+
+ Variables:
+ thread : Comment thread to create the modal for
+%>
+
+
+ <%= form_tag rename_comment_thread_path(thread.id), class: 'modal--container' do %>
+
diff --git a/app/views/comments/_reply_to_thread.html.erb b/app/views/comments/_reply_to_thread.html.erb
new file mode 100644
index 000000000..db94fd5ca
--- /dev/null
+++ b/app/views/comments/_reply_to_thread.html.erb
@@ -0,0 +1,55 @@
+<%#
+ Renders reply to comment thread button & input
+
+ Variables:
+ inline : whether the reply is done via inline thread view
+ post : post the thread is for
+ text : text to display on the button
+ thread : thread to create a reply for
+ user : user that will create a reply to the thread
+%>
+
+<%
+ can_comment = user.can_reply_to?(thread)
+ is_rate_limited, rate_limit_message = comment_rate_limited?(user, post, create_audit_log: false)
+ text ||= defined?(text) && text.present? ? text : t('comments.labels.reply_to_thread')
+ is_exempt = (!post.comments_allowed? || thread.read_only?) && at_least_moderator?
+ exempt_text = is_exempt ? t('comments.errors.exempt_from_disabled') : nil
+ title = if !post.comments_allowed?
+ [comments_post_error_msg(post), exempt_text].compact.join(' ')
+ elsif thread.read_only?
+ [comments_thread_error_msg(thread), exempt_text].compact.join(' ')
+ elsif is_rate_limited
+ rate_limit_message
+ else
+ ''
+ end
+%>
+
+
+ <%= link_to 'javascript:void(0)', class: 'button is-outlined is-small js-reply-to-thread-link',
+ data: { post: post.id },
+ disabled: !can_comment,
+ role: 'button',
+ title: is_exempt ? '' : title do %>
+ <%= text %>
+ <% end %>
+ <% if is_exempt %>
+
+ <% end %>
+
+<%#
+ Renders widgets indicating that deleted comments are not shown
+
+ Variables:
+ num_skipped : number of skipped deleted comments
+ inline : whether the thread is shown inline or not
+ thread : CommentThread to display the widget for
+%>
+
+
diff --git a/app/views/comments/_skip_more.html.erb b/app/views/comments/_skip_more.html.erb
new file mode 100644
index 000000000..7ad84fd6e
--- /dev/null
+++ b/app/views/comments/_skip_more.html.erb
@@ -0,0 +1,12 @@
+<%#
+ Renders widgets indicating that there more comments are not shown
+
+ Variables:
+ shown_count : number of comments shown on the thread
+ thread : CommentThread to display the widget for
+%>
+
+
diff --git a/app/views/comments/_thread_actions_modal.html.erb b/app/views/comments/_thread_actions_modal.html.erb
new file mode 100644
index 000000000..c4bbb988f
--- /dev/null
+++ b/app/views/comments/_thread_actions_modal.html.erb
@@ -0,0 +1,96 @@
+<%#
+ Helper for rendering comment thread actions modal
+
+ Variables:
+ thread : Comment thread to create the modal for
+ user : user to show the modal for
+%>
+
+
+
thread options
+
+ <% if user.at_least_moderator? || !thread.read_only? %>
+
+ rename
+
+ <% end %>
+
+ <% unless thread.archived || thread.deleted %>
+ <% if thread.locked? %>
+
+ unlock
+
+ <% else %>
+
+ lock
+
+ <% end %>
+ <% end %>
+
+ <% unless thread.locked? || thread.deleted %>
+ <% if thread.archived %>
+
+ restore
+
+ <% else %>
+
+ archive
+
+ <% end %>
+ <% end %>
+
+ <% unless thread.locked? || thread.archived %>
+ <% if thread.deleted %>
+
+ undelete
+
+ <% else %>
+
+ delete
+
+ <% end %>
+ <% end %>
+
+ <% if user.at_least_moderator? %>
+
+ followers
+
+ <% end %>
+
+
+
+
+<%= render 'comments/rename_thread_modal', thread: thread %>
+<%= render 'comments/lock_thread_modal', thread: thread %>
+
+<% if current_user&.at_least_moderator? %>
+ <%= render 'comments/thread_followers_modal', thread: thread %>
+<% end %>
diff --git a/app/views/comments/_thread_follow_link.html.erb b/app/views/comments/_thread_follow_link.html.erb
new file mode 100644
index 000000000..0cc8978d6
--- /dev/null
+++ b/app/views/comments/_thread_follow_link.html.erb
@@ -0,0 +1,29 @@
+<%#
+ Helper for rendering comment thread follow/unfollow link buttons
+
+ Variables:
+ thread : Comment thread to create the link for
+ user : user to show the link for
+%>
+
+<% if thread.followed_by?(user) %>
+
+ unfollow
+
+<% else %>
+
+ follow
+
+<% end %>
diff --git a/app/views/comments/_thread_followers_modal.html.erb b/app/views/comments/_thread_followers_modal.html.erb
new file mode 100644
index 000000000..dbd474b77
--- /dev/null
+++ b/app/views/comments/_thread_followers_modal.html.erb
@@ -0,0 +1,21 @@
+<%#
+ Helper for rendering comment thread followers action modal
+
+ Variables:
+ thread : Comment thread to create the modal for
+%>
+
+
+
+
+
+ Thread Followers
+
+
+ Loading...
+
+
+
diff --git a/app/views/comments/_thread_tools_link.html.erb b/app/views/comments/_thread_tools_link.html.erb
new file mode 100644
index 000000000..e8230babf
--- /dev/null
+++ b/app/views/comments/_thread_tools_link.html.erb
@@ -0,0 +1,14 @@
+<%#
+ Helper for rendering comment thread tools link buttons
+
+ Variables:
+ thread : Comment thread to create the link for
+%>
+
+
+ tools
+
diff --git a/app/views/comments/_threads_list.html.erb b/app/views/comments/_threads_list.html.erb
new file mode 100644
index 000000000..f1935d302
--- /dev/null
+++ b/app/views/comments/_threads_list.html.erb
@@ -0,0 +1,36 @@
+<%#
+ List of comment threads below a post.
+
+ Variables:
+ comment_threads : an Array of CommentThread instances to list
+ ? comment_id : Comment to show even if it would be hidden otherwise
+ ? thread_id : CommentThread ID to render expanded view for
+ ? show_deleted : whether to display deleted comments in the expanded view
+%>
+
+<%
+ comment_id ||= defined?(comment_id) ? comment_id : nil
+ thread_id ||= defined?(thread_id) ? thread_id : nil
+ show_deleted ||= defined?(show_deleted) ? show_deleted : false
+%>
+
+<% comment_threads.each do |thread| %>
+ <%
+ state_class = [
+ thread.deleted ? 'is-deleted' : nil,
+ thread.archived ? 'is-archived' : nil,
+ thread.locked? ? 'is-locked' : nil
+ ].compact.join(' ')
+ %>
+
+
Welcome to Codidact, <%= @resource.username %>! We're glad you're here.
+
Welcome to Codidact, <%= @resource.username %>! We're glad you're here. (If this wasn't you, please ignore this email.)
-
Please confirm your registration by clicking below (or copy and paste the URL into your browser).
+
Please confirm your registration by clicking below (or copy and paste the URL into your browser). You cannot sign in to your account until you do this.
<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token, host: RequestContext.community.host),
diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb
index 644611675..87f6b933c 100644
--- a/app/views/devise/registrations/edit.html.erb
+++ b/app/views/devise/registrations/edit.html.erb
@@ -58,3 +58,23 @@
<%= f.submit "Update", class: 'button is-filled is-very-large', disabled: sso %>
<% end %>
+
+<% if current_user.at_least_moderator? %>
+ <%= link_to 'javascript:void(0)', class: 'button is-outlined is-danger', disabled: true do %>
+ Delete my account »
+ <% end %>
+
+ Moderators and admins cannot be self-deleted. Contact support if you wish to delete your account.
+
+<% elsif current_user.enabled_2fa %>
+ <%= link_to 'javascript:void(8)', class: 'button is-outlined is-danger', disabled: true do %>
+ Delete my account »
+ <% end %>
+
+ Your account uses two-factor authentication (2FA). In order to delete your account, you must first disable 2FA.
+
- Attention: The reply to the flag will be shown to the flagger, not the mod escalating this flag. Do not share sensitive, mod-only information.
+ Attention: The reply to the flag will be shown to the flagger, not the mod escalating this
+ flag. Do not share sensitive, mod-only information.
<% end %>
<% if controls %>
diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb
index 9f608f5f8..d3c1fb20c 100644
--- a/app/views/layouts/_sidebar.html.erb
+++ b/app/views/layouts/_sidebar.html.erb
@@ -14,38 +14,81 @@
<% unless @community.is_fake %>
- <% if Rails.env.development? || @hot_questions.to_a.size > 0 || @pinned_links.to_a.size > 0 %>
-
- <% if Rails.env.development? || @pinned_links.to_a.size > 0 %>
-
+ <% end %>
<% end %>
<% end %>
@@ -71,12 +114,16 @@
<% end %>
<% end %>
- <% if moderator? || admin? %>
+ <% if can_see_deleted_posts? || at_least_moderator? %>
-
Moderator Tools
+
Tools
- <% if moderator? %>
+ <% if can_see_deleted_posts? && !at_least_moderator? %>
+
+ <% end %>
+ <%# this calls into application_helper, not the user model! %>
+ <% if at_least_moderator? %>
<%= link_to 'Moderator Tools', moderator_path %>
<% end %>
<% if admin? %>
diff --git a/app/views/moderator/index.html.erb b/app/views/moderator/index.html.erb
index e07bfc300..4924aed6f 100644
--- a/app/views/moderator/index.html.erb
+++ b/app/views/moderator/index.html.erb
@@ -1,6 +1,6 @@
<% content_for :title, "Moderator Dashboard" %>
-<% if current_user.is_admin %>
+<% if current_user.admin? %>
<%= link_to admin_path, class: "has-font-size-small" do %>
Switch to Admin Tools
<% end %>
@@ -97,4 +97,4 @@
<% chat = SiteSetting['ChatLink'] %>
<% if chat.present? %>
As a moderator, you should join our <%= link_to 'community chat server', chat %>. Ping a Codidact team member there and you'll receive access to a special moderator-only lounge, where you can discuss moderation questions with your fellow moderators.
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/views/moderator/recent_comments.html.erb b/app/views/moderator/recent_comments.html.erb
index 9901ee5bb..c9923ee3d 100644
--- a/app/views/moderator/recent_comments.html.erb
+++ b/app/views/moderator/recent_comments.html.erb
@@ -9,8 +9,16 @@
communities; consider using activity logs on user profiles instead.
+<%
+ thread_pingables = @comments.map(&:comment_thread).to_set.to_h do |thread|
+ [thread, thread.pingable]
+ end
+%>
+
<% @comments.each do |comment| %>
- <%= render 'comments/comment', comment: comment, with_post_link: true %>
+ <%= render 'comments/comment', comment: comment,
+ pingable: thread_pingables[comment.comment_thread],
+ with_post_link: true %>
<% end %>
diff --git a/app/views/moderator/recently_deleted_posts.html.erb b/app/views/moderator/recently_deleted_posts.html.erb
index 1eb328d2f..b6366d0f5 100644
--- a/app/views/moderator/recently_deleted_posts.html.erb
+++ b/app/views/moderator/recently_deleted_posts.html.erb
@@ -1,9 +1,17 @@
<% content_for :title, "Recently Deleted Posts" %>
-<%= link_to moderator_path, class: 'has-font-size-small' do %>
- « Return to moderator tools
+
+<% if current_user.at_least_moderator? %>
+ <%= link_to moderator_path, class: 'has-font-size-small' do %>
+ « Return to moderator tools
+ <% end %>
<% end %>
Because you have the Curate ability, you can see deleted posts regardless of who deleted them. If you feel a post has been deleted in error, you can restore it. If a post was deleted by its author and you decide to restore it, consider leaving a comment explaining why (to reduce confusion).
+<% end %>
+
<% @posts.each do |post| %>
<%= render 'posts/type_agnostic', post: post %>
@@ -11,3 +19,4 @@
<%= will_paginate @posts, renderer: BootstrapPagination::Rails %>
+
diff --git a/app/views/post_history/post.html.erb b/app/views/post_history/post.html.erb
index f5e9df6ab..3444c67c9 100644
--- a/app/views/post_history/post.html.erb
+++ b/app/views/post_history/post.html.erb
@@ -15,7 +15,7 @@
by
- <% if deleted_user?(event.user) && !moderator? %>
+ <% if deleted_user?(event.user) && !at_least_moderator? %>
(deleted user)
<% else %>
@@ -45,7 +45,7 @@
you performed the redaction,
<% elsif current_user == @post.user %>
you are the post author,
- <% elsif current_user&.is_admin %>
+ <% elsif current_user&.admin? %>
you are an administrator,
<% end %>
but you should not share this revision with others.
diff --git a/app/views/posts/_edit_link.html.erb b/app/views/posts/_edit_link.html.erb
new file mode 100644
index 000000000..a21a13917
--- /dev/null
+++ b/app/views/posts/_edit_link.html.erb
@@ -0,0 +1,11 @@
+<%#
+ Helper for rendering post edit link buttons
+
+ Variables:
+ post : Post to create the link for
+%>
+
+<%= link_to edit_post_path(post), class: 'tools--item', 'aria-label': 'Edit this post' do %>
+
+ Edit
+<% end %>
diff --git a/app/views/posts/_expanded.html.erb b/app/views/posts/_expanded.html.erb
index 5f28e9d23..feb21a457 100644
--- a/app/views/posts/_expanded.html.erb
+++ b/app/views/posts/_expanded.html.erb
@@ -1,8 +1,18 @@
<%#
- Full post view, containing all details and interactions.
- Variables:
- post : the Post instance to display
- float_notice : whether to display the float notice
+ Full post view, containing all details and interactions.
+
+ Instance variables:
+ ? @category : Category the post belongs to, if any
+ ? @post_type : PostType of the post
+
+ Parameters:
+ ? params[:comment_id] : Comment ID to show even if it would be hidden otherwise
+ ? params[:thread_id] : CommentThread ID to render expanded view for
+ ? params[:sort] : sorting type for the post's children
+
+ Variables:
+ post : the Post instance to display
+ ? float_notice : whether to display the float notice
%>
<% float_notice ||= false %>
@@ -14,7 +24,7 @@
You are accessing this answer with a direct link, so it's being shown above all other answers regardless of its <%= sort_type %>.
- You can <%= link_to 'return to the normal view', generic_share_link(post.parent), class: 'is-teal' %>.
+ You can <%= link_to 'return to the normal view', generic_share_link(post.parent, sort: params[:sort]), class: 'is-teal' %>.
<% end %>
@@ -157,7 +167,7 @@
<% end %>
<% if post_type.is_public_editable && post.pending_suggested_edit? %>
- <% if check_your_post_privilege(post, 'edit_posts') %>
+ <% if current_user&.can_update?(post, post_type) %>
@@ -240,27 +250,18 @@
History
<% end %>
- <% if (post_type.is_public_editable && !post.locked?) || current_user&.is_moderator || post.user == current_user %>
- <% if check_your_post_privilege(post, 'edit_posts') %>
+ <% if (post_type.is_public_editable && !post.locked?) || current_user&.at_least_moderator? || post.user == current_user %>
+ <% if current_user&.can_update?(post, post_type) %>
<% if post.pending_suggested_edit? %>
- <%= link_to suggested_edit_url(post.pending_suggested_edit.id), class: 'tools--item is-danger is-filled' do %>
-
- Review suggested edit
- <% end %>
+ <%= render 'posts/review_edit_link', post: post %>
<% else %>
- <%= link_to edit_post_path(post), class: 'tools--item', 'aria-label': 'Edit this post' do %>
-
- Edit
- <% end %>
+ <%= render 'posts/edit_link', post: post %>
<% end %>
- <% elsif !current_user.nil? %>
+ <% elsif current_user.present? %>
<% if post.pending_suggested_edit? %>
suggested edit pending...
- <% elsif post_type.is_freely_editable %>
- <%= link_to edit_post_path(post), class: 'tools--item', 'aria-label': 'Edit this post' do %>
-
- Edit
- <% end %>
+ <% elsif post_type.is_freely_editable %>
+ <%= render 'posts/edit_link', post: post %>
<% else %>
<%= link_to edit_post_path(post), class: 'tools--item', 'aria-label': 'Suggest edit to this post' do %>
@@ -314,13 +315,13 @@
Tools
- <% flags_count = if current_user&.is_moderator
+ <% flags_count = if current_user&.at_least_moderator?
post.flags
else
post.flags.not_confidential
end.where(handled_by_id: nil).count %>
- <% own_flags_count = if current_user&.is_moderator
+ <% own_flags_count = if current_user&.at_least_moderator?
0
else
post.flags.not_confidential.where(user: current_user, handled_by_id: nil).count
@@ -339,7 +340,7 @@
Why does this post require attention from curators or moderators?
- <% if current_user&.has_active_flags?(post) %>
+ <% if current_user&.active_flags_on?(post) %>
You already have active flags on this post:
@@ -475,8 +476,8 @@
Flags on this post
<% post.flags.where(handled_by_id: nil).each do |flag| %>
- <% next if !current_user.is_moderator && (flag.post_flag_type.nil? || flag.post_flag_type.confidential) %>
- <% next if !current_user.is_moderator && (current_user.id == flag.user.id) %>
+ <% next if !current_user.at_least_moderator? && (flag.post_flag_type.nil? || flag.post_flag_type.confidential) %>
+ <% next if !current_user.at_least_moderator? && (current_user.id == flag.user.id) %>
@@ -501,7 +502,7 @@
<% sorted_answers = post.children.sort_by { |answer| answer.score }.reverse! %>
<% sorted_answers.each do |answer| %>
- <% next if answer.deleted? && !moderator? %>
+ <% next if answer.deleted? && !at_least_moderator? %>
<%= render 'posts/toc_entry', answer: answer %>
<% end %>
- <% unless post.locked? && !moderator? %>
+ <% unless post.locked? && !at_least_moderator? %>
<% if post.answer? %>
Convert to comment
@@ -48,11 +48,11 @@
Lock post
- Locking a post disallows edits, comments and votes. Use post closure to disallow answers, too. You should do that before locking the post, because the interface will be hidden afterwards. <% if moderator? %>If you just want to disable comments to shut down a heated discussion, use that tool.<% end %>
+ Locking a post disallows edits, comments and votes. Use post closure to disallow answers, too. You should do that before locking the post, because the interface will be hidden afterwards. <% if at_least_moderator? %>If you just want to disable comments to shut down a heated discussion, use that tool.<% end %>
<%= form_tag post_lock_path(post), remote: true, class: 'js-lock' do %>
<%= label_tag :length, 'Locked until', class: 'form-element', for: "purge_comments_id_#{post.id}" %>
- <% if current_user.is_moderator %>
+ <% if current_user.at_least_moderator? %>
You can set an number of days, after which the post will automatically unlock.
<% else %>
You have to set an number of days, after which the post will automatically unlock. Will default to 7 if none chosen. Maximum value: 30 days
-<%= line_chart Subscription.where('created_at >= ?', 1.year.ago).group_by_week(:created_at).count %>
\ No newline at end of file
+<%= line_chart Subscription.recent(1.year.ago).group_by_week(:created_at).count %>
diff --git a/app/views/shared/_lock_notice.html.erb b/app/views/shared/_lock_notice.html.erb
new file mode 100644
index 000000000..e73a51741
--- /dev/null
+++ b/app/views/shared/_lock_notice.html.erb
@@ -0,0 +1,16 @@
+<%#
+ Reusable helper view for lock notices (posts & comments).
+
+ Variables:
+ level : notice severity level (one of: 'info', 'error', default 'error')
+ text : notice text to display
+%>
+
+<%
+ #defaults
+ level ||= defined?(level) && !level.nil? ? level : 'error'
+%>
+
+
+ <%= sanitize(text) %>
+
\ No newline at end of file
diff --git a/app/views/shared/_markdown_tools.html.erb b/app/views/shared/_markdown_tools.html.erb
index daa3ea3d3..a7d95d34d 100644
--- a/app/views/shared/_markdown_tools.html.erb
+++ b/app/views/shared/_markdown_tools.html.erb
@@ -39,6 +39,15 @@
<% end %>
+
+ <%= md_button action: 'bullet', label: 'Bullet list', class: 'is-icon-only' do %>
+
+ <% end %>
+ <%= md_button action: 'numbered', label: 'Numbered list', class: 'is-icon-only' do %>
+
+ <% end %>
+
+
<%= md_button label: 'Link', class: 'is-icon-only', data_modal: '#markdown-link-insert' do %>
@@ -49,31 +58,25 @@
<%= md_button label: 'Insert image', class: 'is-icon-only', data_modal: '#markdown-image-upload' do %>
<% end %>
+ <%= md_button action: 'table', label: 'Insert table', class: 'is-icon-only' do %>
+
+ <% end %>
diff --git a/app/views/subscriptions/index.html.erb b/app/views/subscriptions/index.html.erb
index 8c8561f2a..12b0ef996 100644
--- a/app/views/subscriptions/index.html.erb
+++ b/app/views/subscriptions/index.html.erb
@@ -11,7 +11,7 @@
]
end
end.sort_by { |a| a }.map { |_, v| v }.each do |name, sub| %>
-
+ <%= name %>
Subscription to <%= phrase_for sub.type, sub.qualifier %>, emailed every <%=
pluralize(sub.frequency, 'day')
@@ -20,4 +20,4 @@ end.sort_by { |a| a }.map { |_, v| v }.each do |name, sub| %>
<%= label_tag :enabled, 'Enabled?' %> ·
Remove
-<% end %>
\ No newline at end of file
+<% end %>
diff --git a/app/views/sudo/sudo.html.erb b/app/views/sudo/sudo.html.erb
new file mode 100644
index 000000000..c7eefa8d9
--- /dev/null
+++ b/app/views/sudo/sudo.html.erb
@@ -0,0 +1,16 @@
+
Re-enter your password
+
+
+
+
+ You are entering sudo mode, where you are required to verify your password to take certain
+ security-sensitive actions. This mode will last for <%= AppConfig.server_settings['user_sudo_duration'] %>
+ minutes, during which time you will not be asked for your password again.
+
- The QR code below, when scanned, provides immediate access to your <%= t 'platform.network_name' %> account,
+ The QR code below, when scanned, provides immediate access to your <%= t 'platform.network_name' %> network account,
without asking for your password again. This makes it easier to sign in on your phone, but make sure nobody's
looking over your shoulder! Take extra care in public places.
diff --git a/app/views/users/registrations/delete.html.erb b/app/views/users/registrations/delete.html.erb
new file mode 100644
index 000000000..63a5d0b42
--- /dev/null
+++ b/app/views/users/registrations/delete.html.erb
@@ -0,0 +1,38 @@
+
Delete my account
+
+ If you no longer want to use our network of communities, you can delete your account. This means:
+
+
+
Your profiles will be deleted on every community in the network.
+
Your user account will be deleted and anonymized.
+
+ The content of your posts and comments will remain, under the licenses you set for them. Your username will no
+ longer be shown alongside them.
+
+
+
+
+
+
+ This will take effect immediately and cannot be undone. If you're sure, type your username below to
+ confirm.
+
+
+
+<% if @user.errors.any? %>
+
+
Couldn't delete your account:
+
+ <% @user.errors.full_messages.each do |msg| %>
+
<%= msg %>
+ <% end %>
+
+
+<% end %>
+
+<%= form_tag do_delete_account_path, method: :post do %>
+ <%= label_tag :username, 'Type your username', class: 'form-element' %>
+ <%= text_field_tag :username, params[:username], class: 'form-element' %>
+
+ <%= submit_tag 'Delete my account', class: 'button is-danger is-filled' %>
+<% end %>
diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb
index 11816d171..f5cbff1d4 100644
--- a/app/views/users/show.html.erb
+++ b/app/views/users/show.html.erb
@@ -2,7 +2,7 @@
<% content_for :title, "User #{rtl_safe_username(@user)}" %>
-<% if moderator? && deleted_user?(@user) %>
+<% if at_least_moderator? && deleted_user?(@user) %>
<%= render 'deleted', user: @user %>
<% end %>
@@ -71,7 +71,7 @@
Subscribe to user
<% end %>
<% end %>
- <% if current_user&.is_moderator %>
+ <% if current_user&.at_least_moderator? %>
Moderator Tools <% if @user.community_user.mod_warnings&.size.positive? %> (<%= pluralize(@user.community_user.mod_warnings.count, 'message') %>) <% end %>
- <% if current_user&.id == @user.id || current_user&.is_moderator %>
+ <% if current_user&.id == @user.id || current_user&.at_least_moderator? %>
User since <%= @user.created_at %>
<% end %>
@@ -241,7 +241,7 @@
Count
<%= @user.votes.count %>
- <% if @user.id == current_user&.id || current_user&.is_admin %>
+ <% if @user.id == current_user&.id || current_user&.admin? %>
@@ -290,7 +290,7 @@
Count
- <% if current_user&.id == @user.id || moderator? %>
+ <% if current_user&.id == @user.id || at_least_moderator? %>
<%= link_to @user.flags.count, flag_history_path(@user.id), class: 'is-muted',
'aria-label': "View flag history for #{@user.flags.count} flags" %>
<% else %>
diff --git a/bin/rails b/bin/rails
index 7a8ff81e6..efc037749 100755
--- a/bin/rails
+++ b/bin/rails
@@ -1,9 +1,4 @@
#!/usr/bin/env ruby
-begin
- load File.expand_path('spring', __dir__)
-rescue LoadError => e
- raise unless e.message.include?('spring')
-end
-APP_PATH = File.expand_path('../config/application', __dir__)
-require_relative '../config/boot'
-require 'rails/commands'
+APP_PATH = File.expand_path("../config/application", __dir__)
+require_relative "../config/boot"
+require "rails/commands"
diff --git a/bin/rake b/bin/rake
index 0ba8c48cb..4fbf10b96 100755
--- a/bin/rake
+++ b/bin/rake
@@ -1,9 +1,4 @@
#!/usr/bin/env ruby
-begin
- load File.expand_path('spring', __dir__)
-rescue LoadError => e
- raise unless e.message.include?('spring')
-end
-require_relative '../config/boot'
-require 'rake'
+require_relative "../config/boot"
+require "rake"
Rake.application.run
diff --git a/bin/rubocop b/bin/rubocop
new file mode 100755
index 000000000..40330c0ff
--- /dev/null
+++ b/bin/rubocop
@@ -0,0 +1,8 @@
+#!/usr/bin/env ruby
+require "rubygems"
+require "bundler/setup"
+
+# explicit rubocop config increases performance slightly while avoiding config confusion.
+ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__))
+
+load Gem.bin_path("rubocop", "rubocop")
diff --git a/bin/setup b/bin/setup
index a8e630c6a..934cd058f 100755
--- a/bin/setup
+++ b/bin/setup
@@ -1,11 +1,11 @@
#!/usr/bin/env ruby
-require 'fileutils'
+require "fileutils"
-# path to your application root.
-APP_ROOT = File.expand_path('..', __dir__)
+APP_ROOT = File.expand_path("..", __dir__)
+APP_NAME = "qpixel"
def system!(*args)
- system(*args) || abort("\n== Command #{args} failed ==")
+ system(*args, exception: true)
end
FileUtils.chdir APP_ROOT do
@@ -13,21 +13,25 @@ FileUtils.chdir APP_ROOT do
# This script is idempotent, so that you can run it at any time and get an expectable outcome.
# Add necessary setup steps to this file.
- puts '== Installing dependencies =='
- system! 'gem install bundler --conservative'
- system('bundle check') || system!('bundle install')
+ puts "== Installing dependencies =="
+ system! "gem install bundler --conservative"
+ system("bundle check") || system!("bundle install")
# puts "\n== Copying sample files =="
- # unless File.exist?('config/database.yml')
- # FileUtils.cp 'config/database.yml.sample', 'config/database.yml'
+ # unless File.exist?("config/database.yml")
+ # FileUtils.cp "config/database.yml.sample", "config/database.yml"
# end
puts "\n== Preparing database =="
- system! 'bin/rails db:prepare'
+ system! "bin/rails db:prepare"
puts "\n== Removing old logs and tempfiles =="
- system! 'bin/rails log:clear tmp:clear'
+ system! "bin/rails log:clear tmp:clear"
puts "\n== Restarting application server =="
- system! 'bin/rails restart'
+ system! "bin/rails restart"
+
+ # puts "\n== Configuring puma-dev =="
+ # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}"
+ # system "curl -Is https://#{APP_NAME}.test/up | head -n 1"
end
diff --git a/config/boot.rb b/config/boot.rb
index d69bd27dc..282011619 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,3 +1,3 @@
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" # Set up gems listed in the Gemfile.
diff --git a/config/config/preferences.yml b/config/config/preferences.yml
index 5d72b1204..45e7f29bb 100644
--- a/config/config/preferences.yml
+++ b/config/config/preferences.yml
@@ -83,4 +83,18 @@ sticky_header:
default_filter_name:
type: ~
default: none
- category: true
\ No newline at end of file
+ category: true
+
+collapse_hot_posts:
+ type: boolean
+ description: >
+ Collapse the Hot Posts widget in the sidebar by default.
+ default: 'false'
+ global: true
+
+collapse_related_posts:
+ type: boolean
+ description: >
+ Collapse the Related Posts widget in the sidebar by default.
+ default: 'false'
+ global: true
diff --git a/config/config/server_settings.yml b/config/config/server_settings.yml
new file mode 100644
index 000000000..60f36f727
--- /dev/null
+++ b/config/config/server_settings.yml
@@ -0,0 +1,2 @@
+registration_rate_limit: 300
+user_sudo_duration: 30
diff --git a/config/environments/development.rb b/config/environments/development.rb
index f6fa52367..18e37f31f 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -85,6 +85,8 @@
host: 'meta.codidact.com', protocol: ENV['MAILER_PROTOCOL'] || 'https'
}
+ config.active_job.queue_adapter = :inline
+
# Ensure docker ip added to allowed, given that we are in container
if File.file?('/.dockerenv') == true
host_ip = `/sbin/ip route|awk '/default/ { print $3 }'`.strip
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 8212caac0..feb02ce6a 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -13,6 +13,8 @@
config.cache_classes = false
config.action_view.cache_template_loading = true
+ config.log_level = :info
+
# Do not eager load code on boot. This avoids loading your whole application
# just for the purpose of running a single test. If you are using a tool that
# preloads Rails for running tests, you may have to set it to true.
@@ -61,6 +63,8 @@
protocol: ENV['MAILER_PROTOCOL'] || 'https'
}
+ config.active_job.queue_adapter = :test
+
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
@@ -75,4 +79,7 @@
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
+
+ # Don't colorize logs - we are writing to log files directly
+ config.colorize_logging = false
end
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index 54f47cf15..b3076b38f 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -16,9 +16,9 @@
# # policy.report_uri "/csp-violation-report-endpoint"
# end
#
-# # Generate session nonces for permitted importmap and inline scripts
+# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
-# config.content_security_policy_nonce_directives = %w(script-src)
+# config.content_security_policy_nonce_directives = %w(script-src style-src)
#
# # Report violations without enforcing the policy.
# # config.content_security_policy_report_only = true
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 9eadc984e..d07064545 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -11,7 +11,6 @@
# Devise will use the `secret_key_base` as its `secret_key`
# by default. You can change it below and use your own secret key.
# config.secret_key = 'a01993900cb61b8d32ff3a777b37164cc49d880aaa8e03c3f9a2990612fa1b4c588d934556e60ea4f51dd334d23caa8ca9ba6a84d651cb412a3d155096b74556'
- config.secret_key = 'd379fc6a0bc73bc2faf863d6846f1bd676af3adb46b594f8b3287c52b54cd303556961e03716e370c0031692ec9fc698aee177676f939f3825befbf4b5ef8e9a'
# ==> Mailer Configuration
# Configure the e-mail address which will be shown in Devise::Mailer,
@@ -110,7 +109,7 @@
# config.pepper = '87767bd8e2fa1b42fdf61c33717d80f306570b0c0eb964f090023e7e23f3f196ea847ea7a1c7dd16efff145511e9aa0a80094024708dbe3406b1a184f43ee18a'
# Send a notification email when the user's password is changed
- # config.send_password_change_notification = false
+ config.send_password_change_notification = true
# ==> Configuration for :confirmable
# A period that the user is allowed to access the website even without
@@ -126,7 +125,7 @@
# their account can't be confirmed with the token any more.
# Default is nil, meaning there is no restriction on how long a user can take
# before confirming their account.
- # config.confirm_within = 3.days
+ config.confirm_within = 3.hours
# If true, requires any email changes to be confirmed (exactly the same way as
# initial account confirmation) to be applied. Requires additional unconfirmed_email
@@ -169,27 +168,27 @@
# Defines which strategy will be used to lock an account.
# :failed_attempts = Locks an account after a number of failed attempts to sign in.
# :none = No lock strategy. You should handle locking by yourself.
- # config.lock_strategy = :failed_attempts
+ config.lock_strategy = :failed_attempts
# Defines which key will be used when locking and unlocking an account
- # config.unlock_keys = [:email]
+ config.unlock_keys = [:email]
# Defines which strategy will be used to unlock an account.
# :email = Sends an unlock link to the user email
# :time = Re-enables login after a certain amount of time (see :unlock_in below)
# :both = Enables both strategies
# :none = No unlock strategy. You should handle unlocking by yourself.
- # config.unlock_strategy = :both
+ config.unlock_strategy = :email
# Number of authentication tries before locking an account if lock_strategy
# is failed attempts.
- # config.maximum_attempts = 20
+ config.maximum_attempts = 10
# Time interval to unlock the account if :time is enabled as unlock_strategy.
# config.unlock_in = 1.hour
# Warn on the last attempt before the account is locked.
- # config.last_attempt_warning = true
+ config.last_attempt_warning = true
# ==> Configuration for :recoverable
#
@@ -199,7 +198,7 @@
# Time interval you can reset your password with a reset password key.
# Don't put a too small interval or your users won't have the time to
# change their passwords.
- config.reset_password_within = 6.hours
+ config.reset_password_within = 3.hours
# When set to false, does not sign a user in automatically after their password is
# reset. Defaults to true, so a user is signed in automatically after a reset.
diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
index adc6568ce..c010b83dd 100644
--- a/config/initializers/filter_parameter_logging.rb
+++ b/config/initializers/filter_parameter_logging.rb
@@ -1,8 +1,8 @@
# Be sure to restart your server when you modify this file.
-# Configure parameters to be filtered from the log file. Use this to limit dissemination of
-# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
-# notations and behaviors.
+# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file.
+# Use this to limit dissemination of sensitive information.
+# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors.
Rails.application.config.filter_parameters += [
- :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
+ :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
]
diff --git a/config/initializers/new_framework_defaults_7_2.rb b/config/initializers/new_framework_defaults_7_2.rb
new file mode 100644
index 000000000..b549c4a25
--- /dev/null
+++ b/config/initializers/new_framework_defaults_7_2.rb
@@ -0,0 +1,70 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file eases your Rails 7.2 framework defaults upgrade.
+#
+# Uncomment each configuration one by one to switch to the new default.
+# Once your application is ready to run with all new defaults, you can remove
+# this file and set the `config.load_defaults` to `7.2`.
+#
+# Read the Guide for Upgrading Ruby on Rails for more info on each option.
+# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html
+
+###
+# Controls whether Active Job's `#perform_later` and similar methods automatically defer
+# the job queuing to after the current Active Record transaction is committed.
+#
+# Example:
+# Topic.transaction do
+# topic = Topic.create(...)
+# NewTopicNotificationJob.perform_later(topic)
+# end
+#
+# In this example, if the configuration is set to `:never`, the job will
+# be enqueued immediately, even though the `Topic` hasn't been committed yet.
+# Because of this, if the job is picked up almost immediately, or if the
+# transaction doesn't succeed for some reason, the job will fail to find this
+# topic in the database.
+#
+# If `enqueue_after_transaction_commit` is set to `:default`, the queue adapter
+# will define the behaviour.
+#
+# Note: Active Job backends can disable this feature. This is generally done by
+# backends that use the same database as Active Record as a queue, hence they
+# don't need this feature.
+#++
+# Rails.application.config.active_job.enqueue_after_transaction_commit = :default
+
+###
+# Adds image/webp to the list of content types Active Storage considers as an image
+# Prevents automatic conversion to a fallback PNG, and assumes clients support WebP, as they support gif, jpeg, and png.
+# This is possible due to broad browser support for WebP, but older browsers and email clients may still not support
+# WebP. Requires imagemagick/libvips built with WebP support.
+#++
+# Rails.application.config.active_storage.web_image_content_types = %w[image/png image/jpeg image/gif image/webp]
+
+###
+# Enable validation of migration timestamps. When set, an ActiveRecord::InvalidMigrationTimestampError
+# will be raised if the timestamp prefix for a migration is more than a day ahead of the timestamp
+# associated with the current time. This is done to prevent forward-dating of migration files, which can
+# impact migration generation and other migration commands.
+#
+# Applications with existing timestamped migrations that do not adhere to the
+# expected format can disable validation by setting this config to `false`.
+#++
+# Rails.application.config.active_record.validate_migration_timestamps = true
+
+###
+# Controls whether the PostgresqlAdapter should decode dates automatically with manual queries.
+#
+# Example:
+# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.select_value("select '2024-01-01'::date") #=> Date
+#
+# This query used to return a `String`.
+#++
+# Rails.application.config.active_record.postgresql_adapter_decode_dates = true
+
+###
+# Enables YJIT as of Ruby 3.3, to bring sizeable performance improvements. If you are
+# deploying to a memory constrained environment you may want to set this to `false`.
+#++
+# Rails.application.config.yjit = true
diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb
index 00f64d71b..7db3b9577 100644
--- a/config/initializers/permissions_policy.rb
+++ b/config/initializers/permissions_policy.rb
@@ -1,11 +1,13 @@
+# Be sure to restart your server when you modify this file.
+
# Define an application-wide HTTP permissions policy. For further
-# information see https://developers.google.com/web/updates/2018/06/feature-policy
-#
-# Rails.application.config.permissions_policy do |f|
-# f.camera :none
-# f.gyroscope :none
-# f.microphone :none
-# f.usb :none
-# f.fullscreen :self
-# f.payment :self, "https://secure.example.com"
+# information see: https://developers.google.com/web/updates/2018/06/feature-policy
+
+# Rails.application.config.permissions_policy do |policy|
+# policy.camera :none
+# policy.gyroscope :none
+# policy.microphone :none
+# policy.usb :none
+# policy.fullscreen :self
+# policy.payment :self, "https://secure.example.com"
# end
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
index bd4c3ebc6..3469b2f27 100644
--- a/config/locales/devise.en.yml
+++ b/config/locales/devise.en.yml
@@ -18,11 +18,11 @@ en:
unconfirmed: "You have to confirm your email address before continuing."
mailer:
confirmation_instructions:
- subject: "Confirmation instructions"
+ subject: "New Codidact account confirmation instructions"
reset_password_instructions:
- subject: "Reset password instructions"
+ subject: "Codidact reset password instructions"
unlock_instructions:
- subject: "Unlock instructions"
+ subject: "Codidact unlock instructions"
password_change:
subject: "Password Changed"
omniauth_callbacks:
diff --git a/config/locales/strings/en.admin.yml b/config/locales/strings/en.admin.yml
index ac389d08a..d311aaf31 100644
--- a/config/locales/strings/en.admin.yml
+++ b/config/locales/strings/en.admin.yml
@@ -18,6 +18,9 @@ en:
error_search_uuid: 'Search for an error UUID'
privileges_blurb: >
Here you can define the reputation required to gain each available privilege. Click on a value to edit it.
+ email_query_blurb: >
+ This tool allows you to query for a user by their email address. If a user is found, you will be shown a list of
+ their profiles on communities on which you are an admin.
tools:
g_site_settings: 'Global Site Settings'
g_tag_sets: 'Global Tag Sets'
@@ -31,4 +34,8 @@ en:
licenses: 'Licenses'
audit_log: 'Audit Log'
post_types: 'Post Types'
+ email_query: 'User Lookup by Email'
user_fed_stat: 'User fed to STAT.'
+ errors:
+ email_query_not_found:
+ No user found with that email address.
diff --git a/config/locales/strings/en.comments.yml b/config/locales/strings/en.comments.yml
new file mode 100644
index 000000000..044ee1a4b
--- /dev/null
+++ b/config/locales/strings/en.comments.yml
@@ -0,0 +1,39 @@
+en:
+ comments:
+ errors:
+ disabled_on_archived_threads: >
+ Archived threads cannot be replied to.
+ disabled_on_deleted_posts: >
+ Comments are disabled on deleted posts.
+ disabled_on_deleted_threads: >
+ Deleted threads cannot be replied to.
+ disabled_on_locked_posts: >
+ Comments are disabled on locked posts.
+ disabled_on_locked_threads: >
+ Locked threads cannot be replied to.
+ disabled_on_post_specific: >
+ Comments on this post are disabled.
+ disabled_on_post_generic: >
+ This post cannot be commented on.
+ disabled_on_thread_generic: >
+ This thread cannot be replied to.
+ mod_only_undelete: >
+ Threads deleted by a moderator can only be undeleted by a moderator.
+ new_user_rate_limited: >
+ As a new user, you can only comment on your own posts and on answers to them.
+ rate_limited: >
+ You have used your daily limit of %{count} comments. Come back tomorrow to continue.
+ exempt_from_disabled: >
+ However, you are exempt as a moderator.
+ delete_comment_server_error: >
+ Something went wrong when trying to delete the comment.
+ undelete_comment_server_error: >
+ Something went wrong when trying to undelete the comment.
+ labels:
+ create_new_thread: >
+ Start new comment thread
+ reply_to_thread: >
+ Reply to this thread
+ warnings:
+ unrelated_user_not_pinged: >
+ This user was not notified because they have not participated in this thread.
diff --git a/config/locales/strings/en.g.yml b/config/locales/strings/en.g.yml
index 1fdf073cc..2a6d8db17 100644
--- a/config/locales/strings/en.g.yml
+++ b/config/locales/strings/en.g.yml
@@ -25,4 +25,4 @@ en:
user: 'user'
platform:
- network_name: 'Codidact network'
+ network_name: 'Codidact'
diff --git a/config/locales/strings/en.users.yml b/config/locales/strings/en.users.yml
new file mode 100644
index 000000000..bf2863bca
--- /dev/null
+++ b/config/locales/strings/en.users.yml
@@ -0,0 +1,11 @@
+en:
+ users:
+ errors:
+ no_admin_self_delete: >
+ Admin accounts cannot be self-deleted. Contact support.
+ no_mod_self_delete: >
+ Moderator accounts cannot be self-deleted. Contact support.
+ no_2fa_self_delete: >
+ Accounts using 2FA cannot be self-deleted. Disable 2FA first.
+ self_delete_wrong_username: >
+ The username you entered was incorrect.
diff --git a/config/routes.rb b/config/routes.rb
index 5a9c2f225..61f60b596 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -15,11 +15,14 @@
get 'users/saml/sign_in_request_from_other/:id', to: 'users/saml_sessions#sign_in_request_from_other', as: :sign_in_request_from_other
get 'users/saml/sign_in_return_from_base', to: 'users/saml_sessions#sign_in_return_from_base', as: :sign_in_return_from_base
get 'users/saml/after_sign_in_check', to: 'users/saml_sessions#after_sign_in_check', as: :after_sign_in_check
+ get 'users/delete', to: 'users/registrations#delete', as: :delete_account
+ post 'users/delete', to: 'users/registrations#do_delete', as: :do_delete_account
end
root to: 'categories#homepage'
mount LetterOpenerWeb::Engine, at: "/letter_opener" if Rails.env.development?
+ mount Rack::Directory.new('coverage/'), at: '/coverage' if Rails.env.development?
mount MaintenanceTasks::Engine, at: '/maintenance'
scope 'admin' do
@@ -55,6 +58,9 @@
post 'impersonate/:id', to: 'admin#change_users', as: :impersonate
get 'impersonate/:id', to: 'admin#impersonate', as: :start_impersonating
+ get 'email-query', to: 'admin#email_query', as: :admin_email_query
+ post 'email-query', to: 'admin#do_email_query', as: :do_email_query
+
scope 'post-types' do
root to: 'post_types#index', as: :post_types
get 'new', to: 'post_types#new', as: :new_post_type
@@ -200,6 +206,8 @@
get '/avatar/:letter/:color/:size', to: 'users#specific_avatar', as: :specific_auto_avatar
get '/disconnect-sso', to: 'users#disconnect_sso', as: :user_disconnect_sso
post '/disconnect-sso', to: 'users#confirm_disconnect_sso', as: :user_confirm_disconnect_sso
+ get '/sudo', to: 'sudo#sudo', as: :user_sudo
+ post '/sudo', to: 'sudo#enter_sudo', as: :enter_sudo
get '/:id', to: 'users#show', as: :user
get '/:id/flags', to: 'flags#history', as: :flag_history
get '/:id/activity', to: 'users#activity', as: :user_activity
@@ -239,6 +247,7 @@
post 'post/:post_id/follow', to: 'comments#post_follow', as: :follow_post_comments
get ':id', to: 'comments#show', as: :comment
get 'thread/:id', to: 'comments#thread', as: :comment_thread
+ get 'thread/:id/content', to: 'comments#thread_content', as: :comment_thread_content
post ':id/edit', to: 'comments#update', as: :update_comment
delete ':id/delete', to: 'comments#destroy', as: :delete_comment
patch ':id/delete', to: 'comments#undelete', as: :undelete_comment
@@ -363,7 +372,7 @@
get '418', to: 'errors#stat'
get '422', to: 'errors#unprocessable_entity'
get '423', to: 'errors#read_only'
- get '500', to: 'errors#internal_server_error'
+ get '500', to: 'errors#internal_server_error', as: :server_error
get 'osd', to: 'application#osd', as: :osd
diff --git a/config/schedule.rb b/config/schedule.rb
index 1b1c9c343..35d707b38 100644
--- a/config/schedule.rb
+++ b/config/schedule.rb
@@ -25,3 +25,7 @@
every 6.hours do
runner 'scripts/recalc_abilities.rb'
end
+
+every 30.minutes do
+ runner 'scripts/run_summary_mailer.rb'
+end
diff --git a/db/seeds/post_history_types.yml b/db/seeds/post_history_types.yml
index d12b6e28c..d2bfbf55b 100644
--- a/db/seeds/post_history_types.yml
+++ b/db/seeds/post_history_types.yml
@@ -10,4 +10,7 @@
- name: imported_from_external_source
- name: nominated_for_promotion
- name: promotion_removed
-- name: history_hidden
\ No newline at end of file
+- name: history_hidden
+- name: category_changed
+- name: post_locked
+- name: post_unlocked
\ No newline at end of file
diff --git a/db/seeds/site_settings.yml b/db/seeds/site_settings.yml
index 48a1ca475..ffc1b6301 100644
--- a/db/seeds/site_settings.yml
+++ b/db/seeds/site_settings.yml
@@ -223,6 +223,15 @@
value: Codidact
value_type: string
category: Email
+ description: >
+ Community-specific name of the sender of no-reply emails
+ (see NoReplySenderName in global site settings for details)
+
+- name: NoReplySenderName
+ value: Codidact
+ value_type: string
+ category: Email
+ community_id: ~
description: >
The name of the sender of no-reply emails.
@@ -230,6 +239,15 @@
value: noreply@codidact.com
value_type: string
category: Email
+ description: >
+ Community-specific address to send no-reply emails from
+ (see NoReplySenderEmail in global site settings for details).
+
+- name: NoReplySenderEmail
+ value: noreply@codidact.com
+ value_type: string
+ category: Email
+ community_id: ~
description: >
The address to send no-reply emails from (can be a fake address).
Example uses of this address are 2FA emails, flag notifications, account emails, and more.
@@ -411,84 +429,98 @@
value_type: integer
category: RateLimits
description: >
- The amount of votes on questions, articles and answers to non-own questions new users may cast within 24h.
+ The number of votes on questions, articles and answers to non-own questions new users may cast within 24h.
- name: RL_Votes
value: 30
value_type: integer
category: RateLimits
description: >
- The amount of votes on questions, articles and answers to non-own questions users with the unrestricted ability may cast within 24h.
+ The number of votes on questions, articles and answers to non-own questions users with the unrestricted ability may cast within 24h.
- name: RL_NewUserTopLevelPosts
value: 3
value_type: integer
category: RateLimits
description: >
- The amount of questions and articles new users may post within 24h.
+ The number of questions and articles new users may post within 24h.
- name: RL_TopLevelPosts
value: 20
value_type: integer
category: RateLimits
description: >
- The amount of questions and articles users with the unrestricted ability may post within 24h.
+ The number of questions and articles users with the unrestricted ability may post within 24h.
- name: RL_NewUserSecondLevelPosts
value: 10
value_type: integer
category: RateLimits
description: >
- The amount of answers new users may post within 24h.
+ The number of answers new users may post within 24h.
- name: RL_SecondLevelPosts
value: 30
value_type: integer
category: RateLimits
description: >
- The amount of answers users with the unrestricted ability may post within 24h.
+ The number of answers users with the unrestricted ability may post within 24h.
- name: RL_NewUserFlags
value: 10
value_type: integer
category: RateLimits
description: >
- The amount of flags new users may raise within 24h.
+ The number of flags new users may raise within 24h.
- name: RL_Flags
value: 30
value_type: integer
category: RateLimits
description: >
- The amount of flags users with the unrestricted ability may raise within 24h.
+ The number of flags users with the unrestricted ability may raise within 24h.
- name: RL_NewUserSuggestedEdits
value: 3
value_type: integer
category: RateLimits
description: >
- The amount of edits new users may suggest within 24h.
+ The number of edits new users may suggest within 24h.
- name: RL_SuggestedEdits
value: 20
value_type: integer
category: RateLimits
description: >
- The amount of edits users with the unrestricted ability may suggest within 24h.
+ The number of edits users with the unrestricted ability may suggest within 24h.
- name: RL_NewUserComments
value: 0
value_type: integer
category: RateLimits
description: >
- The amount of comments new users may add on other people's posts within 24h.
+ The number of comments new users may add on other people's posts within 24h.
+
+- name: RL_NewUserCommentsOwnPosts
+ value: 30
+ value_type: integer
+ category: RateLimits
+ description: >
+ The number of comments new users may add on their own posts or answers to them within 24h.
- name: RL_Comments
value: 50
value_type: integer
category: RateLimits
description: >
- The amount of comments users with the unrestricted ability may add on other people's posts within 24h.
+ The number of comments users with the unrestricted ability may add on other people's posts within 24h.
+
+- name: RL_CommentsOwnPosts
+ value: 50
+ value_type: integer
+ category: RateLimits
+ description: >
+ The number of comments users with the unrestricted ability may add on their posts or answers to them within 24h.
- name: TableOfContentsThreshold
value: 5
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 8944ffb63..7ee55471c 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -22,18 +22,20 @@ RUN bundle install
# cherry pick only what we really need to run Node.js
COPY --from=node /usr/local/bin/node /usr/local/bin
-COPY --from=node /usr/local/bin/nodejs /usr/local/bin
-COPY --from=node /usr/local/bin/npm /usr/local/bin
-COPY --from=node /usr/local/bin/npx /usr/local/bin
-COPY --from=node /usr/local/bin/yarn /usr/local/bin
-COPY --from=node /usr/local/bin/yarnpkg /usr/local/bin
COPY --from=node /usr/local/include/node /usr/local/include
-COPY --from=node /usr/local/lib/node_modules /usr/local/lib
+COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node /usr/local/share/doc/node /usr/local/share/doc
COPY --from=node /usr/local/share/man/man1/node.1 /usr/local/share/man/man1
COPY --from=node /usr/local/share/systemtap/tapset/node.stp /usr/local/share/systemtap/tapset
COPY --from=node /opt/yarn-v1.22.4 /opt/yarn-v1.22.4
+# create symlinks needed to run Node.js & NPM
+RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm
+RUN ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
+RUN ln -s /opt/yarn-v1.22.4/bin/yarn /usr/local/bin/yarn
+RUN ln -s /opt/yarn-v1.22.4/bin/yarnpkg /usr/local/bin/yarnpkg
+RUN ln -s /usr/local/bin/node /usr/local/bin/nodejs
+
FROM build
# setup a dedicated user for Node.js
diff --git a/docker/Dockerfile.arm b/docker/Dockerfile.arm
deleted file mode 100644
index b6790b416..000000000
--- a/docker/Dockerfile.arm
+++ /dev/null
@@ -1,55 +0,0 @@
-FROM ruby:3.1.2-bullseye AS ruby
-FROM node:12.18.3-slim AS node
-
-FROM ruby AS build
-
-# Set all encoding to UTF-8
-ENV RUBYOPT="-KU -E utf-8:utf-8"
-
-# Install additional dependencies not present in the base image
-RUN apt-get update && \
- apt-get install -y bison \
- build-essential \
- libxslt-dev \
- default-mysql-server
-
-# Add core code to container
-WORKDIR /code
-COPY . /code
-
-RUN gem install bundler:2.4.13
-RUN bundle install
-
-# cherry pick only what we really need to run Node.js
-COPY --from=node /usr/local/bin/node /usr/local/bin
-COPY --from=node /usr/local/bin/nodejs /usr/local/bin
-COPY --from=node /usr/local/bin/npm /usr/local/bin
-COPY --from=node /usr/local/bin/npx /usr/local/bin
-COPY --from=node /usr/local/bin/yarn /usr/local/bin
-COPY --from=node /usr/local/bin/yarnpkg /usr/local/bin
-COPY --from=node /usr/local/include/node /usr/local/include
-COPY --from=node /usr/local/lib/node_modules /usr/local/lib
-COPY --from=node /usr/local/share/doc/node /usr/local/share/doc
-COPY --from=node /usr/local/share/man/man1/node.1 /usr/local/share/man/man1
-COPY --from=node /usr/local/share/systemtap/tapset/node.stp /usr/local/share/systemtap/tapset
-COPY --from=node /opt/yarn-v1.22.4 /opt/yarn-v1.22.4
-
-FROM build
-
-# setup a dedicated user for Node.js
-RUN groupadd --gid 1000 node
-RUN useradd --uid 1000 \
- --gid node \
- --shell /bin/bash \
- --create-home node
-
-# setup Node.js environment
-ENV NODEJS_HOME=/usr/local/bin/node
-ENV PATH=$NODEJS_HOME:$PATH
-
-WORKDIR /code
-
-EXPOSE 80 443 3000
-ENTRYPOINT ["/bin/bash"]
-CMD ["/code/docker/entrypoint.sh"]
-
diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev
index 994452878..e1e3abea1 100644
--- a/docker/Dockerfile.dev
+++ b/docker/Dockerfile.dev
@@ -11,7 +11,8 @@ RUN apt-get update && \
apt-get install -y bison \
build-essential \
libxslt-dev \
- default-mysql-server
+ default-mysql-server \
+ firefox-esr
# Add core code to container
WORKDIR /code
@@ -22,18 +23,20 @@ RUN bundle install
# cherry pick only what we really need to run Node.js
COPY --from=node /usr/local/bin/node /usr/local/bin
-COPY --from=node /usr/local/bin/nodejs /usr/local/bin
-COPY --from=node /usr/local/bin/npm /usr/local/bin
-COPY --from=node /usr/local/bin/npx /usr/local/bin
-COPY --from=node /usr/local/bin/yarn /usr/local/bin
-COPY --from=node /usr/local/bin/yarnpkg /usr/local/bin
COPY --from=node /usr/local/include/node /usr/local/include
-COPY --from=node /usr/local/lib/node_modules /usr/local/lib
+COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=node /usr/local/share/doc/node /usr/local/share/doc
COPY --from=node /usr/local/share/man/man1/node.1 /usr/local/share/man/man1
COPY --from=node /usr/local/share/systemtap/tapset/node.stp /usr/local/share/systemtap/tapset
COPY --from=node /opt/yarn-v1.22.4 /opt/yarn-v1.22.4
+# create symlinks needed to run Node.js & NPM
+RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm
+RUN ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
+RUN ln -s /opt/yarn-v1.22.4/bin/yarn /usr/local/bin/yarn
+RUN ln -s /opt/yarn-v1.22.4/bin/yarnpkg /usr/local/bin/yarnpkg
+RUN ln -s /usr/local/bin/node /usr/local/bin/nodejs
+
FROM build
# setup a dedicated user for Node.js
diff --git a/docker/README.md b/docker/README.md
index 7aafe9b9f..58bc64c33 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -156,7 +156,7 @@ That's it!
Running in this docker-compose setup, the system does not actually send emails. However, you can see the emails that would have been sent by going to [http://localhost:3000/letter_opener](http://localhost:3000/letter_opener).
This is especially useful to confirm other accounts that you make in the container.
-### 9. Running commands in the docker container
+## 9. Running commands in the docker container
Often, it may be useful to run some ruby/rails code directly, e.g. for debugging purposes. You can do so with the following command:
```bash
@@ -178,7 +178,31 @@ RequestContext.community = Community.first
This correctly scopes all database actions to the first (and probably only) community in your system.
-### 10. Stop Containers
+## 10. Running tests
+For full details, refer to [The Rails Test Runner](https://guides.rubyonrails.org/testing.html#the-rails-test-runner).
+
+To run the tests (except the system tests):
+
+```bash
+$ docker compose exec uwsgi rails test
+```
+
+To run the system tests:
+
+```bash
+$ docker compose exec uwsgi rails test:system
+```
+
+The system tests require a browser to be available (by default Firefox). Firefox is included in the uwsgi container if
+you use `Dockerfile.dev`, otherwise you will need to install a browser.
+
+To run all of the tests (including the system tests):
+
+```bash
+$ docker compose exec uwsgi rails test:all
+```
+
+## 11. Stop Containers
When you are finished, don't forget to clean up.
@@ -187,7 +211,7 @@ docker compose stop
docker compose rm
```
-### 11. Next steps
+## 12. Next steps
The current goal of this container is to provide a development environment for
working on QPixel. This deployment has not been tested with email notifications
diff --git a/global.d.ts b/global.d.ts
index 488a9f0fe..8828b7467 100644
--- a/global.d.ts
+++ b/global.d.ts
@@ -67,27 +67,61 @@ interface QPixelKeyboard {
type NotificationType = "warning" | "success" | "danger";
+type QPixelPopupCallback = (ev: JQuery.ClickEvent, popup: QPixelPopup) => void
+
+type QPixelPingablePopupCallback = (ev: JQuery.KeyUpEvent)=> Promise
+
declare class QPixelPopup {
static destroyAll: () => void;
static getPopup: (
items: JQuery[],
- field: JQuery,
- callback: (ev: JQuery.Event, popup: QPixelPopup) => void
+ field: HTMLInputElement | HTMLTextAreaElement,
+ callback: QPixelPopupCallback
) => QPixelPopup;
static isSpecialKey: (keyCode: number) => boolean;
- constructor(items: JQuery[], field: JQuery, callback: (ev: JQuery.Event, popup: QPixelPopup) => void);
+ constructor(
+ items: JQuery[],
+ field: HTMLInputElement | HTMLTextAreaElement,
+ callback: QPixelPopupCallback
+ );
destroy: () => void;
getActiveIdx: () => number | null;
setActive: (index: number) => void;
- setCallback: (callback: (ev: JQuery.Event, popup: QPixelPopup) => void) => void;
+ setCallback: (callback: QPixelPopupCallback) => void;
getClickHandler: () => (ev: JQuery.Event) => void;
getKeyHandler: () => (ev: JQuery.KeyboardEventBase) => void;
updateItems: (items: JQuery[]) => void;
updatePosition: () => void;
}
+type QPixelResponseJSON = {
+ status: 'success' | 'failed',
+ message?: string,
+ errors?: string[]
+}
+
+type QPixelComment = {
+ id: number
+ created_at: string
+ updated_at: string
+ post_id: number
+ content: string
+ deleted: boolean
+ user_id: number
+ community_id: number
+ comment_thread_id: number
+ has_reference: false
+ reference_text: string | null
+ references_comment_id: string | null
+}
+
+interface GetThreadContentOptions {
+ inline?: boolean,
+ showDeleted?: boolean
+}
+
interface QPixel {
// private properties
_filters?: Filter[] | null;
@@ -97,31 +131,202 @@ interface QPixel {
_user?: User | null;
// private methods
+
+ /**
+ * Call _fetchPreferences but only the first time to prevent redundant HTTP requests
+ */
_cachedFetchPreferences?: () => Promise;
+
+ /**
+ * Update local variable _preferences and localStorage with an AJAX call for the user preferences
+ */
_fetchPreferences?: () => Promise;
+
+ /**
+ * FIFO-style fetch wrapper for /users/me requests
+ */
_fetchUser?: () => Promise;
+
+ /**
+ * Get an object containing the current user's preferences. Loads, in order of precedence, from local variable,
+ * {@link localStorage}, or Redis via AJAX.
+ * @returns JSON object containing user preferences
+ */
_getPreferences?: () => Promise;
+
+ /**
+ * Get the key to use for storing user preferences in localStorage, to avoid conflating users
+ * @returns string the localStorage key
+ */
_preferencesLocalStorageKey?: () => string;
+
+ /**
+ * Set local variable _preferences and localStorage to new preferences data
+ * @param data an object, containing the new preferences data
+ */
_updatePreferencesLocally?: (data: UserPreferences) => void;
// public methods
+
+ /**
+ * Add a button to the Markdown editor.
+ * @param $buttonHtml the HTML content that the button should show - just text, if you like, or
+ * something more complex if you want to.
+ * @param shortName a short name for the action that will be used as the title and aria-label attributes.
+ * @param callback a function that will be passed as the click event callback.
+ */
addEditorButton?: ($buttonHtml: JQuery.htmlString, shortName: string, callback: () => void) => void;
+
+ /**
+ * Add a validator that will be called before creating a post.
+ * callback should take one parameter, the post text, and should return an array in
+ * the following format:
+ *
+ * [
+ * true | false, // is the post valid for this check?
+ * [
+ * { type: 'warning', message: 'warning message - will not block posting' },
+ * { type: 'error', message: 'error message - will block posting' }
+ * ]
+ * ]
+ */
addPrePostValidation?: (callback: PostValidator) => void;
+
+ /**
+ * Get the current CSRF anti-forgery token. Should be passed as the X-CSRF-Token header when
+ * making AJAX POST requests.
+ */
csrfToken?: () => string;
+
+ /**
+ * Create a notification popup - not an inbox notification.
+ * @param type the type to apply to the popup - warning, danger, etc.
+ * @param message the message to show
+ */
createNotification?: (type: NotificationType, message: string) => void;
+
+ /**
+ * Get the word in a string that the given position is in, and the position within that word.
+ * @param splat an array, containing the string already split by however you define a "word"
+ * @param posIdx the index to search for
+ * @returns the word the given position is in, and the position within that word
+ */
currentCaretSequence?: (splat: string[], posIdx: number) => [string, number];
+ /**
+ * Fetches default user filter for a given category
+ * @param categoryId id of the category to fetch
+ */
defaultFilter?: (categoryId: string) => Promise;
deleteFilter?: (name: string, system?: boolean) => Promise;
filters?: () => Promise>;
- offset?: ($el: JQuery) => ElementOffset;
+
+ /**
+ * Get the absolute offset of an element.
+ * @param element the element for which to find the offset.
+ * @returns element offset information
+ */
+ offset?: (element: HTMLElement) => ElementOffset;
+
+ /**
+ * Get a single user preference by name.
+ * @param name the name of the requested preference
+ * @param community is the requested preference community-local (true), or network-wide (false)?
+ * @returns the value of the requested preference
+ */
preference?: (name: string, community?: boolean) => Promise;
- replaceSelection?: ($field: JQuery, text: string) => void;
+
+ /**
+ * Replace the selected text in an input field with a provided replacement.
+ * @param $field the field in which to replace text
+ * @param text the text with which to replace the selection
+ */
+ replaceSelection?: ($field: JQuery, text: string) => void;
setFilter?: (name: string, filter: Filter, category: string, isDefault: boolean) => Promise;
setFilterAsDefault?: (categoryId: string, name: string) => Promise;
+
+ /**
+ * Set a user preference by name to the value provided.
+ * @param name the name of the preference to set
+ * @param value the value to set to - must respond to toString() for {@link localStorage} and Redis
+ * @param community is this preference community-local (true), or network-wide (false)?
+ */
setPreference?: (name: string, value: unknown, community?: boolean) => Promise;
+
+ /**
+ * Get the user object for the current user.
+ * @returns JSON object containing user details
+ */
user?: () => Promise;
+
+ /**
+ * Internal. Called just before a post is sent to the server to validate that it passes
+ * all custom checks.
+ */
validatePost?: (postText: string) => [boolean, PostValidatorMessage[]];
+ /**
+ * Send a request with JSON data, pre-authorized with QPixel credentials for the signed in user.
+ * @param uri The URI to which to send the request.
+ * @param data An object containing data to send as the request body. Must be acceptable by JSON.stringify.
+ * @param options An optional {@link RequestInit} to override the defaults provided by this method.
+ * @returns The Response promise returned from {@link fetch}.
+ */
+ fetchJSON?: (uri: string, data: any, options?: RequestInit) => Promise;
+
+ /**
+ * @param uri The URI to which to send the request.
+ * @param options An optional {@link RequestInit} to override the defaults provided by {@link fetchJSON}
+ * @returns
+ */
+ getJSON?: (uri: string, options?: Omit) => Promise;
+
+ /**
+ * Attempts get a JSON reprentation of a comment
+ * @param id id of the comment to get
+ */
+ getComment?: (id: string) => Promise
+
+ /**
+ * Attempts to dynamically load thread content
+ * @param id id of the comment thread
+ * @param options configuration options
+ */
+ getThreadContent?: (id: string, options?: GetThreadContentOptions) => Promise
+
+ /**
+ * Attempts to dynamically load a list of comment threads for a given post
+ * @param id id of the post to load
+ */
+ getThreadsListContent?: (id: string) => Promise
+
+ /**
+ * Processes JSON responses from QPixel API
+ * @param data
+ * @param onSuccess callback to call for successful requests
+ */
+ handleJSONResponse?: (data: T, onSuccess: (data: T) => void) => void
+
+ /**
+ * Attempts to delete a comment
+ * @param id id of the comment to delete
+ * @returns result of the operation
+ */
+ deleteComment?: (id: string) => Promise
+
+ /**
+ * Attempts to undelete a comment
+ * @param id id of the comment to undelete
+ * @returns result of the operation
+ */
+ undeleteComment?: (id: string) => Promise
+
+ /**
+ * Attempts to lock a comment thread
+ * @param id id of the comment thread to lock
+ * @returns result of the operation
+ */
+ lockThread?: (id: string) => Promise;
+
// qpixel_dom
DOM?: QPixelDOM;
Popup?: typeof QPixelPopup;
diff --git a/lib/rubocop/path_in_helpers.rb b/lib/rubocop/path_in_helpers.rb
index c078815b6..452c75e28 100644
--- a/lib/rubocop/path_in_helpers.rb
+++ b/lib/rubocop/path_in_helpers.rb
@@ -5,7 +5,7 @@ class PathInHelpers < RuboCop::Cop::Base
def_node_matcher :method_call, '(send nil? $_ ...)'
MSG = "Don't use _path URL helpers in helper methods; use _url instead.".freeze
- PATH_HELPER = /^[\da-zA-z_]+_path$/.freeze
+ PATH_HELPER = /^[\da-zA-Z_]+_path$/
def on_send(node)
method_call(node) do |name|
diff --git a/package-lock.json b/package-lock.json
index 785d0a1d3..8e19e8c1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,8 +6,8 @@
"": {
"name": "qpixel",
"devDependencies": {
- "@types/jquery": "3.5.32",
- "@types/select2": "4.0.63",
+ "@types/jquery": "^3.5.32",
+ "@types/select2": "^4.0.63",
"typescript": "5.6.3"
}
},
@@ -16,6 +16,7 @@
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz",
"integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/sizzle": "*"
}
@@ -25,6 +26,7 @@
"resolved": "https://registry.npmjs.org/@types/select2/-/select2-4.0.63.tgz",
"integrity": "sha512-/DXUfPSj3iVTGlRYRYPCFKKSogAGP/j+Z0fIMXbBiBtmmZj0WH7vnfNuckafq9C43KnqPPQW2TI/Rj/vTSGnQQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/jquery": "*"
}
diff --git a/package.json b/package.json
index f18337027..c3e3860bf 100644
--- a/package.json
+++ b/package.json
@@ -6,8 +6,8 @@
"typecheck": "tsc"
},
"devDependencies": {
- "@types/jquery": "3.5.32",
- "@types/select2": "4.0.63",
+ "@types/jquery": "^3.5.32",
+ "@types/select2": "^4.0.63",
"typescript": "5.6.3"
}
}
diff --git a/public/404.html b/public/404.html
index b612547fc..2be3af26f 100644
--- a/public/404.html
+++ b/public/404.html
@@ -4,7 +4,7 @@
The page you were looking for doesn't exist (404)
-
+
diff --git a/public/406-unsupported-browser.html b/public/406-unsupported-browser.html
new file mode 100644
index 000000000..7cf1e168e
--- /dev/null
+++ b/public/406-unsupported-browser.html
@@ -0,0 +1,66 @@
+
+
+
+ Your browser is not supported (406)
+
+
+
+
+
+
+
+
+
Your browser is not supported.
+
Please upgrade your browser to continue.
+
+
+
+
diff --git a/public/422.html b/public/422.html
index a21f82b3b..c08eac0d1 100644
--- a/public/422.html
+++ b/public/422.html
@@ -4,7 +4,7 @@
The change you wanted was rejected (422)
-
+
diff --git a/public/500.html b/public/500.html
index 061abc587..78a030af2 100644
--- a/public/500.html
+++ b/public/500.html
@@ -4,7 +4,7 @@
We're sorry, but something went wrong (500)
-
+
`;
@@ -222,19 +273,25 @@
if (answer.code) {
row.querySelector('.username').after(document.createElement('code'));
row.querySelector('code').innerText = answer.code.split('\n')[0].substring(0, 200);
- } else {
+ } else if (answer.code !== '') {
row.querySelector('.username').insertAdjacentHTML('afterend', 'Invalid entry format');
}
return row;
}
- async function renderLeaderboardsByLanguage() {
+ /**
+ * @param {SortComparator} comparator
+ */
+ async function renderLeaderboardsByLanguage(comparator) {
leaderboard = leaderboard || await getLeaderboard(CHALLENGE_ID);
const languageLeaderboards = createGroups(leaderboard, (entry) => entry.full_language);
- for (const language in languageLeaderboards) {
- augmentLeaderboardWithPlacements(languageLeaderboards[language], sort);
+ // sorted using default alphanumeric sort
+ const sortedLanguageKeys = Object.keys(languageLeaderboards).sort()
+
+ for (const language of sortedLanguageKeys) {
+ augmentLeaderboardWithPlacements(languageLeaderboards[language], comparator);
for (const answer of languageLeaderboards[language]) {
const row = createRow(answer);
@@ -243,9 +300,12 @@
}
}
- async function renderLeaderboardsByByteCount() {
+ /**
+ * @param {SortComparator} comparator
+ */
+ async function renderLeaderboardsByByteCount(comparator) {
leaderboard = leaderboard || await getLeaderboard(CHALLENGE_ID);
- augmentLeaderboardWithPlacements(leaderboard, sort);
+ augmentLeaderboardWithPlacements(leaderboard, comparator);
for (const answer of leaderboard) {
const row = createRow(answer);
@@ -253,19 +313,41 @@
}
}
- window.addEventListener('DOMContentLoaded', (_) => {
- if (document.querySelector('.category-header--name').innerText.trim() === 'Challenges') {
- const question_tags = [...document.querySelector('.post--tags').children].map((el) => el.innerText);
-
- if (question_tags.includes('code-golf') || question_tags.includes('lowest-score')) {
- sort = (x, y) => x.score - y.score;
- document.querySelector('.post:first-child').nextElementSibling.insertAdjacentElement('afterend', embed);
- refreshBoard();
- } else if (question_tags.includes('code-bowling') || question_tags.includes('highest-score')) {
- sort = (x, y) => y.score - x.score;
- document.querySelector('.post:first-child').nextElementSibling.insertAdjacentElement('afterend', embed);
- refreshBoard();
- }
+ window.addEventListener("DOMContentLoaded", (_) => {
+ const categoryName = document.querySelector(".category-header--name").innerText.trim();
+
+ if (categoryName !== 'Challenges') {
+ return;
+ }
+
+ const question_tags = [
+ ...document.querySelector(".post--tags").children,
+ ].map((el) => el.innerText);
+
+ if (
+ question_tags.includes("code-golf") ||
+ question_tags.includes("lowest-score")
+ ) {
+ // If x were undefined, it would be automatically sorted to the end, but not so if x.score is undefined, so this needs to be stated explicitly.
+ sort = (x, y) => typeof x.score === "undefined" ? 1 : x.score - y.score;
+
+ document
+ .querySelector(".post:first-child")
+ .nextElementSibling.insertAdjacentElement("afterend", embed);
+
+ refreshBoard(sort);
+ } else if (
+ question_tags.includes("code-bowling") ||
+ question_tags.includes("highest-score")
+ ) {
+ // If x were undefined, it would be automatically sorted to the end, but not so if x.score is undefined, so this needs to be stated explicitly.
+ sort = (x, y) => typeof x.score === "undefined" ? 1 : y.score - x.score;
+
+ document
+ .querySelector(".post:first-child")
+ .nextElementSibling.insertAdjacentElement("afterend", embed);
+
+ refreshBoard(sort);
}
});
})();
diff --git a/public/icon.png b/public/icon.png
new file mode 100644
index 000000000..f3b5abcbd
Binary files /dev/null and b/public/icon.png differ
diff --git a/public/icon.svg b/public/icon.svg
new file mode 100644
index 000000000..78307ccd4
--- /dev/null
+++ b/public/icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/scripts/recalc_abilities.rb b/scripts/recalc_abilities.rb
index 17c99d4fd..cb2566483 100644
--- a/scripts/recalc_abilities.rb
+++ b/scripts/recalc_abilities.rb
@@ -39,7 +39,7 @@
cu.recalc_abilities
# Grant mod ability if mod status is given
- if (cu.is_moderator || cu.is_admin || u.is_global_moderator || u.is_global_admin) && !cu.ability?('mod')
+ if cu.at_least_moderator? && !cu.ability?('mod')
cu.grant_ability!('mod')
end
@@ -55,4 +55,4 @@
unless options.quiet
puts "Completed, #{resolved.size}/#{all.size} tasks successful, #{destroy.size} tasks invalid"
-end
\ No newline at end of file
+end
diff --git a/scripts/recalc_abilities_upon_first_migration.rb b/scripts/recalc_abilities_upon_first_migration.rb
index 32b3c48e4..b7394be55 100644
--- a/scripts/recalc_abilities_upon_first_migration.rb
+++ b/scripts/recalc_abilities_upon_first_migration.rb
@@ -3,14 +3,18 @@
User.unscoped.all.map do |u|
puts "Scope: User.Id=#{u.id}"
- CommunityUser.unscoped.where(user: u).all.map do |cu|
+
+ u.community_users.each do |cu|
puts " Attempt CommunityUser.Id=#{cu.id}"
RequestContext.community = cu.community
- cu.recalc_privileges
+ cu.recalc_privileges!
- if (cu.is_moderator || cu.is_admin || u.is_global_moderator || u.is_global_admin) && !cu.privilege?('mod')
+ if cu.at_least_moderator? && !cu.privilege?('mod')
+ puts " Granting mod privilege to CommunityUser.Id=#{cu.id}"
cu.grant_privilege!('mod')
end
+
+ puts " Recalc success for CommunityUser.Id=#{cu.id}"
rescue
puts " !!! Error recalcing for CommunityUser.Id=#{cu.id}"
end
diff --git a/scripts/run_summary_mailer.rb b/scripts/run_summary_mailer.rb
new file mode 100644
index 000000000..ce82f1544
--- /dev/null
+++ b/scripts/run_summary_mailer.rb
@@ -0,0 +1 @@
+SendSummaryEmailsJob.perform_later
diff --git a/test/.rubocop.yml b/test/.rubocop.yml
index ebc9063e1..259d9869f 100644
--- a/test/.rubocop.yml
+++ b/test/.rubocop.yml
@@ -6,6 +6,14 @@ Layout/LineLength:
Metrics/BlockLength:
CountAsOne: ['array']
+ AllowedMethods:
+ - included
Metrics/ClassLength:
- Enabled: false
\ No newline at end of file
+ Enabled: false
+
+Style/MethodCallWithArgsParentheses:
+ Enabled: true
+ AllowedMethods: ['get', 'post', 'put', 'delete', 'puts', 'patch']
+ AllowedPatterns: ['assert$', 'assert_equal', 'raise', '^click_', '^fill_', 'visit', 'within']
+ IncludedMacros: ['assert_response']
diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb
index da6f04b00..73a444150 100644
--- a/test/application_system_test_case.rb
+++ b/test/application_system_test_case.rb
@@ -56,7 +56,7 @@ def confirm_email(user_or_fixture)
#
# @param user_or_fixture [User, Symbol] either a user or a symbol referring to a fixture
def user(user_or_fixture)
- if user_or_fixture.is_a? User
+ if user_or_fixture.is_a?(User)
user_or_fixture
else
users(user_or_fixture)
@@ -73,29 +73,26 @@ def post_form_select_tag(tag_name, create_new = false)
find('.select2-search__field').fill_in(with: tag_name)
end
- # Get the first item listed that is not the "Searching..." item
- first_option = find('#select2-post_tags_cache-results li:first-child') { |el| el.text != 'Searching…' }
+ # Wait for tag search to finish
+ assert_text('Searching…')
+ assert_no_text('Searching…')
- if first_option.first('span').text == tag_name
- # If the text matches the tag name, first check whether we are creating a new tag.
- # If so, confirm that we are allowed to. If all is good, actually click on the item.
- if create_new || !first_option.text.include?('Create new tag')
- first_option.click
- else
- raise "Expected to find tag with the name #{tag_name}, " \
- 'but could not select it from options without creating a new tag.'
- end
+ # Get the first and last options listed
+ first_option = find('#select2-post_tags_cache-results li:first-child')
+ last_option = find('#select2-post_tags_cache-results li:last-child')
+
+ # If the first option matches the tag name, and the tag already exists, click it.
+ if first_option.first('span').text == tag_name && !first_option.text.include?('Create new tag')
+ first_option.click
+ # If we are allowed to create a tag, select the last option from the list, which is always the tag creation.
elsif create_new
- # The first item returned is not the tag we were looking for (another tag partial match + not existing)
- # If we are allowed to create a tag, select the last option from the list, which is always the tag creation.
- last_option = find('#select2-post_tags_cache-results li:last-child')
if last_option.first('span').text == tag_name
last_option.click
else
raise "Tried to select tag #{tag_name} for creation, but it does not seem to be a presented option."
end
+ # The first option is not the tag we were looking for, and we are not allowed to create a tag.
else
- # The first item returned is not the tag we were looking for, and we are not allowed to create a tag.
raise "Expected to find tag with the name #{tag_name}, " \
'but could not select it from options without creating a new tag.'
end
diff --git a/test/comments_test_helpers.rb b/test/comments_test_helpers.rb
new file mode 100644
index 000000000..797a69f5f
--- /dev/null
+++ b/test/comments_test_helpers.rb
@@ -0,0 +1,124 @@
+module CommentsControllerTestHelpers
+ extend ActiveSupport::Concern
+
+ private
+
+ # Attempts to archive a given comment thread
+ # @param thread [CommentThread] thread to archive
+ def try_archive_thread(thread)
+ post :thread_restrict, params: { id: thread.id, type: 'archive' }
+ end
+
+ # Attempts to create a comment thread on a given post
+ # @param post [Post] post to create the thread on
+ # @param mentions [Array] list of user @-mentions, if any
+ # @param content [String] content of the initial thread comment
+ # @param title [String] title of the thread, if any
+ def try_create_thread(post,
+ mentions: [],
+ content: 'sample comment content',
+ title: 'sample thread title',
+ format: :html)
+ body_parts = [content] + mentions.map { |u| "@##{u.id}" }
+
+ post(:create_thread, params: { post_id: post.id,
+ title: title,
+ body: body_parts.join(' ') },
+ format: format)
+ end
+
+ # Attempts to create a comment in a given thread
+ # @param thread [CommentThread] thread to create the comment in
+ # @param mentions [Array] list of user @-mentions, if any
+ # @param content [String] content of the comment, if any
+ # @param format [Symbol] whether to respond with HTML or JSON
+ # @param inline [Boolean] whether to stay on the post page
+ def try_create_comment(thread,
+ mentions: [],
+ content: 'sample comment content',
+ format: :html,
+ inline: false)
+ content_parts = [content] + mentions.map { |u| "@##{u.id}" }
+
+ post(:create, params: { id: thread.id,
+ post_id: thread.post.id,
+ content: content_parts.join(' '),
+ inline: inline },
+ format: format)
+ end
+
+ # Attempts to delete a given comment thread
+ # @param thread [CommentThread] thread to delete
+ def try_delete_thread(thread)
+ post :thread_restrict, params: { id: thread.id, type: 'delete' }
+ end
+
+ # Attempts to undelete a given comment thread
+ # @param thread [CommentThread] thread to undelete
+ def try_undelete_thread(thread)
+ post :thread_unrestrict, params: { id: thread.id, type: 'delete' }
+ end
+
+ # Attempts to follow a given comment thread
+ # @param thread [CommentThread] thread to follow
+ def try_follow_thread(thread)
+ post :thread_restrict, params: { id: thread.id, type: 'follow' }
+ end
+
+ # Attempts to unfollow a given comment thread
+ # @param thread [CommentThread] thread to unfollow
+ def try_unfollow_thread(thread)
+ post :thread_unrestrict, params: { id: thread.id, type: 'follow' }
+ end
+
+ # Attempts to lock a given comment thread
+ # @param thread [CommentThread] thread to lock
+ # @param duration [Integer] lock duration, in days
+ def try_lock_thread(thread, duration: nil)
+ post :thread_restrict, params: { duration: duration, id: thread.id, type: 'lock' }
+ end
+
+ # Attempts to unlock a given comment thread
+ # @param thread [CommentThread] thread to unlock
+ def try_unlock_thread(thread)
+ post :thread_unrestrict, params: { id: thread.id, type: 'lock' }
+ end
+
+ # Attempts to rename a given comment thread
+ # @param thread [CommentThread] thread to rename
+ # @param title [String] new thread title, if any
+ def try_rename_thread(thread, title: 'new thread title')
+ post :thread_rename, params: { id: thread.id, title: title }
+ end
+
+ # Attempts to show a single comment
+ # @param comment [Comment] comment to show
+ def try_show_comment(comment, format: :html)
+ get :show, params: { id: comment.id, format: format }
+ end
+
+ # Attempts to show a single comment thread
+ # @param thread [CommentThread] comment thread to show
+ def try_show_thread(thread, format: :html)
+ get :thread, params: { id: thread.id, format: format }
+ end
+
+ # Attempts to delete a single comment
+ # @param comment [Comment] comment to delete
+ def try_delete_comment(comment, format: :html)
+ delete :destroy, params: { id: comment.id, format: format }
+ end
+
+ # Attempts to undelete a single comment
+ # @param comment [Comment] comment to undelete
+ def try_undelete_comment(comment, format: :html)
+ patch :undelete, params: { id: comment.id, format: format }
+ end
+
+ # Attempts to update a given comment
+ # @param comment [Comment] comment to update
+ # @param content [String] new content of the comment, if any
+ def try_update_comment(comment, content: 'Edited comment content')
+ post :update, params: { id: comment.id, comment: { content: content } }
+ end
+end
diff --git a/test/controllers/abilities_controller_test.rb b/test/controllers/abilities_controller_test.rb
index 8d993ff80..a9ea0fe94 100644
--- a/test/controllers/abilities_controller_test.rb
+++ b/test/controllers/abilities_controller_test.rb
@@ -6,7 +6,7 @@ class AbilitiesControllerTest < ActionController::TestCase
test 'should get index when logged in' do
sign_in users(:standard_user)
get :index
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:abilities)
assert_not_nil assigns(:user)
end
@@ -14,13 +14,13 @@ class AbilitiesControllerTest < ActionController::TestCase
test 'should get index when not logged in' do
sign_out :user
get :index
- assert_response 200
+ assert_response(:success)
end
test 'should get index for other user when logged in' do
sign_in users(:standard_user)
get :index, params: { for: users(:closer).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:abilities)
assert_not_nil assigns(:user)
end
@@ -28,7 +28,7 @@ class AbilitiesControllerTest < ActionController::TestCase
test 'should get index for other user when not logged in' do
sign_out :user
get :index, params: { for: users(:closer).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:abilities)
assert_not_nil assigns(:user)
end
@@ -36,7 +36,7 @@ class AbilitiesControllerTest < ActionController::TestCase
test 'should get show when logged in' do
sign_in users(:standard_user)
get :show, params: { id: 'unrestricted' }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:ability)
assert_not_nil assigns(:user)
assert_not_nil assigns(:your_ability)
@@ -45,13 +45,13 @@ class AbilitiesControllerTest < ActionController::TestCase
test 'should get show when not logged in' do
sign_out :user
get :show, params: { id: 'unrestricted' }
- assert_response 200
+ assert_response(:success)
end
test 'should get show for other user when logged in' do
sign_in users(:standard_user)
get :show, params: { id: 'unrestricted', for: users(:closer).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:ability)
assert_not_nil assigns(:user)
assert_not_nil assigns(:your_ability)
@@ -60,7 +60,7 @@ class AbilitiesControllerTest < ActionController::TestCase
test 'should get show for other user when not logged in' do
sign_out :user
get :show, params: { id: 'unrestricted', for: users(:closer).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:ability)
assert_not_nil assigns(:user)
assert_not_nil assigns(:your_ability)
diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb
index dd5037ee0..c1f6af3b4 100644
--- a/test/controllers/admin_controller_test.rb
+++ b/test/controllers/admin_controller_test.rb
@@ -3,19 +3,19 @@
class AdminControllerTest < ActionController::TestCase
include Devise::Test::ControllerHelpers
- PARAM_LESS_ACTIONS = [:index, :error_reports, :privileges, :audit_log].freeze
+ PARAM_LESS_ACTIONS = [:index, :error_reports, :privileges, :audit_log, :email_query, :admin_email, :all_email].freeze
test 'should get index' do
sign_in users(:admin)
get :index
- assert_response :success
+ assert_response(:success)
end
test 'should deny anonymous users access' do
sign_out :user
PARAM_LESS_ACTIONS.each do |path|
get path
- assert_response(404)
+ assert_response(:not_found)
end
end
@@ -23,7 +23,7 @@ class AdminControllerTest < ActionController::TestCase
sign_in users(:standard_user)
PARAM_LESS_ACTIONS.each do |path|
get path
- assert_response(404)
+ assert_response(:not_found)
end
end
@@ -31,7 +31,7 @@ class AdminControllerTest < ActionController::TestCase
sign_in users(:editor)
PARAM_LESS_ACTIONS.each do |path|
get path
- assert_response(404)
+ assert_response(:not_found)
end
end
@@ -39,7 +39,7 @@ class AdminControllerTest < ActionController::TestCase
sign_in users(:deleter)
PARAM_LESS_ACTIONS.each do |path|
get path
- assert_response(404)
+ assert_response(:not_found)
end
end
@@ -47,7 +47,7 @@ class AdminControllerTest < ActionController::TestCase
sign_in users(:moderator)
PARAM_LESS_ACTIONS.each do |path|
get path
- assert_response(404)
+ assert_response(:not_found)
end
end
@@ -60,7 +60,7 @@ class AdminControllerTest < ActionController::TestCase
sign_in users(:admin)
PARAM_LESS_ACTIONS.each do |path|
get path
- assert_response(404)
+ assert_response(:not_found)
end
end
@@ -73,50 +73,88 @@ class AdminControllerTest < ActionController::TestCase
sign_in users(:global_admin)
PARAM_LESS_ACTIONS.each do |path|
get path
- assert_response(200)
+ assert_response(:success)
end
end
test 'should get single privilege' do
sign_in users(:admin)
get :show_privilege, params: { name: 'unrestricted', format: :json }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:ability)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
end
test 'should update privilege threshold' do
sign_in users(:admin)
post :update_privilege, params: { name: 'unrestricted', threshold: 0.6, type: 'post' }
- assert_response 202
+
+ assert_response(:accepted)
assert_not_nil assigns(:ability)
assert_equal 0.6, assigns(:ability).post_score_threshold
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'OK', JSON.parse(response.body)['status']
end
test 'should access error reports' do
sign_in users(:admin)
get :error_reports
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:reports)
end
test 'should search error reports' do
sign_in users(:admin)
get :error_reports, params: { uuid: error_logs(:without_context).uuid }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:reports)
end
test 'should get audit log' do
sign_in users(:admin)
get :audit_log
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:logs)
end
+
+ test 'should do email query' do
+ sign_in users(:admin)
+ post :do_email_query, params: { email: users(:standard_user).email }
+ assert_response(:success)
+ assert_not_nil assigns(:user)
+ assert_not_nil assigns(:profiles)
+ end
+
+ test 'do_email_query should add a notice if the email does not exist' do
+ sign_in users(:admin)
+ post :do_email_query, params: { email: 'spock@vulcan.ufp' }
+ assert_response(:success)
+ assert_equal flash[:danger], I18n.t('admin.errors.email_query_not_found')
+ end
+
+ test 'send email methods should require auth' do
+ [:send_admin_email, :send_all_email].each do |action|
+ post action, params: { subject: 'test', body_markdown: 'test' }
+ assert_response(:not_found)
+ end
+ end
+
+ test 'send email methods should require global admin' do
+ [:send_admin_email, :send_all_email].each do |action|
+ sign_in users(:admin)
+ post action, params: { subject: 'test', body_markdown: 'test' }
+ assert_response(:not_found)
+ end
+ end
+
+ test 'send email methods should work for global admins' do
+ [:send_admin_email, :send_all_email].each do |action|
+ sign_in users(:global_admin)
+ post action, params: { subject: 'test', body_markdown: 'test' }
+ assert_response(:found)
+ assert_redirected_to admin_path
+ assert_not_nil flash[:success]
+ end
+ end
end
diff --git a/test/controllers/advertisement_controller_test.rb b/test/controllers/advertisement_controller_test.rb
index eed236ff7..d9180e37a 100644
--- a/test/controllers/advertisement_controller_test.rb
+++ b/test/controllers/advertisement_controller_test.rb
@@ -5,23 +5,41 @@ class AdvertisementControllerTest < ActionController::TestCase
test 'index should return html' do
get :index
- assert_response(200)
+ assert_response(:success)
assert_equal 'text/html', response.media_type
end
test 'image paths should return png' do
- # Cannot test :random_question, as it requires initialization
- # of hot posts, which isn't provided via get helper.
- [:codidact, :community].each do |path|
+ [:codidact, :community, :random_question, :promoted_post].each do |path|
get path
- assert_response(200)
+ assert_response(:success)
assert_equal 'image/png', response.media_type
end
end
- test 'post image path should return png' do
- get :specific_question, params: { id: posts(:question_one).id }
- assert_response(200)
+ test 'specific_question for different post types' do
+ { 123_456_789 => :not_found,
+ posts(:question_one).id => :success,
+ posts(:article_one).id => :success,
+ posts(:answer_one).id => :not_found }.each do |id, status|
+ try_specific_question(id)
+ assert_response(status)
+ if status == :success
+ assert_equal 'image/png', response.media_type
+ end
+ end
+ end
+
+ test 'specific_category' do
+ # :specific_category uses random post selection, so we can't easily test for different post types
+ get :specific_category, params: { id: categories(:main).id, score: -1, days: 3650 }
+ assert_response(:success)
assert_equal 'image/png', response.media_type
end
+
+ private
+
+ def try_specific_question(id)
+ get :specific_question, params: { id: id }
+ end
end
diff --git a/test/controllers/answers_controller_test.rb b/test/controllers/answers_controller_test.rb
index b5a1ade00..5388aa150 100644
--- a/test/controllers/answers_controller_test.rb
+++ b/test/controllers/answers_controller_test.rb
@@ -5,14 +5,14 @@ class AnswersControllerTest < ActionController::TestCase
test 'should convert to comment' do
sign_in users(:moderator)
+
pre_count = posts(:question_one).comments.count
post :convert_to_comment, params: { id: posts(:answer_one).id, post_id: posts(:question_one).id }
post_count = posts(:question_one).comments.count
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:answer)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal pre_count + 1, post_count
assert_equal true, assigns(:answer).deleted
end
@@ -20,6 +20,7 @@ class AnswersControllerTest < ActionController::TestCase
test 'should 404 convert comment for non-moderator' do
sign_in users(:editor)
post :convert_to_comment, params: { id: posts(:answer_one).id, post_id: posts(:question_one).id }
- assert_response 404
+
+ assert_response(:not_found)
end
end
diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb
index 3f2071974..82bdbfdc5 100644
--- a/test/controllers/categories_controller_test.rb
+++ b/test/controllers/categories_controller_test.rb
@@ -5,78 +5,111 @@ class CategoriesControllerTest < ActionController::TestCase
test 'should get index' do
get :index
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:categories)
end
- test 'should get show' do
- get :show, params: { id: categories(:main).id }
- assert_response 200
- assert_not_nil assigns(:category)
- assert_not_nil assigns(:posts)
+ test 'should correctly show public categories' do
+ public_categories = categories.select(&:public?)
+
+ assert_not public_categories.empty?
+
+ public_categories.each do |category|
+ try_show_category(category)
+
+ assert_response(:success)
+ assert_not_nil assigns(:category)
+ assert_not_nil assigns(:posts)
+ end
end
- test 'fake community should not get show' do
+ test 'fake community should never be shown' do
RequestContext.community = communities(:fake)
request.env['HTTP_HOST'] = 'fake.qpixel.com'
- get :show, params: { id: categories(:main).id }
- assert_response(404)
- end
+ try_show_category(categories(:main))
- test 'should require authentication to get new' do
- get :new
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_response(:not_found)
end
- test 'should require admin to get new' do
- sign_in users(:standard_user)
- get :new
- assert_response 404
+ test 'categories should only be shown to those who can see them' do
+ users.each do |user|
+ sign_in user
+
+ categories.each do |category|
+ try_show_category(category)
+
+ if category.public? || user.can_see_category?(category)
+ assert_response(:success)
+ else
+ assert_response(:not_found)
+ end
+
+ assert_not_nil assigns(:category)
+ end
+ end
end
- test 'should allow admins to get new' do
- sign_in users(:admin)
- get :new
- assert_response 200
- assert_not_nil assigns(:category)
+ test ':new should require the user to be an admin' do
+ users.each do |user|
+ sign_in user
+
+ get :new
+
+ if user.admin?
+ assert_response(:success)
+ assert_not_nil assigns(:category)
+ elsif @controller.helpers.user_signed_in?
+ assert_response(:not_found)
+ else
+ assert_redirected_to_sign_in
+ end
+ end
end
test 'should require authentication to create category' do
- post :create, params: { category: { name: 'test', short_wiki: 'test', display_post_types: [Question.post_type_id],
- post_type_ids: [Question.post_type_id, Answer.post_type_id],
- tag_set: tag_sets(:main).id, color_code: 'blue',
- license_id: licenses(:cc_by_sa).id } }
- assert_response 302
- assert_redirected_to new_user_session_path
+ try_create_category
+ assert_redirected_to_sign_in
end
test 'should require admin to create category' do
sign_in users(:standard_user)
- post :create, params: { category: { name: 'test', short_wiki: 'test', display_post_types: [Question.post_type_id],
- post_type_ids: [Question.post_type_id, Answer.post_type_id],
- tag_set: tag_sets(:main).id, color_code: 'blue',
- license_id: licenses(:cc_by_sa).id } }
- assert_response 404
+ try_create_category
+ assert_response(:not_found)
end
test 'should allow admins to create category' do
sign_in users(:admin)
- post :create, params: { category: { name: 'test', short_wiki: 'test', display_post_types: [Question.post_type_id],
- post_type_ids: [Question.post_type_id, Answer.post_type_id],
- tag_set_id: tag_sets(:main).id, color_code: 'blue',
- license_id: licenses(:cc_by_sa).id } }
- assert_response 302
+ try_create_category
+
+ assert_response(:found)
assert_not_nil assigns(:category)
assert_not_nil assigns(:category).id
assert_equal false, assigns(:category).errors.any?
assert_redirected_to category_path(assigns(:category))
end
- test 'should prevent users under min_view_trust_level viewing category that requires higher' do
- get :show, params: { id: categories(:admin_only).id }
- assert_response 404
- assert_not_nil assigns(:category)
+ private
+
+ def try_create_category(**opts)
+ name = opts[:name] || 'test'
+ short_wiki = opts[:short_wiki] || 'test'
+ license = opts[:license] || licenses(:cc_by_sa)
+ color_code = opts[:color_code] || 'blue'
+ display_post_types = opts[:display_post_types] || [Question.post_type_id]
+ post_types = opts[:post_types] || [Question, Answer]
+ tag_set = opts[:tag_set] || tag_sets(:main)
+
+ post :create, params: { category: { name: name,
+ short_wiki: short_wiki,
+ display_post_types: display_post_types,
+ post_type_ids: post_types.map(&:post_type_id),
+ tag_set_id: tag_set.id,
+ color_code: color_code,
+ license_id: license.id } }
+ end
+
+ def try_show_category(category)
+ get :show, params: { id: category.id }
end
end
diff --git a/test/controllers/close_reasons_controller_test.rb b/test/controllers/close_reasons_controller_test.rb
index 41e6a0309..b01f12ced 100644
--- a/test/controllers/close_reasons_controller_test.rb
+++ b/test/controllers/close_reasons_controller_test.rb
@@ -8,7 +8,7 @@ class CloseReasonsControllerTest < ActionController::TestCase
test 'should get index' do
sign_in users(:admin)
get :index
- assert_response :success
+ assert_response(:success)
assert_not_nil assigns(:close_reasons)
end
@@ -16,7 +16,7 @@ class CloseReasonsControllerTest < ActionController::TestCase
sign_out :user
PARAM_LESS_ACTIONS.each do |path|
get path
- assert_response(404)
+ assert_response(:not_found)
end
end
@@ -24,14 +24,14 @@ class CloseReasonsControllerTest < ActionController::TestCase
sign_in users(:standard_user)
PARAM_LESS_ACTIONS.each do |path|
get path
- assert_response(404)
+ assert_response(:not_found)
end
end
test 'should get new' do
sign_in users(:global_admin)
get :new
- assert_response :success
+ assert_response(:success)
assert_not_nil assigns(:close_reason)
end
@@ -39,7 +39,7 @@ class CloseReasonsControllerTest < ActionController::TestCase
sign_in users(:global_admin)
post :create, params: { close_reason: { name: 'test', description: 'test', requires_other_post: true,
active: true } }
- assert_response 302
+ assert_response(:found)
assert_redirected_to close_reasons_path
assert_not_nil assigns(:close_reason)
assert_not_nil assigns(:close_reason).id
@@ -48,16 +48,22 @@ class CloseReasonsControllerTest < ActionController::TestCase
test 'should get edit' do
sign_in users(:global_admin)
get :edit, params: { id: close_reasons(:duplicate).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:close_reason)
end
+ test 'edit should fail for non-global admin on global reason' do
+ sign_in users(:admin)
+ get :edit, params: { id: close_reasons(:global).id }
+ assert_response(:not_found)
+ end
+
test 'should update close reason' do
sign_in users(:global_admin)
patch :update, params: { id: close_reasons(:duplicate).id, close_reason: { name: 'test', description: 'test',
requires_other_post: true,
active: false } }
- assert_response 302
+ assert_response(:found)
assert_redirected_to close_reasons_path
assert_not_nil assigns(:close_reason)
assert_equal false, assigns(:close_reason).active
diff --git a/test/controllers/comments/archive_test.rb b/test/controllers/comments/archive_test.rb
new file mode 100644
index 000000000..796ae0e27
--- /dev/null
+++ b/test/controllers/comments/archive_test.rb
@@ -0,0 +1,24 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should archive thread' do
+ sign_in users(:deleter)
+ try_archive_thread(comment_threads(:normal))
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'should require privilege to archive thread' do
+ sign_in users(:standard_user)
+ try_archive_thread(comment_threads(:normal))
+
+ assert_response(:not_found)
+ assert_not_nil assigns(:comment_thread)
+ end
+end
diff --git a/test/controllers/comments/create_test.rb b/test/controllers/comments/create_test.rb
new file mode 100644
index 000000000..df763a1cb
--- /dev/null
+++ b/test/controllers/comments/create_test.rb
@@ -0,0 +1,200 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should correctly create threads' do
+ sign_in users(:editor)
+
+ before_author_notifs = users(:standard_user).notifications.count
+ before_uninvolved_notifs = users(:moderator).notifications.count
+
+ try_create_thread(posts(:question_one), mentions: [users(:deleter), users(:moderator)])
+
+ assert_response(:found)
+ assert_redirected_to post_path(assigns(:post))
+ assert_not_nil assigns(:post)
+ assert_not_nil assigns(:comment)&.id
+ assert_not_nil assigns(:comment_thread)&.id
+ assert_nil flash[:danger]
+ assert_equal before_author_notifs + 1, users(:standard_user).notifications.count,
+ 'Author notification not created when it should have been'
+ assert_equal before_uninvolved_notifs, users(:moderator).notifications.count,
+ 'Uninvolved notification created when it should not have been'
+ assert assigns(:comment_thread).followed_by?(users(:editor)), 'Follower record not created for thread author'
+ end
+
+ test 'should correctly default thread title if not provided' do
+ sign_in users(:editor)
+
+ {
+ 'a' * 99 => ->(thread) { thread.comments.first.content },
+ 'a' * 200 => ->(thread) { "#{thread.comments.first.content[0..100]}..." }
+ }.each do |content, matcher|
+ try_create_thread(posts(:question_one), content: content, mentions: [], title: '')
+
+ assert_response(:found)
+ thread = assigns(:comment_thread)
+ assert_equal thread.title, matcher.call(thread)
+ end
+ end
+
+ test 'should require auth to create thread' do
+ try_create_thread(posts(:question_one))
+ assert_redirected_to_sign_in
+ end
+
+ test 'should not create threads on posts of others without the unrestricted ability when rate-limited' do
+ sign_in users(:basic_user)
+
+ SiteSetting['RL_NewUserComments'] = 0
+
+ post = posts(:question_one)
+
+ try_create_thread(post)
+
+ assert_not_nil flash[:danger]
+ assert_redirected_to @controller.helpers.generic_share_link(post)
+ end
+
+ test 'should not create thread if the target post is inaccessible' do
+ sign_in users(:editor)
+ try_create_thread(posts(:high_trust))
+ assert_response(:not_found)
+ end
+
+ test 'should not create thread if the target post does not allow comments for known reasons' do
+ sign_in users(:editor)
+
+ [:comments_disabled, :deleted, :locked].each do |name|
+ post = posts(name)
+
+ assert !post.comments_allowed?
+
+ try_create_thread(post, format: :json)
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+ assert_json_response_message(@controller.helpers.comments_post_error_msg(post))
+ end
+ end
+
+ test 'should return a catch-all response if the target post does not allow comments for an unknown reason' do
+ sign_in users(:editor)
+
+ post = posts(:question_one)
+
+ post.stub(:comments_allowed?, false) do
+ Post.stub(:find, post) do
+ try_create_thread(post, format: :json)
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+ assert_json_response_message(@controller.helpers.comments_post_error_msg(post))
+ end
+ end
+ end
+
+ test 'should correclty create comments in threads' do
+ sign_in users(:editor)
+ before_author_notifs = users(:standard_user).notifications.count
+ before_follow_notifs = users(:deleter).notifications.count
+ before_uninvolved_notifs = users(:moderator).notifications.count
+
+ try_create_comment(comment_threads(:normal), mentions: [users(:deleter), users(:moderator)])
+
+ assert_response(:found)
+ assert_redirected_to comment_thread_path(assigns(:comment_thread))
+ assert_not_nil assigns(:post)
+ assert_not_nil assigns(:comment_thread)
+ assert_not_nil assigns(:comment)&.id
+ assert_equal before_author_notifs + 1, users(:standard_user).notifications.count,
+ 'Post author notification not created when it should have been'
+ assert_equal before_follow_notifs + 1, users(:deleter).notifications.count,
+ 'Thread follower notification not created when it should have been'
+ assert_equal before_uninvolved_notifs, users(:moderator).notifications.count,
+ 'Uninvolved notification created when it should not have been'
+ assert assigns(:comment_thread).followed_by?(users(:editor)), 'Follower record not created for comment author'
+ end
+
+ test 'should correctly redirect depending on the inline parameter' do
+ thread = comment_threads(:normal)
+ editor = users(:editor)
+
+ sign_in editor
+
+ [false, true].each do |inline|
+ try_create_comment(thread, inline: inline)
+
+ assert_response(:found)
+
+ if inline
+ comment = Comment.by(editor).where(comment_thread: thread).last
+
+ assert_redirected_to @controller.helpers.generic_share_link(thread.post,
+ comment_id: comment.id,
+ thread_id: thread.id)
+ else
+ assert_redirected_to comment_thread_path(thread)
+ end
+ end
+ end
+
+ test 'should require auth to create comments' do
+ try_create_comment(comment_threads(:normal))
+ assert_redirected_to_sign_in
+ end
+
+ test 'should not create comments on threads on posts of others without the unrestricted ability when rate-limited' do
+ sign_in users(:basic_user)
+
+ SiteSetting['RL_NewUserComments'] = 0
+
+ thread = comment_threads(:normal)
+
+ try_create_comment(thread)
+
+ assert_not_nil flash[:danger]
+ assert_redirected_to @controller.helpers.generic_share_link(thread.post)
+ end
+
+ test 'should not create comment if the target post is inaccessible' do
+ sign_in users(:editor)
+ try_create_comment(comment_threads(:high_trust))
+ assert_response(:not_found)
+ end
+
+ test 'should not create comment if the target thread is readonly for a known reason' do
+ sign_in users(:editor)
+
+ [:locked, :deleted, :archived].each do |name|
+ thread = comment_threads(name)
+
+ assert thread.read_only?
+
+ try_create_comment(thread, format: :json)
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+ assert_json_response_message(@controller.helpers.comments_thread_error_msg(thread))
+ end
+ end
+
+ test 'should return a catch-all response if the target thread is readonly for an unknown reason' do
+ sign_in users(:editor)
+
+ thread = comment_threads(:normal)
+
+ thread.stub(:read_only?, true) do
+ CommentThread.stub(:find, thread) do
+ try_create_comment(thread, format: :json)
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+ assert_json_response_message(@controller.helpers.comments_thread_error_msg(thread))
+ end
+ end
+ end
+end
diff --git a/test/controllers/comments/delete_test.rb b/test/controllers/comments/delete_test.rb
new file mode 100644
index 000000000..2a04d0ce0
--- /dev/null
+++ b/test/controllers/comments/delete_test.rb
@@ -0,0 +1,64 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should delete comment' do
+ sign_in users(:standard_user)
+
+ try_delete_comment(comments(:one), format: :json)
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'should require auth to delete comment' do
+ [:html, :json].each do |format|
+ try_delete_comment(comments(:one), format: format)
+
+ if format == :html
+ assert_redirected_to_sign_in
+ else
+ assert_response(:unauthorized)
+ assert_valid_json_response
+ end
+ end
+ end
+
+ test 'should allow moderators to delete comments' do
+ sign_in users(:moderator)
+
+ try_delete_comment(comments(:one), format: :json)
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'should not allow other users to delete comment' do
+ sign_in users(:editor)
+ try_delete_comment(comments(:one))
+
+ assert_response(:forbidden)
+ end
+
+ test 'should correctly delete threads' do
+ sign_in users(:deleter)
+ try_delete_thread(comment_threads(:normal))
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'should require privilege to delete thread' do
+ sign_in users(:standard_user)
+ try_delete_thread(comment_threads(:normal))
+
+ assert_response(:not_found)
+ assert_not_nil assigns(:comment_thread)
+ end
+end
diff --git a/test/controllers/comments/follow_test.rb b/test/controllers/comments/follow_test.rb
new file mode 100644
index 000000000..38735538e
--- /dev/null
+++ b/test/controllers/comments/follow_test.rb
@@ -0,0 +1,23 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'any non-deleted user with a profile should be able to follow threads' do
+ thread = comment_threads(:normal)
+
+ users.each do |user|
+ next unless user.profile_on?(RequestContext.community)
+ next if user.deleted? || user.community_user.deleted?
+
+ sign_in user
+ try_follow_thread(thread)
+
+ assert_response(:success, user.community_user.inspect)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+ end
+end
diff --git a/test/controllers/comments/lock_test.rb b/test/controllers/comments/lock_test.rb
new file mode 100644
index 000000000..b98d9b45a
--- /dev/null
+++ b/test/controllers/comments/lock_test.rb
@@ -0,0 +1,41 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should lock thread indefinitely by default' do
+ thread = comment_threads(:normal)
+
+ sign_in users(:deleter)
+
+ try_lock_thread(thread)
+ thread.reload
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ assert thread.locked?, "Expected thread #{thread.title} to be locked"
+ end
+
+ test 'should lock thread for a specific duration if provided' do
+ sign_in users(:deleter)
+ try_lock_thread(comment_threads(:normal), duration: 2)
+
+ @thread = assigns(:comment_thread)
+
+ assert_not_nil @thread
+ assert @thread.lock_active?
+ travel_to 3.days.from_now
+ assert_not @thread.lock_active?
+ end
+
+ test 'should require privilege to lock thread' do
+ sign_in users(:standard_user)
+ try_lock_thread(comment_threads(:normal))
+
+ assert_response(:not_found)
+ assert_not_nil assigns(:comment_thread)
+ end
+end
diff --git a/test/controllers/comments/pingable_test.rb b/test/controllers/comments/pingable_test.rb
new file mode 100644
index 000000000..8b6318046
--- /dev/null
+++ b/test/controllers/comments/pingable_test.rb
@@ -0,0 +1,23 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should get pingable users on thread' do
+ sign_in users(:standard_user)
+
+ try_pingable(posts(:question_one))
+
+ assert_response(:success)
+ assert_valid_json_response
+ end
+
+ private
+
+ # @param post [Post]
+ def try_pingable(post)
+ get :pingable, params: { id: -1, post: post.id }
+ end
+end
diff --git a/test/controllers/comments/post_test.rb b/test/controllers/comments/post_test.rb
new file mode 100644
index 000000000..1f6424de5
--- /dev/null
+++ b/test/controllers/comments/post_test.rb
@@ -0,0 +1,46 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'non-moderator users without flag_curate ability should not see deleted threads' do
+ sign_in users(:editor)
+ get :post, params: { post_id: posts(:question_one).id }, format: :json
+
+ assert_response(:success)
+ assert_valid_json_response
+ threads = JSON.parse(response.body)
+ assert_equal threads.any? { |t| t['deleted'] }, false
+ end
+
+ test 'moderators and users with flag_curate ability should see deleted threads' do
+ sign_in users(:deleter)
+ get :post, params: { post_id: posts(:question_one).id }, format: :json
+ threads = JSON.parse(response.body)
+ assert_equal threads.any? { |t| t['deleted'] }, true
+
+ sign_in users(:moderator)
+ get :post, params: { post_id: posts(:question_one).id }, format: :json
+ threads = JSON.parse(response.body)
+ assert_equal threads.any? { |t| t['deleted'] }, true
+ end
+
+ test 'users should see deleted threads on their own posts even if those threads are deleted' do
+ sign_in users(:standard_user)
+ get :post, params: { post_id: posts(:question_one).id }, format: :json
+
+ assert_response(:success)
+ assert_valid_json_response
+ threads = JSON.parse(response.body)
+ assert_equal threads.any? { |t| t['deleted'] }, true
+ end
+
+ test 'should get comment threads on post' do
+ get :post, params: { post_id: posts(:question_one).id }
+ assert_response(:success)
+ assert_not_nil assigns(:post)
+ assert_not_nil assigns(:comment_threads)
+ end
+end
diff --git a/test/controllers/comments/rename_test.rb b/test/controllers/comments/rename_test.rb
new file mode 100644
index 000000000..c534929f2
--- /dev/null
+++ b/test/controllers/comments/rename_test.rb
@@ -0,0 +1,35 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should correctly rename thread' do
+ sign_in users(:deleter)
+
+ try_rename_thread(comment_threads(:normal))
+
+ assert_response(:found)
+ assert_not_nil assigns(:comment_thread)
+ assert_redirected_to comment_thread_path(assigns(:comment_thread))
+ assert_equal 'new thread title', assigns(:comment_thread).title
+ end
+
+ test 'non-moderators should not be able to rename restricted threads' do
+ sign_in users(:deleter)
+
+ [:locked, :archived, :deleted].each do |name|
+ before_title = comment_threads(name).title
+
+ try_rename_thread(comment_threads(name))
+
+ @thread = assigns(:comment_thread)
+
+ assert_response(:found)
+ assert_not_nil @thread
+ assert_redirected_to comment_thread_path(@thread)
+ assert_equal before_title, @thread.title
+ end
+ end
+end
diff --git a/test/controllers/comments/show_test.rb b/test/controllers/comments/show_test.rb
new file mode 100644
index 000000000..92e81359b
--- /dev/null
+++ b/test/controllers/comments/show_test.rb
@@ -0,0 +1,34 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should correctly get one comment' do
+ [:html, :json].each do |format|
+ try_show_comment(comments(:one), format: format)
+
+ assert_response(:success)
+ assert_not_nil assigns(:comment)
+
+ if format == :json
+ assert_valid_json_response
+ end
+ end
+ end
+
+ test 'should correctly get one thread' do
+ [:html, :json].each do |format|
+ try_show_thread(comment_threads(:normal), format: format)
+
+ assert_response(:success)
+ assert_not_nil assigns(:comment_thread)
+ assert_not_nil assigns(:post)
+
+ if format == :json
+ assert_valid_json_response
+ end
+ end
+ end
+end
diff --git a/test/controllers/comments/thread_followers_test.rb b/test/controllers/comments/thread_followers_test.rb
new file mode 100644
index 000000000..730ca3377
--- /dev/null
+++ b/test/controllers/comments/thread_followers_test.rb
@@ -0,0 +1,27 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should get thread followers' do
+ sign_in users(:admin)
+ get :thread_followers, params: { id: comment_threads(:normal).id }
+ assert_response(:success)
+ assert_not_nil assigns(:comment_thread)
+ assert_not_nil assigns(:followers)
+ end
+
+ test 'should require auth to get thread followers' do
+ get :thread_followers, params: { id: comment_threads(:normal).id }
+ assert_redirected_to_sign_in
+ end
+
+ test 'should require moderator to get thread followers' do
+ sign_in users(:standard_user)
+ get :thread_followers, params: { id: comment_threads(:normal).id }
+ assert_response(:not_found)
+ assert_not_nil assigns(:comment_thread)
+ end
+end
diff --git a/test/controllers/comments/thread_test.rb b/test/controllers/comments/thread_test.rb
new file mode 100644
index 000000000..947c77d10
--- /dev/null
+++ b/test/controllers/comments/thread_test.rb
@@ -0,0 +1,33 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should get thread' do
+ get :thread, params: { id: comment_threads(:normal).id }
+ assert_response(:success)
+ assert_not_nil assigns(:comment_thread)
+ end
+
+ test 'should require auth to access high trust thread' do
+ get :thread, params: { id: comment_threads(:high_trust).id }
+ assert_response(:not_found)
+ assert_not_nil assigns(:comment_thread)
+ end
+
+ test 'should require privileges to access high trust thread' do
+ sign_in users(:deleter)
+ get :thread, params: { id: comment_threads(:high_trust).id }
+ assert_response(:not_found)
+ assert_not_nil assigns(:comment_thread)
+ end
+
+ test 'should access thread on own deleted post' do
+ sign_in users(:closer)
+ get :thread, params: { id: comment_threads(:on_deleted_post).id }
+ assert_response(:success)
+ assert_not_nil assigns(:comment_thread)
+ end
+end
diff --git a/test/controllers/comments/unarchive_test.rb b/test/controllers/comments/unarchive_test.rb
new file mode 100644
index 000000000..12e82ae67
--- /dev/null
+++ b/test/controllers/comments/unarchive_test.rb
@@ -0,0 +1,31 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should unarchive thread' do
+ sign_in users(:deleter)
+ try_unarchive_thread(comment_threads(:archived))
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'should require privilege to unarchive thread' do
+ sign_in users(:standard_user)
+ try_unarchive_thread(comment_threads(:archived))
+
+ assert_response(:not_found)
+ assert_not_nil assigns(:comment_thread)
+ end
+
+ private
+
+ # @param thread [CommentThread]
+ def try_unarchive_thread(thread)
+ post :thread_unrestrict, params: { id: thread.id, type: 'archive' }
+ end
+end
diff --git a/test/controllers/comments/undelete_test.rb b/test/controllers/comments/undelete_test.rb
new file mode 100644
index 000000000..025cef807
--- /dev/null
+++ b/test/controllers/comments/undelete_test.rb
@@ -0,0 +1,97 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should correctly undelete comments' do
+ sign_in users(:standard_user)
+ try_undelete_comment(comments(:deleted), format: :json)
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'should require auth to undelete comments' do
+ try_undelete_comment(comments(:deleted))
+ assert_redirected_to_sign_in
+ end
+
+ test 'should allow moderators to undelete comments' do
+ sign_in users(:moderator)
+ try_undelete_comment(comments(:deleted), format: :json)
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'should not allow non-moderator users to undelete comments' do
+ sign_in users(:editor)
+ try_undelete_comment(comments(:deleted))
+ assert_response(:forbidden)
+ end
+
+ test 'comment undeletion should correctly handle validation' do
+ sign_in users(:moderator)
+
+ comment = comments(:deleted)
+
+ # this is a bit cursed, but IIRC the easiest way to test this
+ comment.stub(:update, false) do
+ Comment.stub(:unscoped, Comment) do
+ Comment.stub(:find, comment) do
+ try_undelete_comment(comment, format: :json)
+
+ assert_response(:internal_server_error)
+ end
+ end
+ end
+ end
+
+ test 'only mods or admins should be able to undelete threads deleted by one of them' do
+ thread = comment_threads(:normal)
+
+ sign_in users(:moderator)
+ try_delete_thread(thread)
+
+ assert_response(:success)
+
+ sign_in users(:deleter)
+ try_undelete_thread(thread)
+
+ assert_response(:success)
+ assert_valid_json_response
+ response_body = JSON.parse(response.body)
+ assert_equal('error', response_body['status'])
+ assert_equal I18n.t('comments.errors.mod_only_undelete'), response_body['message']
+
+ sign_in users(:moderator)
+ try_undelete_thread(thread)
+
+ assert_response(:success)
+ assert_valid_json_response
+ response_body = JSON.parse(response.body)
+ assert_equal('success', response_body['status'])
+ assert_nil response_body['message']
+ end
+
+ test 'should correctly undelete threads' do
+ sign_in users(:moderator)
+ try_undelete_thread(comment_threads(:deleted))
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'should require privilege to undelete thread' do
+ sign_in users(:standard_user)
+ try_undelete_thread(comment_threads(:deleted))
+
+ assert_response(:not_found)
+ assert_not_nil assigns(:comment_thread)
+ end
+end
diff --git a/test/controllers/comments/unfollow_test.rb b/test/controllers/comments/unfollow_test.rb
new file mode 100644
index 000000000..d770bac3d
--- /dev/null
+++ b/test/controllers/comments/unfollow_test.rb
@@ -0,0 +1,16 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should correctly unfollow threads' do
+ sign_in users(:standard_user)
+ try_unfollow_thread(comment_threads(:normal))
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+end
diff --git a/test/controllers/comments/unlock_test.rb b/test/controllers/comments/unlock_test.rb
new file mode 100644
index 000000000..14269b243
--- /dev/null
+++ b/test/controllers/comments/unlock_test.rb
@@ -0,0 +1,24 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should correctly unlock threads' do
+ sign_in users(:deleter)
+ try_unlock_thread(comment_threads(:locked))
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'should require privilege to unlock thread' do
+ sign_in users(:standard_user)
+ try_unlock_thread(comment_threads(:locked))
+
+ assert_response(:not_found)
+ assert_not_nil assigns(:comment_thread)
+ end
+end
diff --git a/test/controllers/comments/update_test.rb b/test/controllers/comments/update_test.rb
new file mode 100644
index 000000000..cf4af790e
--- /dev/null
+++ b/test/controllers/comments/update_test.rb
@@ -0,0 +1,38 @@
+require 'test_helper'
+require 'comments_test_helpers'
+
+class CommentsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include CommentsControllerTestHelpers
+
+ test 'should correctly update comments' do
+ sign_in users(:standard_user)
+
+ try_update_comment(comments(:one))
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'should require auth to update comments' do
+ try_update_comment(comments(:one))
+ assert_redirected_to_sign_in
+ end
+
+ test 'should allow moderators to update comments' do
+ sign_in users(:moderator)
+
+ try_update_comment(comments(:one))
+
+ assert_response(:success)
+ assert_valid_json_response
+ assert_equal 'success', JSON.parse(response.body)['status']
+ end
+
+ test 'non-moderator users should not be able to update comments' do
+ sign_in users(:editor)
+ try_update_comment(comments(:one))
+ assert_response(:forbidden)
+ end
+end
diff --git a/test/controllers/comments_controller_test.rb b/test/controllers/comments_controller_test.rb
deleted file mode 100644
index bb53a9c6e..000000000
--- a/test/controllers/comments_controller_test.rb
+++ /dev/null
@@ -1,394 +0,0 @@
-require 'test_helper'
-
-class CommentsControllerTest < ActionController::TestCase
- include Devise::Test::ControllerHelpers
-
- test 'should create new thread' do
- sign_in users(:editor)
- before_author_notifs = users(:standard_user).notifications.count
- before_uninvolved_notifs = users(:moderator).notifications.count
- post :create_thread, params: { post_id: posts(:question_one).id, title: 'sample thread title',
- body: "sample comment content @##{users(:deleter).id} @##{users(:moderator).id}" }
- assert_response 302
- assert_redirected_to post_path(assigns(:post))
- assert_not_nil assigns(:post)
- assert_not_nil assigns(:comment)&.id
- assert_not_nil assigns(:comment_thread)&.id
- assert_nil flash[:danger]
- assert_equal before_author_notifs + 1, users(:standard_user).notifications.count,
- 'Author notification not created when it should have been'
- assert_equal before_uninvolved_notifs, users(:moderator).notifications.count,
- 'Uninvolved notification created when it should not have been'
- assert assigns(:comment_thread).followed_by?(users(:editor)), 'Follower record not created for thread author'
- end
-
- test 'should require auth to create thread' do
- post :create_thread, params: { post_id: posts(:question_one).id, title: 'sample thread title',
- body: "sample comment content @##{users(:deleter).id} @##{users(:moderator).id}" }
- assert_response 302
- assert_redirected_to new_user_session_path
- end
-
- test 'should not create thread if comments disabled' do
- sign_in users(:editor)
- post :create_thread, params: { post_id: posts(:comments_disabled).id, title: 'sample thread title',
- body: "sample comment content @##{users(:deleter).id} @##{users(:moderator).id}" }
- assert_response 403
- assert_equal 'Comments have been disabled on this post.', JSON.parse(response.body)['message']
- end
-
- test 'should not create thread on inaccessible post' do
- sign_in users(:editor)
- post :create_thread, params: { post_id: posts(:high_trust).id, title: 'sample thread title',
- body: "sample comment content @##{users(:deleter).id} @##{users(:moderator).id}" }
- assert_response 404
- end
-
- test 'should add comment to existing thread' do
- sign_in users(:editor)
- before_author_notifs = users(:standard_user).notifications.count
- before_follow_notifs = users(:deleter).notifications.count
- before_uninvolved_notifs = users(:moderator).notifications.count
- post :create, params: { id: comment_threads(:normal).id, post_id: posts(:question_one).id,
- content: "comment content @##{users(:deleter).id} @##{users(:moderator).id}" }
- assert_response 302
- assert_redirected_to comment_thread_path(assigns(:comment_thread))
- assert_not_nil assigns(:post)
- assert_not_nil assigns(:comment_thread)
- assert_not_nil assigns(:comment)&.id
- assert_equal before_author_notifs + 1, users(:standard_user).notifications.count,
- 'Post author notification not created when it should have been'
- assert_equal before_follow_notifs + 1, users(:deleter).notifications.count,
- 'Thread follower notification not created when it should have been'
- assert_equal before_uninvolved_notifs, users(:moderator).notifications.count,
- 'Uninvolved notification created when it should not have been'
- assert assigns(:comment_thread).followed_by?(users(:editor)), 'Follower record not created for comment author'
- end
-
- test 'should require auth to add comment' do
- post :create, params: { id: comment_threads(:normal).id, post_id: posts(:question_one).id,
- content: "comment content @##{users(:deleter).id} @##{users(:moderator).id}" }
- assert_response 302
- assert_redirected_to new_user_session_path
- end
-
- test 'should not add comment if comments disabled' do
- sign_in users(:editor)
- post :create, params: { id: comment_threads(:comments_disabled).id, post_id: posts(:comments_disabled).id,
- content: "comment content @##{users(:deleter).id} @##{users(:moderator).id}" }
- assert_response 403
- assert_equal 'Comments have been disabled on this post.', JSON.parse(response.body)['message']
- end
-
- test 'should not add comment on inaccessible post' do
- sign_in users(:editor)
- post :create, params: { id: comment_threads(:high_trust).id, post_id: posts(:high_trust).id,
- content: "comment content @##{users(:deleter).id} @##{users(:moderator).id}" }
- assert_response 404
- end
-
- test 'should edit comment' do
- sign_in users(:standard_user)
- post :update, params: { id: comments(:one).id, comment: { content: 'Edited comment content' } }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should require auth to edit comment' do
- post :update, params: { id: comments(:one).id, comment: { content: 'Edited comment content' } }
- assert_response 302
- assert_redirected_to new_user_session_path
- end
-
- test 'should allow moderator to edit comment' do
- sign_in users(:moderator)
- post :update, params: { id: comments(:one).id, comment: { content: 'Edited comment content' } }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should not allow other users to edit comment' do
- sign_in users(:editor)
- post :update, params: { id: comments(:one).id, comment: { content: 'Edited comment content' } }
- assert_response 403
- end
-
- test 'should delete comment' do
- sign_in users(:standard_user)
- delete :destroy, params: { id: comments(:one).id }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should require auth to delete comment' do
- delete :destroy, params: { id: comments(:one).id }
- assert_response 302
- assert_redirected_to new_user_session_path
- end
-
- test 'should allow moderator to delete comment' do
- sign_in users(:moderator)
- delete :destroy, params: { id: comments(:one).id }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should not allow other users to delete comment' do
- sign_in users(:editor)
- delete :destroy, params: { id: comments(:one).id }
- assert_response 403
- end
-
- test 'should restore comment' do
- sign_in users(:standard_user)
- patch :undelete, params: { id: comments(:one).id }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should require auth to restore comment' do
- patch :undelete, params: { id: comments(:one).id }
- assert_response 302
- assert_redirected_to new_user_session_path
- end
-
- test 'should allow moderator to restore comment' do
- sign_in users(:moderator)
- patch :undelete, params: { id: comments(:one).id }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should not allow other users to restore comment' do
- sign_in users(:editor)
- patch :undelete, params: { id: comments(:one).id }
- assert_response 403
- end
-
- test 'should get comment' do
- get :show, params: { id: comments(:one).id }
- assert_response 200
- assert_not_nil assigns(:comment)
- end
-
- test 'should get thread' do
- get :thread, params: { id: comment_threads(:normal).id }
- assert_response 200
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should require auth to access high trust thread' do
- get :thread, params: { id: comment_threads(:high_trust).id }
- assert_response 404
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should require privileges to access high trust thread' do
- sign_in users(:deleter)
- get :thread, params: { id: comment_threads(:high_trust).id }
- assert_response 404
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should access thread on own deleted post' do
- sign_in users(:closer)
- get :thread, params: { id: comment_threads(:on_deleted_post).id }
- assert_response 200
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should get thread followers' do
- sign_in users(:admin)
- get :thread_followers, params: { id: comment_threads(:normal).id }
- assert_response 200
- assert_not_nil assigns(:comment_thread)
- assert_not_nil assigns(:followers)
- end
-
- test 'should require auth to get thread followers' do
- get :thread_followers, params: { id: comment_threads(:normal).id }
- assert_response 302
- assert_redirected_to new_user_session_path
- end
-
- test 'should require moderator to get thread followers' do
- sign_in users(:standard_user)
- get :thread_followers, params: { id: comment_threads(:normal).id }
- assert_response 404
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should rename thread' do
- sign_in users(:deleter)
- post :thread_rename, params: { id: comment_threads(:normal).id, title: 'new thread title' }
- assert_response 302
- assert_not_nil assigns(:comment_thread)
- assert_redirected_to comment_thread_path(assigns(:comment_thread))
- assert_equal 'new thread title', assigns(:comment_thread).title
- end
-
- test 'should prevent non-moderator renaming locked thread' do
- sign_in users(:deleter)
- before_title = comment_threads(:locked).title
- post :thread_rename, params: { id: comment_threads(:locked).id, title: 'new thread title' }
- assert_response 302
- assert_not_nil assigns(:comment_thread)
- assert_redirected_to comment_thread_path(assigns(:comment_thread))
- assert_equal before_title, assigns(:comment_thread).title
- end
-
- test 'should lock thread' do
- sign_in users(:deleter)
- post :thread_restrict, params: { id: comment_threads(:normal).id, type: 'lock' }
- assert_response 302
- assert_not_nil assigns(:comment_thread)
- assert_redirected_to comment_thread_path(assigns(:comment_thread))
- assert_equal true, assigns(:comment_thread).locked
- end
-
- test 'should require privilege to lock thread' do
- sign_in users(:standard_user)
- post :thread_restrict, params: { id: comment_threads(:normal).id, type: 'lock' }
- assert_response 404
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should delete thread' do
- sign_in users(:deleter)
- post :thread_restrict, params: { id: comment_threads(:normal).id, type: 'delete' }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should require privilege to delete thread' do
- sign_in users(:standard_user)
- post :thread_restrict, params: { id: comment_threads(:normal).id, type: 'delete' }
- assert_response 404
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should archive thread' do
- sign_in users(:deleter)
- post :thread_restrict, params: { id: comment_threads(:normal).id, type: 'archive' }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should require privilege to archive thread' do
- sign_in users(:standard_user)
- post :thread_restrict, params: { id: comment_threads(:normal).id, type: 'archive' }
- assert_response 404
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should follow thread' do
- sign_in users(:standard_user)
- post :thread_restrict, params: { id: comment_threads(:normal).id, type: 'follow' }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should unlock thread' do
- sign_in users(:deleter)
- post :thread_unrestrict, params: { id: comment_threads(:locked).id, type: 'lock' }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should require privilege to unlock thread' do
- sign_in users(:standard_user)
- post :thread_unrestrict, params: { id: comment_threads(:locked).id, type: 'lock' }
- assert_response 404
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should undelete thread' do
- sign_in users(:moderator)
- post :thread_unrestrict, params: { id: comment_threads(:deleted).id, type: 'delete' }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should require privilege to undelete thread' do
- sign_in users(:standard_user)
- post :thread_unrestrict, params: { id: comment_threads(:deleted).id, type: 'delete' }
- assert_response 404
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should unarchive thread' do
- sign_in users(:deleter)
- post :thread_unrestrict, params: { id: comment_threads(:archived).id, type: 'archive' }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should require privilege to unarchive thread' do
- sign_in users(:standard_user)
- post :thread_unrestrict, params: { id: comment_threads(:archived).id, type: 'archive' }
- assert_response 404
- assert_not_nil assigns(:comment_thread)
- end
-
- test 'should unfollow thread' do
- sign_in users(:standard_user)
- post :thread_unrestrict, params: { id: comment_threads(:normal).id, type: 'follow' }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'success', JSON.parse(response.body)['status']
- end
-
- test 'should get comment threads on post' do
- get :post, params: { post_id: posts(:question_one).id }
- assert_response 200
- assert_not_nil assigns(:post)
- assert_not_nil assigns(:comment_threads)
- end
-
- test 'should get pingable users on thread' do
- sign_in users(:standard_user)
- get :pingable, params: { id: -1, post: posts(:question_one).id }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- end
-end
diff --git a/test/controllers/concerns/users/users_abilities_test.rb b/test/controllers/concerns/users/users_abilities_test.rb
new file mode 100644
index 000000000..295e922e4
--- /dev/null
+++ b/test/controllers/concerns/users/users_abilities_test.rb
@@ -0,0 +1,54 @@
+module UsersAbilitiesTest
+ extend ActiveSupport::Concern
+
+ included do
+ test 'mod_privilege_action: grant as new' do
+ sign_in users(:moderator)
+ post :mod_privilege_action, params: { ability: abilities(:flag_curate).internal_id, do: 'grant',
+ id: users(:standard_user).id }
+ assert_response(:success)
+ assert users(:standard_user).community_user.ability?(abilities(:flag_curate).internal_id),
+ "User was not granted expected ability #{abilities(:flag_curate).internal_id}"
+ end
+
+ test 'mod_privilege_action: grant as unsuspend' do
+ sign_in users(:moderator)
+ post :mod_privilege_action, params: { ability: abilities(:edit_posts).internal_id, do: 'grant',
+ id: users(:enabled_2fa).id }
+ assert_response(:success)
+ assert users(:enabled_2fa).community_user.ability?(abilities(:edit_posts).internal_id),
+ "User was not granted expected ability #{abilities(:edit_posts).internal_id}"
+ end
+
+ test 'mod_privilege_action: suspend' do
+ sign_in users(:moderator)
+ post :mod_privilege_action, params: { ability: abilities(:unrestricted).internal_id, do: 'suspend',
+ id: users(:standard_user).id, duration: -1 }
+ assert_response(:success)
+ assert_not users(:standard_user).community_user.ability?(abilities(:unrestricted).internal_id),
+ "User still has ability #{abilities(:unrestricted).internal_id} that should have been suspended"
+ end
+
+ test 'mod_privilege_action: delete' do
+ sign_in users(:moderator)
+ post :mod_privilege_action, params: { ability: abilities(:unrestricted).internal_id, do: 'delete',
+ id: users(:standard_user).id }
+ assert_response(:success)
+ assert_not users(:standard_user).community_user.ability?(abilities(:unrestricted).internal_id),
+ "User still has ability #{abilities(:unrestricted).internal_id} that should have been deleted"
+ end
+
+ test 'mod_privilege_action: unrecognized action' do
+ sign_in users(:moderator)
+ post :mod_privilege_action, params: { ability: abilities(:unrestricted).internal_id, do: 'unrecognized',
+ id: users(:standard_user).id }
+ assert_response(:not_found)
+ end
+
+ test 'mod_privilege_action: require moderator' do
+ post :mod_privilege_action, params: { ability: abilities(:unrestricted).internal_id, do: 'unrecognized',
+ id: users(:standard_user).id }
+ assert_response(:not_found)
+ end
+ end
+end
diff --git a/test/controllers/donations_controller_test.rb b/test/controllers/donations_controller_test.rb
index c2ed31624..21fa129a2 100644
--- a/test/controllers/donations_controller_test.rb
+++ b/test/controllers/donations_controller_test.rb
@@ -5,13 +5,14 @@ class DonationsControllerTest < ActionController::TestCase
test 'should get index' do
get :index
- assert_response 200
+ assert_response(:success)
end
test 'should create PaymentIntent' do
skip unless Stripe.api_key
post :intent, params: { currency: 'EUR', amount: '24.99', desc: 'Created from Rails test' }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:intent)&.id
end
end
diff --git a/test/controllers/fake_community_controller_test.rb b/test/controllers/fake_community_controller_test.rb
index dd5e089de..279f67237 100644
--- a/test/controllers/fake_community_controller_test.rb
+++ b/test/controllers/fake_community_controller_test.rb
@@ -8,7 +8,7 @@ class FakeCommunityControllerTest < ActionController::TestCase
request.env['HTTP_HOST'] = 'sample.qpixel.com'
get(:communities)
- assert_response(404)
+ assert_response(:not_found)
end
test 'fake community should be able to access fake community controller' do
@@ -16,7 +16,7 @@ class FakeCommunityControllerTest < ActionController::TestCase
request.env['HTTP_HOST'] = 'fake.qpixel.com'
get(:communities)
- assert_response(200)
+ assert_response(:success)
assert_not_nil assigns(:communities)
end
end
diff --git a/test/controllers/flags_controller_test.rb b/test/controllers/flags_controller_test.rb
index 58569e762..53b0aca80 100644
--- a/test/controllers/flags_controller_test.rb
+++ b/test/controllers/flags_controller_test.rb
@@ -6,83 +6,121 @@ class FlagsControllerTest < ActionController::TestCase
test 'should create new post flag' do
sign_in users(:standard_user)
post :new, params: { reason: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', post_id: posts(:answer_two).id, post_type: 'Post' }
+
assert_not_nil assigns(:flag)
assert_not_nil assigns(:flag).post
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response(201)
+ assert_response(:created)
end
test 'should create new comment flag' do
sign_in users(:standard_user)
post :new, params: { reason: 'ABCDEF GHIJKL MNOPQR STUVWX YZ', post_id: comments(:one).id, post_type: 'Comment' }
+
assert_not_nil assigns(:flag)
assert_not_nil assigns(:flag).post
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response(201)
+ assert_response(:created)
end
test 'should retrieve flag queue' do
sign_in users(:moderator)
get :queue
assert_not_nil assigns(:flags)
- assert_response(200)
+ assert_response(:success)
end
test 'should add status to flag' do
sign_in users(:moderator)
- post :resolve, params: { id: flags(:one).id, result: 'ABCDEF', message: 'ABCDEF GHIJKL MNOPQR STUVWX YZ' }
+
+ try_resolve_flag(flags(:one), result: 'Helpful', message: 'Please send us more flags')
+
assert_not_nil assigns(:flag)
assert_not_nil assigns(:flag).status
assert_not_nil assigns(:flag).handled_by
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response(200)
+ assert_response(:success)
end
test 'should require authentication to create flag' do
sign_out :user
post :new
- assert_response(302)
+ assert_response(:found)
end
test 'should require authentication to get queue' do
sign_out :user
get :queue
- assert_response(302)
+ assert_response(:found)
end
test 'should require moderator status to get queue' do
sign_in users(:standard_user)
get :queue
- assert_response(404)
+ assert_response(:not_found)
end
test 'should require authentication to resolve flag' do
sign_out :user
- post :resolve, params: { id: flags(:one).id }
- assert_response(302)
+ try_resolve_flag(flags(:one))
+ assert_response(:found)
end
test 'should require moderator status to resolve flag' do
sign_in users(:standard_user)
- post :resolve, params: { id: flags(:one).id }
- assert_response(404)
+ try_resolve_flag(flags(:one))
+ assert_response(:not_found)
+ end
+
+ test 'should not allow non-moderator users to resolve flags on themselves' do
+ sign_in users(:deleter)
+ try_resolve_flag(flags(:on_deleter))
+ assert_response(:not_found)
+ end
+
+ test 'should not allow non-moderator users to resolve confidential flags' do
+ sign_in users(:deleter)
+ try_resolve_flag(flags(:confidential_on_deleter))
+ assert_response(:not_found)
end
test 'should get handled flags list' do
sign_in users(:moderator)
get :handled
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:flags)
end
test 'should require authentication to get handled flags list' do
get :handled
- assert_response 302
+ assert_response(:found)
end
test 'should require moderator status to get handled flags list' do
sign_in users(:standard_user)
get :handled
- assert_response 404
+ assert_response(:not_found)
+ end
+
+ test 'non-moderator users should only see their flag history' do
+ mod_user = users(:moderator)
+ std_user = users(:standard_user)
+
+ sign_in std_user
+ get :history, params: { id: mod_user.id }
+ assert_response(:not_found)
+
+ get :history, params: { id: std_user.id }
+ assert_response(:success)
+
+ sign_in mod_user
+ get :history, params: { id: std_user.id }
+ assert_response(:success)
+ end
+
+ private
+
+ def try_resolve_flag(flag, result: nil, message: nil)
+ post :resolve, params: { id: flag.id, result: result, message: message }
end
end
diff --git a/test/controllers/licenses_controller_test.rb b/test/controllers/licenses_controller_test.rb
index 18b20515a..830c9d663 100644
--- a/test/controllers/licenses_controller_test.rb
+++ b/test/controllers/licenses_controller_test.rb
@@ -6,70 +6,68 @@ class LicensesControllerTest < ActionController::TestCase
test 'should require authentication to access license pages' do
[:index, :new].each do |action|
get action
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
get :edit, params: { id: licenses(:cc_by_sa).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'should require authentication to modify licenses' do
post :create, params: { license: { name: 'Test', url: 'Test', default: false } }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
patch :update, params: { id: licenses(:cc_by_sa).id, license: { name: 'Test', url: 'Test', default: false } }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'should require admin to access license pages' do
sign_in users(:standard_user)
[:index, :new].each do |action|
get action
- assert_response 404
+ assert_response(:not_found)
end
get :edit, params: { id: licenses(:cc_by_sa).id }
- assert_response 404
+ assert_response(:not_found)
end
test 'should require admin to modify licenses' do
sign_in users(:standard_user)
+
post :create, params: { license: { name: 'Test', url: 'Test', default: false } }
- assert_response 404
+ assert_response(:not_found)
patch :update, params: { id: licenses(:cc_by_sa).id, license: { name: 'Test', url: 'Test', default: false } }
- assert_response 404
+ assert_response(:not_found)
end
test 'should allow admins to access index' do
sign_in users(:admin)
get :index
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:licenses)
end
test 'should allow admins to access new' do
sign_in users(:admin)
get :new
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:license)
end
test 'should allow admins to access edit' do
sign_in users(:admin)
get :edit, params: { id: licenses(:cc_by_sa).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:license)
end
test 'should allow admins to create new licenses' do
sign_in users(:admin)
post :create, params: { license: { name: 'Test', url: 'Test', default: false } }
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to licenses_path
assert_not_nil assigns(:license)
assert_not_nil assigns(:license).id
@@ -79,7 +77,8 @@ class LicensesControllerTest < ActionController::TestCase
test 'should allow admins to update existing license' do
sign_in users(:admin)
patch :update, params: { id: licenses(:cc_by_sa).id, license: { name: 'Test', url: 'Test', default: false } }
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to licenses_path
assert_not_nil assigns(:license)
assert_equal 'Test', assigns(:license).name
@@ -88,7 +87,8 @@ class LicensesControllerTest < ActionController::TestCase
test 'should allow admins to disable not-in-use license' do
sign_in users(:admin)
post :toggle, params: { id: licenses(:not_in_use).id }
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to licenses_path
assert_nil flash[:danger]
assert_not_nil assigns(:license)
@@ -98,7 +98,8 @@ class LicensesControllerTest < ActionController::TestCase
test 'should prevent admins disabling in-use license' do
sign_in users(:admin)
post :toggle, params: { id: licenses(:cc_by_sa).id }
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to licenses_path
assert_not_nil assigns(:license)
assert_not_nil flash[:danger]
@@ -108,7 +109,8 @@ class LicensesControllerTest < ActionController::TestCase
test 'should only allow one default license' do
sign_in users(:admin)
post :update, params: { id: licenses(:cc_by_nc_sa).id, license: { name: 'Test', url: 'Test', default: true } }
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to licenses_path
assert_not_nil assigns(:license)
assert_equal true, assigns(:license).default
diff --git a/test/controllers/micro_auth/apps_controller_test.rb b/test/controllers/micro_auth/apps_controller_test.rb
index 781c55df3..df7ea7e2b 100644
--- a/test/controllers/micro_auth/apps_controller_test.rb
+++ b/test/controllers/micro_auth/apps_controller_test.rb
@@ -1,7 +1,57 @@
require 'test_helper'
class MicroAuth::AppsControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
+ include Devise::Test::IntegrationHelpers
+
+ test 'should require authentication to create apps' do
+ post create_oauth_app_path
+
+ assert_response(:found)
+ assert_redirected_to new_user_session_path
+ end
+
+ test 'should correctly create apps' do
+ sign_in users(:standard_user)
+
+ try_create_oauth_app
+
+ assert_response(:found)
+
+ @app = assigns(:app)
+ assert_not_nil @app
+ assert_redirected_to oauth_app_path(@app.app_id)
+ end
+
+ test 'only owners or admins should be able to update apps' do
+ app = micro_auth_apps(:owned_by_standard)
+
+ [:standard_user, :global_admin, :editor].each do |name|
+ updater = users(name)
+
+ sign_in updater
+
+ post update_oauth_app_path(app.app_id), params: {
+ micro_auth_app: { name: 'Updated name' }
+ }
+
+ if app.user.same_as?(updater) || updater.admin?
+ assert_response(:found, "Expected #{updater.username} to be able to update the app")
+ assert_redirected_to oauth_app_path(app.app_id)
+ else
+ assert_response(:not_found)
+ end
+ end
+ end
+
+ private
+
+ def try_create_oauth_app(name: 'MyApp', description: 'test MicroAuth App', auth_domain: 'localhost')
+ post create_oauth_app_path, params: {
+ micro_auth_app: {
+ auth_domain: auth_domain,
+ name: name,
+ description: description
+ }
+ }
+ end
end
diff --git a/test/controllers/micro_auth/authentication_controller_test.rb b/test/controllers/micro_auth/authentication_controller_test.rb
index 78ab8cf06..27283f869 100644
--- a/test/controllers/micro_auth/authentication_controller_test.rb
+++ b/test/controllers/micro_auth/authentication_controller_test.rb
@@ -1,7 +1,38 @@
require 'test_helper'
class MicroAuth::AuthenticationControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
+ include Devise::Test::IntegrationHelpers
+
+ test 'token should correctly handle missing apps' do
+ post oauth_token_path, params: {
+ app_id: 'i_do_not_exist',
+ secret: SecureRandom.base58(32),
+ format: :json
+ }
+
+ assert_response(:bad_request)
+ assert_valid_json_response
+ res_body = JSON.parse(response.body)
+ error = res_body['error']
+ assert_equal 'app_mismatch', error['type']
+ assert_not_nil error['message']
+ end
+
+ test 'token should correctly handle missing tokens' do
+ app = micro_auth_apps(:owned_by_standard)
+
+ post oauth_token_path, params: {
+ app_id: app.app_id,
+ secret: app.secret_key,
+ code: 'this_is_not_the_code_you_are_looking_for',
+ format: :json
+ }
+
+ assert_response(:bad_request)
+ assert_valid_json_response
+ res_body = JSON.parse(response.body)
+ error = res_body['error']
+ assert_equal 'token_mismatch', error['type']
+ assert_not_nil error['message']
+ end
end
diff --git a/test/controllers/mod_warning_controller_test.rb b/test/controllers/mod_warning_controller_test.rb
index e2a0c2a10..edbcfda1d 100644
--- a/test/controllers/mod_warning_controller_test.rb
+++ b/test/controllers/mod_warning_controller_test.rb
@@ -7,7 +7,7 @@ class ModWarningControllerTest < ActionController::TestCase
sign_out :user
[:log, :new].each do |path|
get path, params: { user_id: users(:standard_user).id }
- assert_response(404)
+ assert_response(:not_found)
end
end
@@ -15,7 +15,18 @@ class ModWarningControllerTest < ActionController::TestCase
sign_in users(:standard_user)
[:log, :new].each do |path|
get path, params: { user_id: users(:standard_user).id }
- assert_response(404)
+ assert_response(:not_found)
+ end
+ end
+
+ test 'mods or admins should be able to access pages' do
+ [users(:moderator), users(:admin)].each do |user|
+ sign_in user
+
+ [:log, :new].each do |path|
+ get path, params: { user_id: users(:standard_user).id }
+ assert_response(:success)
+ end
end
end
@@ -58,4 +69,18 @@ class ModWarningControllerTest < ActionController::TestCase
@warning.reload
assert_not @warning.active
end
+
+ test 'lift should correctly deactivate user suspensions' do
+ sign_in users(:moderator)
+
+ std = users(:standard_user)
+ warning = mod_warnings(:third_warning)
+
+ warning.update(active: true)
+ post :lift, params: { user_id: std.id }
+
+ assert_response(:found)
+ warning.reload
+ assert_not warning.active
+ end
end
diff --git a/test/controllers/moderator_controller_test.rb b/test/controllers/moderator_controller_test.rb
index c425defa4..062626b51 100644
--- a/test/controllers/moderator_controller_test.rb
+++ b/test/controllers/moderator_controller_test.rb
@@ -6,14 +6,14 @@ class ModeratorControllerTest < ActionController::TestCase
test 'should get index' do
sign_in users(:moderator)
get :index
- assert_response(200)
+ assert_response(:success)
end
test 'should require authentication to access pages' do
sign_out :user
[:index, :recently_deleted_posts].each do |path|
get path
- assert_response(404)
+ assert_response(:not_found)
end
end
@@ -21,119 +21,190 @@ class ModeratorControllerTest < ActionController::TestCase
sign_in users(:standard_user)
[:index, :recently_deleted_posts].each do |path|
get path
- assert_response(404)
+ assert_response(:not_found)
end
end
+ # TODO: more descriptive test case descriptions
+ test 'should get recently deleted posts page' do
+ sign_in users(:moderator)
+ get :recently_deleted_posts
+
+ posts = assigns(:posts)
+
+ assert_response(:success)
+ assert_not_nil posts
+ assert posts.all?(&:deleted?)
+ end
+
test 'should get recent comments page' do
sign_in users(:moderator)
get :recent_comments
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:comments)
end
test 'can nominate for promotion' do
sign_in users(:deleter)
post :nominate_promotion, params: { id: posts(:question_one).id }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:success)
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
test 'cannot nominate locked post' do
sign_in users(:deleter)
post :nominate_promotion, params: { id: posts(:locked).id, format: :json }
- assert_response 403
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:forbidden)
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'nominate requires authentication' do
post :nominate_promotion, params: { id: posts(:question_one).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'unprivileged user cannot nominate' do
sign_in users(:standard_user)
post :nominate_promotion, params: { id: posts(:question_one).id, format: :json }
- assert_response 404
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:not_found)
+ assert_valid_json_response
assert_equal ['no_privilege'], JSON.parse(response.body)['errors']
end
test 'cannot nominate second-level post' do
sign_in users(:deleter)
post :nominate_promotion, params: { id: posts(:answer_one).id, format: :json }
- assert_response 404
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:not_found)
+ assert_valid_json_response
assert_equal ['unavailable_for_type'], JSON.parse(response.body)['errors']
end
test 'can get promotions list' do
sign_in users(:deleter)
get :promotions
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:promotions)
assert_not_nil assigns(:posts)
end
test 'promotions list requires auth' do
get :promotions
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'promotions list requires privileges' do
sign_in users(:standard_user)
get :promotions
- assert_response 404
+ assert_response(:not_found)
end
test 'can remove a post from promotions' do
- RequestContext.redis.set 'network/promoted_posts',
- JSON.dump({ posts(:question_one).id.to_s => 28.days.from_now.to_i })
+ RequestContext.redis.set('network/promoted_posts',
+ JSON.dump({ posts(:question_one).id.to_s => 28.days.from_now.to_i }))
sign_in users(:deleter)
delete :remove_promotion, params: { id: posts(:question_one).id }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:success)
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
assert_equal '{}', RequestContext.redis.get('network/promoted_posts')
end
test 'remove promotion requires auth' do
delete :remove_promotion, params: { id: posts(:question_one).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'remove promotion requires privileges' do
sign_in users(:standard_user)
+
delete :remove_promotion, params: { id: posts(:question_one).id, format: :json }
- assert_response 404
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:not_found)
+ assert_valid_json_response
assert_equal ['no_privilege'], JSON.parse(response.body)['errors']
end
test 'cannot remove unpromoted post' do
sign_in users(:deleter)
+
delete :remove_promotion, params: { id: posts(:question_two).id, format: :json }
- assert_response 404
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:not_found)
+ assert_valid_json_response
assert_equal ['not_promoted'], JSON.parse(response.body)['errors']
end
+
+ test 'user_vote_summary should provide correct user info' do
+ std = users(:standard_user)
+
+ sign_in users(:moderator)
+
+ get :user_vote_summary, params: { id: std.id }
+
+ related_votes = votes.select { |v| v.user.same_as?(std) || v.recv_user.same_as?(std) }
+ user = assigns(:user)
+ users = assigns(:users)
+
+ assert_response(:success)
+ assert user.same_as?(std)
+
+ related_votes.each do |v|
+ assert(users.any? { |u| u.same_as?(v.user) || u.same_as?(v.recv_user) })
+ end
+ end
+
+ test 'user_vote_summary should provide correct stats for votes cast' do
+ std = users(:standard_user)
+
+ sign_in users(:moderator)
+
+ get :user_vote_summary, params: { id: std.id }
+
+ vote_data = assigns(:vote_data)
+ votes_cast = votes.select { |v| v.user.same_as?(std) }
+
+ assert_equal vote_data[:cast][:total], votes_cast.length
+
+ vote_data[:cast][:breakdown].each do |data, count|
+ recv_user_id, type = data
+ votes = votes_cast.select { |v| v.vote_type == type && v.recv_user.id == recv_user_id }
+ assert_equal count, votes.length
+ end
+
+ vote_data[:cast][:types].each do |type, count|
+ votes = votes_cast.select { |v| v.vote_type == type }
+ assert_equal count, votes.length
+ end
+ end
+
+ test 'user_vote_summary should provide correct stats for votes received' do
+ std = users(:standard_user)
+
+ sign_in users(:moderator)
+
+ get :user_vote_summary, params: { id: std.id }
+
+ vote_data = assigns(:vote_data)
+ votes_received = votes.select { |v| v.recv_user.same_as?(std) }
+
+ assert_equal vote_data[:received][:total], votes_received.length
+
+ vote_data[:received][:breakdown].each do |data, count|
+ user_id, type = data
+ votes = votes_received.select { |v| v.vote_type == type && v.user.id == user_id }
+ assert_equal count, votes.length
+ end
+
+ vote_data[:received][:types].each do |type, count|
+ votes = votes_received.select { |v| v.vote_type == type }
+ assert_equal count, votes.length
+ end
+ end
end
diff --git a/test/controllers/notifications_controller_test.rb b/test/controllers/notifications_controller_test.rb
index 6d4596080..1b7b570cb 100644
--- a/test/controllers/notifications_controller_test.rb
+++ b/test/controllers/notifications_controller_test.rb
@@ -7,7 +7,7 @@ class NotificationsControllerTest < ActionController::TestCase
sign_in users(:standard_user)
get :index, params: { format: :json }
assert_not_nil assigns(:notifications)
- assert_response(200)
+ assert_response(:success)
end
test 'should mark notification as read' do
@@ -16,7 +16,7 @@ class NotificationsControllerTest < ActionController::TestCase
assert_not_nil assigns(:notification)
assert_equal true, assigns(:notification).is_read
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response(200)
+ assert_response(:success)
end
test 'should mark all notifications as read' do
@@ -27,18 +27,18 @@ class NotificationsControllerTest < ActionController::TestCase
assert_equal true, notification.is_read
end
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response(200)
+ assert_response(:success)
end
test 'should prevent users marking others notifications read' do
sign_in users(:editor)
post :read, params: { id: notifications(:one).id, format: :json }
- assert_response(403)
+ assert_response(:forbidden)
end
test 'should require authentication to get index' do
sign_out :user
get :index, params: { format: :json }
- assert_response(401) # Devise seems to respond 401 for JSON requests.
+ assert_response(:unauthorized) # Devise seems to respond 401 for JSON requests.
end
end
diff --git a/test/controllers/pinned_links_controller_test.rb b/test/controllers/pinned_links_controller_test.rb
index b7cb3d3d1..2003eafbe 100644
--- a/test/controllers/pinned_links_controller_test.rb
+++ b/test/controllers/pinned_links_controller_test.rb
@@ -1,7 +1,33 @@
require 'test_helper'
-class PinnedLinksControllerTest < ActionDispatch::IntegrationTest
- # test "the truth" do
- # assert true
- # end
+class PinnedLinksControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+
+ test 'edit should require moderator' do
+ sign_in users(:standard_user)
+ get :edit, params: { id: pinned_links(:active_with_label).id }
+ assert_response(:not_found)
+ end
+
+ test 'edit should work for moderators' do
+ sign_in users(:moderator)
+ get :edit, params: { id: pinned_links(:active_with_label).id }
+ assert_response(:success)
+ assert_not_nil assigns(:link)
+ end
+
+ test 'update should require moderator' do
+ sign_in users(:standard_user)
+ post :update, params: { id: pinned_links(:active_with_label).id, pinned_link: { label: 'updated label' } }
+ assert_response(:not_found)
+ end
+
+ test 'update should work for moderators' do
+ sign_in users(:moderator)
+ post :update, params: { id: pinned_links(:active_with_label).id, pinned_link: { label: 'updated label' } }
+ assert_response(:found)
+ assert_redirected_to pinned_links_path
+ assert_not_nil assigns(:link)
+ assert_equal 'updated label', assigns(:link).label
+ end
end
diff --git a/test/controllers/post_history_controller_test.rb b/test/controllers/post_history_controller_test.rb
index c86257132..636ac45a0 100644
--- a/test/controllers/post_history_controller_test.rb
+++ b/test/controllers/post_history_controller_test.rb
@@ -5,27 +5,27 @@ class PostHistoryControllerTest < ActionController::TestCase
test 'should get post history page' do
get :post, params: { id: posts(:question_one).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:history)
assert_not_nil assigns(:post)
end
test 'anon user can access public post history' do
get :post, params: { id: posts(:question_one).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:history)
assert_not_nil assigns(:post)
end
test 'anon user cannot access deleted post history' do
get :post, params: { id: posts(:deleted).id }
- assert_response 404
+ assert_response(:not_found)
end
test 'privileged user can access deleted post history' do
sign_in users(:deleter)
get :post, params: { id: posts(:deleted).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:history)
assert_not_nil assigns(:post)
end
diff --git a/test/controllers/post_types_controller_test.rb b/test/controllers/post_types_controller_test.rb
index af1c033d3..363c0b1a7 100644
--- a/test/controllers/post_types_controller_test.rb
+++ b/test/controllers/post_types_controller_test.rb
@@ -6,39 +6,37 @@ class PostTypesControllerTest < ActionController::TestCase
test 'can get index' do
sign_in users(:global_admin)
get :index
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:types)
end
test 'index requires auth' do
get :index
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'index requires global admin' do
sign_in users(:admin)
get :index
- assert_response 404
+ assert_response(:not_found)
end
test 'can get new' do
sign_in users(:global_admin)
get :new
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:type)
end
test 'new requires auth' do
get :new
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'new requires global admin' do
sign_in users(:admin)
get :new
- assert_response 404
+ assert_response(:not_found)
end
test 'can create post type' do
@@ -48,7 +46,7 @@ class PostTypesControllerTest < ActionController::TestCase
answer_type_id: Answer.post_type_id, has_reactions: false,
has_only_specific_reactions: true }
post :create, params: { post_type: data }
- assert_response 302
+ assert_response(:found)
assert_redirected_to post_types_path
# Test, if the correct values are applied
@@ -61,34 +59,32 @@ class PostTypesControllerTest < ActionController::TestCase
test 'create requires auth' do
post :create, params: { post_type: { name: 'Test Type', description: 'words', icon_name: 'heart',
has_answers: 'true', has_license: 'true', has_category: 'true' } }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'create requires global admin' do
sign_in users(:admin)
post :create, params: { post_type: { name: 'Test Type', description: 'words', icon_name: 'heart',
has_answers: 'true', has_license: 'true', has_category: 'true' } }
- assert_response 404
+ assert_response(:not_found)
end
test 'can get edit' do
sign_in users(:global_admin)
get :edit, params: { id: post_types(:question).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:type)
end
test 'edit requires auth' do
get :edit, params: { id: post_types(:question).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'edit requires global admin' do
sign_in users(:admin)
get :edit, params: { id: post_types(:question).id }
- assert_response 404
+ assert_response(:not_found)
end
test 'can update post type' do
@@ -99,7 +95,7 @@ class PostTypesControllerTest < ActionController::TestCase
has_only_specific_reactions: true }
patch :update, params: { post_type: data,
id: post_types(:question).id }
- assert_response 302
+ assert_response(:found)
assert_redirected_to post_types_path
# Test, if the correct values are applied
@@ -113,8 +109,7 @@ class PostTypesControllerTest < ActionController::TestCase
patch :update, params: { post_type: { name: 'Test Type', description: 'words', icon_name: 'heart',
has_answers: 'true', has_license: 'true', has_category: 'true' },
id: post_types(:question).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'update requires global admin' do
@@ -122,6 +117,6 @@ class PostTypesControllerTest < ActionController::TestCase
patch :update, params: { post_type: { name: 'Test Type', description: 'words', icon_name: 'heart',
has_answers: 'true', has_license: 'true', has_category: 'true' },
id: post_types(:question).id }
- assert_response 404
+ assert_response(:not_found)
end
end
diff --git a/test/controllers/posts/change_category_test.rb b/test/controllers/posts/change_category_test.rb
index acb546eef..5b8022a8f 100644
--- a/test/controllers/posts/change_category_test.rb
+++ b/test/controllers/posts/change_category_test.rb
@@ -5,33 +5,33 @@ class PostsControllerTest < ActionController::TestCase
test 'should change category' do
sign_in users(:deleter)
+
post :change_category, params: { id: posts(:article_one).id, target_id: categories(:articles_only).id }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
assert_not_nil assigns(:target)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal categories(:articles_only).id, assigns(:post).category_id
end
test 'should deny change category to unprivileged' do
sign_in users(:standard_user)
+
post :change_category, params: { id: posts(:article_one).id, target_id: categories(:articles_only).id }
- assert_response 403
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:forbidden)
+ assert_valid_json_response
assert_equal ["You don't have permission to make that change.\n"], JSON.parse(response.body)['errors']
end
test 'should refuse to change category of wrong post type' do
sign_in users(:deleter)
+
post :change_category, params: { id: posts(:question_one).id, target_id: categories(:articles_only).id }
- assert_response 409
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:conflict)
+ assert_valid_json_response
assert_equal ["This post type is not allowed in the #{categories(:articles_only).name} category.\n"],
JSON.parse(response.body)['errors']
end
diff --git a/test/controllers/posts/close_test.rb b/test/controllers/posts/close_test.rb
index 1c69c5a41..0b5545058 100644
--- a/test/controllers/posts/close_test.rb
+++ b/test/controllers/posts/close_test.rb
@@ -5,100 +5,101 @@ class PostsControllerTest < ActionController::TestCase
test 'can close question' do
sign_in users(:closer)
+
before_history = PostHistory.where(post: posts(:question_one)).count
post :close, params: { id: posts(:question_one).id, reason_id: close_reasons(:not_good).id }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
assert_equal before_history + 1, after_history, 'PostHistory event not created on closure'
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
test 'user can close own question' do
sign_in users(:standard_user)
+
before_history = PostHistory.where(post: posts(:question_one)).count
post :close, params: { id: posts(:question_one).id, reason_id: close_reasons(:not_good).id }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
assert_equal before_history + 1, after_history, 'PostHistory event not created on closure'
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
test 'close requires authentication' do
post :close, params: { id: posts(:question_one).id, reason_id: close_reasons(:not_good).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'unprivileged user cannot close' do
sign_in users(:standard_user)
+
before_history = PostHistory.where(post: posts(:question_two)).count
post :close, params: { id: posts(:question_two).id, reason_id: close_reasons(:not_good).id }
after_history = PostHistory.where(post: posts(:question_two)).count
- assert_response 403
+
+ assert_response(:forbidden)
assert_not_nil assigns(:post)
assert_equal before_history, after_history, 'PostHistory event incorrectly created on closure'
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'cannot close a closed post' do
sign_in users(:closer)
+
before_history = PostHistory.where(post: posts(:closed)).count
post :close, params: { id: posts(:closed).id, reason_id: close_reasons(:not_good).id }
after_history = PostHistory.where(post: posts(:closed)).count
- assert_response 400
+
+ assert_response(:bad_request)
assert_not_nil assigns(:post)
assert_equal before_history, after_history, 'PostHistory event incorrectly created on closure'
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'close rejects nonexistent close reason' do
sign_in users(:closer)
+
before_history = PostHistory.where(post: posts(:question_one)).count
post :close, params: { id: posts(:question_one).id, reason_id: -999 }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 404
+
+ assert_response(:not_found)
assert_not_nil assigns(:post)
assert_equal before_history, after_history, 'PostHistory event incorrectly created on closure'
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'close ensures other post exists if reason requires it' do
sign_in users(:closer)
+
before_history = PostHistory.where(post: posts(:question_one)).count
post :close, params: { id: posts(:question_one).id, reason_id: close_reasons(:duplicate) }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 400
+
+ assert_response(:bad_request)
assert_not_nil assigns(:post)
assert_equal before_history, after_history, 'PostHistory event incorrectly created on closure'
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'cannot close a locked post' do
sign_in users(:closer)
+
before_history = PostHistory.where(post: posts(:locked)).count
post :close, params: { id: posts(:locked).id, reason_id: close_reasons(:not_good).id }
after_history = PostHistory.where(post: posts(:locked)).count
- assert_response 403
+
+ assert_response(:forbidden)
assert_equal before_history, after_history, 'PostHistory event incorrectly created on close'
end
end
diff --git a/test/controllers/posts/create_test.rb b/test/controllers/posts/create_test.rb
index e955cfa4c..255f50742 100644
--- a/test/controllers/posts/create_test.rb
+++ b/test/controllers/posts/create_test.rb
@@ -3,72 +3,72 @@
class PostsControllerTest < ActionController::TestCase
include Devise::Test::ControllerHelpers
- test 'can create help post' do
- sign_in users(:moderator)
- post :create, params: { post_type: post_types(:help_doc).id,
- post: { post_type_id: post_types(:help_doc).id, title: sample.title, doc_slug: 'topic',
- body_markdown: sample.body_markdown, help_category: 'A', help_ordering: '99' } }
- assert_response 302
- assert_not_nil assigns(:post).id
- assert_redirected_to help_path(assigns(:post).doc_slug)
- end
-
test 'can create category post' do
sign_in users(:standard_user)
- post :create, params: { post_type: post_types(:question).id, category: categories(:main).id,
- post: { post_type_id: post_types(:question).id, title: sample.title,
- body_markdown: sample.body_markdown, category_id: categories(:main).id,
- tags_cache: sample.tags_cache, license_id: licenses(:cc_by_sa).id } }
- assert_response 302
+
+ try_create_post
+
+ assert_response(:found)
assert_not_nil assigns(:post).id
assert_redirected_to post_path(assigns(:post))
end
test 'can create answer' do
sign_in users(:closer)
+
before_notifs = posts(:question_one).user.notifications.count
- post :create, params: { post_type: post_types(:answer).id, parent: posts(:question_one).id,
- post: { post_type_id: post_types(:answer).id, title: sample.title,
- body_markdown: sample.body_markdown, parent_id: posts(:question_one).id,
- license_id: licenses(:cc_by_sa).id } }
+ try_create_post(post_type: post_types(:answer), parent: posts(:question_one))
after_notifs = posts(:question_one).user.notifications.count
- assert_response 302
+
+ assert_response(:found)
assert_not_nil assigns(:post).id
assert_equal before_notifs + 1, after_notifs, 'Notification not created on answer create'
assert_redirected_to post_path(posts(:question_one).id, anchor: "answer-#{assigns(:post).id}")
end
test 'create requires authentication' do
- post :create, params: { post_type: post_types(:question).id, category: categories(:main).id,
- post: { post_type_id: post_types(:question).id, title: sample.title,
- body_markdown: sample.body_markdown, category_id: categories(:main).id,
- tags_cache: sample.tags_cache } }
- assert_response 302
- assert_redirected_to new_user_session_path
+ try_create_post
+ assert_redirected_to_sign_in
+ end
+
+ test 'can create help post' do
+ sign_in users(:moderator)
+
+ post :create, params: { post_type: post_types(:help_doc).id,
+ post: { post_type_id: post_types(:help_doc).id, title: sample.title, doc_slug: 'topic',
+ body_markdown: sample.body_markdown, help_category: 'A', help_ordering: '99' } }
+
+ assert_response(:found)
+ assert_not_nil assigns(:post).id
+ assert_redirected_to help_path(assigns(:post).doc_slug)
end
test 'standard users cannot create help posts' do
sign_in users(:standard_user)
+
post :create, params: { post_type: post_types(:help_doc).id,
post: { post_type_id: post_types(:help_doc).id, title: sample.title, doc_slug: 'topic',
body_markdown: sample.body_markdown, help_category: 'A', help_ordering: '99' } }
- assert_response 404
+
+ assert_response(:not_found)
end
test 'moderators cannot create policy posts' do
sign_in users(:moderator)
+
post :create, params: { post_type: post_types(:policy_doc).id,
post: { post_type_id: post_types(:policy_doc).id, title: sample.title, doc_slug: 'topic',
body_markdown: sample.body_markdown, help_category: 'A', help_ordering: '99' } }
- assert_response 404
+
+ assert_response(:not_found)
end
test 'category post type rejects without category' do
sign_in users(:standard_user)
- post :create, params: { post_type: post_types(:question).id,
- post: { post_type_id: post_types(:question).id, title: sample.title,
- body_markdown: sample.body_markdown, tags_cache: sample.tags_cache } }
- assert_response 302
+
+ try_create_post(category: nil)
+
+ assert_response(:found)
assert_redirected_to root_path
assert_not_nil flash[:danger]
assert_nil assigns(:post).id
@@ -76,21 +76,20 @@ class PostsControllerTest < ActionController::TestCase
test 'category post type checks required trust level' do
sign_in users(:standard_user)
- post :create, params: { post_type: post_types(:question).id, category: categories(:high_trust).id,
- post: { post_type_id: post_types(:question).id, title: sample.title,
- body_markdown: sample.body_markdown, category_id: categories(:high_trust).id,
- tags_cache: sample.tags_cache } }
- assert_response 403
+
+ try_create_post(category: categories(:high_trust))
+
+ assert_response(:forbidden)
assert_nil assigns(:post).id
assert_not_empty assigns(:post).errors.full_messages
end
test 'parented post type rejects without parent' do
sign_in users(:standard_user)
- post :create, params: { post_type: post_types(:answer).id,
- post: { post_type_id: post_types(:answer).id, title: sample.title,
- body_markdown: sample.body_markdown } }
- assert_response 302
+
+ try_create_post(post_type: post_types(:answer))
+
+ assert_response(:found)
assert_redirected_to root_path
assert_not_nil flash[:danger]
assert_nil assigns(:post).id
@@ -101,32 +100,44 @@ class PostsControllerTest < ActionController::TestCase
before = CommunityUser.where(user: user, community: communities(:sample)).count
sign_in user
- post :create, params: { post_type: post_types(:question).id, category: categories(:main).id,
- post: { post_type_id: post_types(:question).id, title: sample.title,
- body_markdown: sample.body_markdown, category_id: categories(:main).id,
- tags_cache: sample.tags_cache } }
+ try_create_post
after = CommunityUser.where(user: user, community: communities(:sample)).count
assert_equal before + 1, after, 'No CommunityUser record was created'
end
- test 'should prevent deleted account creating post' do
+ test 'should prevent deleted accounts from creating posts' do
sign_in users(:deleted_account)
- post :create, params: { post_type: post_types(:question).id, category: categories(:main).id,
- post: { post_type_id: post_types(:question).id, title: sample.title,
- body_markdown: sample.body_markdown, category_id: categories(:main).id,
- tags_cache: sample.tags_cache } }
- assert_response 302
- assert_redirected_to new_user_session_path
+ try_create_post
+ assert_redirected_to_sign_in
end
- test 'should prevent deleted profile creating post' do
+ test 'should prevent deleted profiles from creating posts' do
sign_in users(:deleted_profile)
- post :create, params: { post_type: post_types(:question).id, category: categories(:main).id,
- post: { post_type_id: post_types(:question).id, title: sample.title,
- body_markdown: sample.body_markdown, category_id: categories(:main).id,
- tags_cache: sample.tags_cache } }
- assert_response 302
- assert_redirected_to new_user_session_path
+ try_create_post
+ assert_redirected_to_sign_in
+ end
+
+ private
+
+ # Attempts to create a post
+ # @param post_type [PostType]
+ # @param category [Category, nil]
+ # @param parent [Post, nil]
+ # @param license [String]
+ def try_create_post(post_type: post_types(:question),
+ category: categories(:main),
+ parent: nil,
+ license: licenses(:cc_by_sa))
+ post :create, params: { post_type: post_type.id,
+ parent: parent&.id,
+ category: category&.id,
+ post: { post_type_id: post_type.id,
+ title: sample.title,
+ body_markdown: sample.body_markdown,
+ category_id: category&.id,
+ parent_id: parent&.id,
+ tags_cache: sample.tags_cache,
+ license_id: license.id } }
end
end
diff --git a/test/controllers/posts/delete_test.rb b/test/controllers/posts/delete_test.rb
index 072289c50..9ec9144bc 100644
--- a/test/controllers/posts/delete_test.rb
+++ b/test/controllers/posts/delete_test.rb
@@ -5,10 +5,12 @@ class PostsControllerTest < ActionController::TestCase
test 'can delete post' do
sign_in users(:deleter)
+
before_history = PostHistory.where(post: posts(:question_two)).count
post :delete, params: { id: posts(:question_two).id }
after_history = PostHistory.where(post: posts(:question_two)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(assigns(:post))
assert_nil flash[:danger]
assert_equal before_history + 1, after_history, 'PostHistory event not created on deletion'
@@ -16,16 +18,17 @@ class PostsControllerTest < ActionController::TestCase
test 'delete requires authentication' do
post :delete, params: { id: posts(:question_one).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'unprivileged user cannot delete' do
sign_in users(:closer)
+
before_history = PostHistory.where(post: posts(:question_one)).count
post :delete, params: { id: posts(:question_one).id }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(assigns(:post))
assert_not_nil flash[:danger]
assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion'
@@ -33,10 +36,12 @@ class PostsControllerTest < ActionController::TestCase
test 'cannot delete a post with responses' do
sign_in users(:deleter)
+
before_history = PostHistory.where(post: posts(:question_one)).count
post :delete, params: { id: posts(:question_one).id }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(assigns(:post))
assert_not_nil flash[:danger]
assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion'
@@ -44,10 +49,12 @@ class PostsControllerTest < ActionController::TestCase
test 'cannot delete a deleted post' do
sign_in users(:deleter)
+
before_history = PostHistory.where(post: posts(:deleted)).count
post :delete, params: { id: posts(:deleted).id }
after_history = PostHistory.where(post: posts(:deleted)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(assigns(:post))
assert_not_nil flash[:danger]
assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion'
@@ -55,23 +62,73 @@ class PostsControllerTest < ActionController::TestCase
test 'cannot delete a locked post' do
sign_in users(:deleter)
+
before_history = PostHistory.where(post: posts(:locked)).count
post :delete, params: { id: posts(:locked).id }
after_history = PostHistory.where(post: posts(:locked)).count
- assert_response 403
+
+ assert_response(:forbidden)
assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion'
end
test 'delete ensures all children are deleted' do
- sign_in users(:deleter)
- before_history = PostHistory.where(post_id: posts(:bad_answers).children.map(&:id)).count
- post :delete, params: { id: posts(:bad_answers).id }
- after_history = PostHistory.where(post_id: posts(:bad_answers).children.map(&:id)).count
- assert_response 302
- assert_redirected_to post_path(assigns(:post))
+ parent = posts(:question_one)
+
+ sign_in users(:moderator)
+
+ assert_not_equal(0, parent.children.undeleted.count, 'Expected post to have undeleted children')
+
+ before_history = PostHistory.where(post_id: parent.children.map(&:id)).count
+ post :delete, params: { id: parent.id }
+ after_history = PostHistory.where(post_id: parent.children.map(&:id)).count
+
+ @post = assigns(:post)
+ @post.reload
+
+ assert_response(:found)
+ assert_redirected_to post_path(@post)
assert_nil flash[:danger]
- assert assigns(:post).children.all?(&:deleted), 'Answers not deleted with question'
- assert_equal before_history + posts(:bad_answers).children.count, after_history,
- 'Answer PostHistory events not created on question deletion'
+ assert @post.children.all?(&:deleted), 'Expected all post children to be deleted as well'
+ assert_equal before_history + parent.children.count, after_history,
+ 'Expected deletion history events to be created for every child'
+ end
+
+ test 'delete should be an atomic operation' do
+ parent = posts(:question_one)
+ user = users(:moderator)
+
+ sign_in user
+
+ assert_not_equal(0, parent.children.undeleted.count, 'Expected post to have undeleted children')
+
+ old_parent_events_count = PostHistory.where(post: parent).count
+ old_children_events_count = PostHistory.where(post: parent.children)
+
+ assert_delete_atomic = lambda do |check_flash: true|
+ post :delete, params: { id: parent.id }
+ parent.reload
+
+ if check_flash
+ assert_not_nil(flash[:danger])
+ end
+
+ assert_not_equal(0, parent.children.undeleted.count)
+ assert_equal old_parent_events_count, PostHistory.where(post: parent).count
+ assert_equal old_children_events_count, PostHistory.where(post: parent.children)
+ end
+
+ @controller.stub(:do_delete, false) { assert_delete_atomic.call }
+ @controller.stub(:do_delete_children, false) { assert_delete_atomic.call }
+
+ failed_history = PostHistory.new(
+ community: parent.community,
+ post: parent,
+ post_history_type: PostHistoryType.find_by(name: 'post_deleted'),
+ user: user
+ )
+
+ failed_history.errors.add(:test, 'this is only to make deletion fial')
+
+ PostHistory.stub(:post_deleted, failed_history) { assert_delete_atomic.call(check_flash: false) }
end
end
diff --git a/test/controllers/posts/drafts_test.rb b/test/controllers/posts/drafts_test.rb
index bc7c9603b..76ff65161 100644
--- a/test/controllers/posts/drafts_test.rb
+++ b/test/controllers/posts/drafts_test.rb
@@ -15,10 +15,9 @@ class PostsControllerTest < ActionController::TestCase
tags: ['tag1', 'tag2'],
title: 'test_title'
}
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:success)
+ assert_valid_json_response
base_key = JSON.parse(response.body)['key']
@@ -35,6 +34,6 @@ class PostsControllerTest < ActionController::TestCase
test 'can delete draft' do
sign_in users(:standard_user)
post :delete_draft, params: { path: 'test' }
- assert_response 200
+ assert_response(:success)
end
end
diff --git a/test/controllers/posts/edit_test.rb b/test/controllers/posts/edit_test.rb
index bc36d2f55..058b4a7b9 100644
--- a/test/controllers/posts/edit_test.rb
+++ b/test/controllers/posts/edit_test.rb
@@ -6,26 +6,25 @@ class PostsControllerTest < ActionController::TestCase
test 'can get edit' do
sign_in users(:standard_user)
get :edit, params: { id: posts(:question_one).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:post)
end
test 'edit requires authentication' do
get :edit, params: { id: posts(:question_one).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'cannot edit locked post' do
sign_in users(:standard_user)
get :edit, params: { id: posts(:locked).id }
- assert_response 403
+ assert_response(:forbidden)
end
test 'cannot edit non-public post without permissions' do
sign_in users(:standard_user)
get :edit, params: { id: posts(:blog_post).id }
- assert_response 302
+ assert_response(:found)
assert_redirected_to root_path
assert_not_nil flash[:danger]
end
@@ -33,14 +32,14 @@ class PostsControllerTest < ActionController::TestCase
test 'author can edit non-public post' do
sign_in users(:closer)
get :edit, params: { id: posts(:blog_post).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:post)
end
test 'moderator can edit non-public post' do
sign_in users(:moderator)
get :edit, params: { id: posts(:blog_post).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:post)
end
end
diff --git a/test/controllers/posts/feature_test.rb b/test/controllers/posts/feature_test.rb
index 6a649ebee..eeeaaed0a 100644
--- a/test/controllers/posts/feature_test.rb
+++ b/test/controllers/posts/feature_test.rb
@@ -7,7 +7,8 @@ class PostsControllerTest < ActionController::TestCase
sign_in users(:moderator)
before_audits = AuditLog.count
post :feature, params: { id: posts(:question_one).id }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
assert_not_nil assigns(:link).id
assert_equal before_audits + 1, AuditLog.count, 'AuditLog not created on post feature'
@@ -15,17 +16,15 @@ class PostsControllerTest < ActionController::TestCase
test 'feature requires authentication' do
post :feature, params: { id: posts(:question_one).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'regular user cannot feature' do
sign_in users(:deleter)
post :feature, params: { id: posts(:question_one).id, format: :json }
- assert_response 404
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:not_found)
+ assert_valid_json_response
assert_equal ['no_privilege'], JSON.parse(response.body)['errors']
end
end
diff --git a/test/controllers/posts/help_test.rb b/test/controllers/posts/help_test.rb
index db3e59f4d..151659826 100644
--- a/test/controllers/posts/help_test.rb
+++ b/test/controllers/posts/help_test.rb
@@ -5,40 +5,40 @@ class PostsControllerTest < ActionController::TestCase
test 'can get help center' do
get :help_center
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:posts)
end
test 'can get help article' do
get :document, params: { slug: posts(:help_article).doc_slug }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:post)
end
test 'moderator can get mod help article' do
sign_in users(:moderator)
get :document, params: { slug: posts(:mod_help_article).doc_slug }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:post)
end
test 'moderator help requires authentication' do
get :document, params: { slug: posts(:mod_help_article).doc_slug }
- assert_response 404
+ assert_response(:not_found)
assert_nil assigns(:post)
end
test 'regular user cannot get mod help' do
sign_in users(:standard_user)
get :document, params: { slug: posts(:mod_help_article).doc_slug }
- assert_response 404
+ assert_response(:not_found)
assert_nil assigns(:post)
end
test 'cannot get disabled help article' do
sign_in users(:moderator)
get :document, params: { slug: posts(:disabled_help_article).doc_slug }
- assert_response 404
+ assert_response(:not_found)
assert_nil assigns(:post)
end
end
diff --git a/test/controllers/posts/lock_test.rb b/test/controllers/posts/lock_test.rb
index cfb7b2ddc..c8cdfabe7 100644
--- a/test/controllers/posts/lock_test.rb
+++ b/test/controllers/posts/lock_test.rb
@@ -6,88 +6,80 @@ class PostsControllerTest < ActionController::TestCase
test 'can lock post' do
sign_in users(:deleter)
post :lock, params: { id: posts(:question_one).id, format: :json }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
assert assigns(:post).locked_until <= 7.days.from_now
assert assigns(:post).locked_until >= 7.days.from_now - 1.minute
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
test 'lock requires authentication' do
post :lock, params: { id: posts(:question_one).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'unprivileged user cannot lock' do
sign_in users(:standard_user)
post :lock, params: { id: posts(:question_one).id, format: :json }
- assert_response 404
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:not_found)
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'cannot lock locked post' do
sign_in users(:deleter)
post :lock, params: { id: posts(:locked).id, format: :json }
- assert_response 404
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:not_found)
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'cannot lock longer than 30 days' do
sign_in users(:deleter)
post :lock, params: { id: posts(:question_one).id, length: 60, format: :json }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
assert assigns(:post).locked_until <= 30.days.from_now
assert assigns(:post).locked_until >= 30.days.from_now - 1.minute
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
test 'moderator can lock longer than 30 days' do
sign_in users(:moderator)
post :lock, params: { id: posts(:question_one).id, length: 60, format: :json }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
assert assigns(:post).locked_until <= 60.days.from_now
assert assigns(:post).locked_until >= 60.days.from_now - 1.minute
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
test 'moderator can lock indefinitely' do
sign_in users(:moderator)
post :lock, params: { id: posts(:question_one).id, format: :json }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
assert_nil assigns(:post).locked_until
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
test 'Locks on posts expire' do
sign_in users(:moderator)
post :lock, params: { id: posts(:question_one).id, length: 1, format: :json }
- assert_response 200
+ assert_response(:success)
# Change the locked_until to have already passed
assigns(:post).update(locked_until: 1.second.ago)
-
assert_not assigns(:post).locked?
end
end
diff --git a/test/controllers/posts/new_test.rb b/test/controllers/posts/new_test.rb
index 4c02f2dce..24938df01 100644
--- a/test/controllers/posts/new_test.rb
+++ b/test/controllers/posts/new_test.rb
@@ -7,30 +7,32 @@ class PostsControllerTest < ActionController::TestCase
sign_in users(:moderator)
get :new, params: { post_type: post_types(:help_doc).id }
assert_nil flash[:danger]
- assert_response 200
+ assert_response(:success)
get :new, params: { post_type: post_types(:answer).id, parent: posts(:question_one).id }
assert_nil flash[:danger]
- assert_response 200
+ assert_response(:success)
get :new, params: { post_type: post_types(:question).id, category: categories(:main).id }
assert_nil flash[:danger]
- assert_response 200
+ assert_response(:success)
end
test 'new requires authentication' do
get :new, params: { post_type: post_types(:help_doc).id }
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
+
get :new, params: { post_type: post_types(:answer).id, parent: posts(:question_one).id }
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
+
get :new, params: { post_type: post_types(:question).id, category: categories(:main).id }
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'new rejects category post type without category' do
sign_in users(:standard_user)
get :new, params: { post_type: post_types(:question).id }
- assert_response 302
+ assert_response(:found)
assert_redirected_to root_path
assert_not_nil flash[:danger]
end
@@ -38,7 +40,7 @@ class PostsControllerTest < ActionController::TestCase
test 'new rejects parented post type without parent' do
sign_in users(:standard_user)
get :new, params: { post_type: post_types(:answer).id }
- assert_response 302
+ assert_response(:found)
assert_redirected_to root_path
assert_not_nil flash[:danger]
end
diff --git a/test/controllers/posts/reopen_test.rb b/test/controllers/posts/reopen_test.rb
index 9f574644b..d9985f6f2 100644
--- a/test/controllers/posts/reopen_test.rb
+++ b/test/controllers/posts/reopen_test.rb
@@ -8,7 +8,8 @@ class PostsControllerTest < ActionController::TestCase
before_history = PostHistory.where(post: posts(:closed)).count
post :reopen, params: { id: posts(:closed).id }
after_history = PostHistory.where(post: posts(:closed)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(posts(:closed))
assert_nil flash[:danger]
assert_equal before_history + 1, after_history, 'PostHistory event not created on reopen'
@@ -16,8 +17,7 @@ class PostsControllerTest < ActionController::TestCase
test 'reopen requires authentication' do
post :reopen, params: { id: posts(:closed).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'unprivileged user cannot reopen' do
@@ -25,7 +25,8 @@ class PostsControllerTest < ActionController::TestCase
before_history = PostHistory.where(post: posts(:closed)).count
post :reopen, params: { id: posts(:closed).id }
after_history = PostHistory.where(post: posts(:closed)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(posts(:closed))
assert_not_nil flash[:danger]
assert_equal before_history, after_history, 'PostHistory event incorrectly created on reopen'
@@ -36,7 +37,8 @@ class PostsControllerTest < ActionController::TestCase
before_history = PostHistory.where(post: posts(:question_one)).count
post :reopen, params: { id: posts(:question_one).id }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(posts(:question_one))
assert_not_nil flash[:danger]
assert_equal before_history, after_history, 'PostHistory event incorrectly created on reopen'
@@ -47,7 +49,8 @@ class PostsControllerTest < ActionController::TestCase
before_history = PostHistory.where(post: posts(:locked)).count
post :reopen, params: { id: posts(:locked).id }
after_history = PostHistory.where(post: posts(:locked)).count
- assert_response 403
+
+ assert_response(:forbidden)
assert_equal before_history, after_history, 'PostHistory event incorrectly created on reopen'
end
end
diff --git a/test/controllers/posts/restore_test.rb b/test/controllers/posts/restore_test.rb
index 4471e8d89..d77d7c3de 100644
--- a/test/controllers/posts/restore_test.rb
+++ b/test/controllers/posts/restore_test.rb
@@ -5,10 +5,12 @@ class PostsControllerTest < ActionController::TestCase
test 'can restore post' do
sign_in users(:deleter)
+
before_history = PostHistory.where(post: posts(:deleted)).count
post :restore, params: { id: posts(:deleted).id }
after_history = PostHistory.where(post: posts(:deleted)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(assigns(:post))
assert_nil flash[:danger]
assert_equal before_history + 1, after_history, 'PostHistory event not created on deletion'
@@ -16,16 +18,17 @@ class PostsControllerTest < ActionController::TestCase
test 'restore requires authentication' do
post :restore, params: { id: posts(:deleted).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'unprivileged user cannot restore' do
sign_in users(:closer)
+
before_history = PostHistory.where(post: posts(:deleted)).count
post :restore, params: { id: posts(:deleted).id }
after_history = PostHistory.where(post: posts(:deleted)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(assigns(:post))
assert_not_nil flash[:danger]
assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion'
@@ -33,10 +36,12 @@ class PostsControllerTest < ActionController::TestCase
test 'cannot restore a post deleted by a moderator' do
sign_in users(:deleter)
+
before_history = PostHistory.where(post: posts(:deleted_mod)).count
post :restore, params: { id: posts(:deleted_mod).id }
after_history = PostHistory.where(post: posts(:deleted_mod)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(assigns(:post))
assert_not_nil flash[:danger]
assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion'
@@ -44,10 +49,12 @@ class PostsControllerTest < ActionController::TestCase
test 'cannot restore a restored post' do
sign_in users(:deleter)
+
before_history = PostHistory.where(post: posts(:question_one)).count
post :restore, params: { id: posts(:question_one).id }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(assigns(:post))
assert_not_nil flash[:danger]
assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion'
@@ -55,22 +62,26 @@ class PostsControllerTest < ActionController::TestCase
test 'cannot restore a locked post' do
sign_in users(:deleter)
+
before_history = PostHistory.where(post: posts(:locked)).count
post :restore, params: { id: posts(:locked).id }
after_history = PostHistory.where(post: posts(:locked)).count
- assert_response 403
+
+ assert_response(:forbidden)
assert_equal before_history, after_history, 'PostHistory event incorrectly created on deletion'
end
test 'restore brings back all answers deleted after question' do
sign_in users(:deleter)
+
deleted_at = posts(:deleted).deleted_at
children = posts(:deleted).children.where('deleted_at >= ?', deleted_at)
children_count = children.count
before_history = PostHistory.where(post_id: children.where('deleted_at >= ?', deleted_at)).count
post :restore, params: { id: posts(:deleted).id }
after_history = PostHistory.where(post_id: children.where('deleted_at >= ?', deleted_at)).count
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to post_path(assigns(:post))
assert_nil flash[:danger]
assert_equal before_history + children_count, after_history,
diff --git a/test/controllers/posts/show_test.rb b/test/controllers/posts/show_test.rb
index b3f894b53..db18e7113 100644
--- a/test/controllers/posts/show_test.rb
+++ b/test/controllers/posts/show_test.rb
@@ -5,7 +5,7 @@ class PostsControllerTest < ActionController::TestCase
test 'anonymous user can get show' do
get :show, params: { id: posts(:question_one).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:post)
assert_not_nil assigns(:children)
assert_not assigns(:children).any?(&:deleted), 'Anonymous user can see deleted answers'
@@ -14,7 +14,7 @@ class PostsControllerTest < ActionController::TestCase
test 'standard user can get show' do
sign_in users(:standard_user)
get :show, params: { id: posts(:question_one).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:post)
assert_not_nil assigns(:children)
assert_not assigns(:children).any?(&:deleted), 'Anonymous user can see deleted answers'
@@ -23,7 +23,7 @@ class PostsControllerTest < ActionController::TestCase
test 'privileged user can see deleted post' do
sign_in users(:deleter)
get :show, params: { id: posts(:deleted).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:post)
assert_not_nil assigns(:children)
end
@@ -31,7 +31,7 @@ class PostsControllerTest < ActionController::TestCase
test 'privileged user can see deleted answers' do
sign_in users(:deleter)
get :show, params: { id: posts(:question_one).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:post)
assert_not_nil assigns(:children)
assert assigns(:children).any?(&:deleted), 'Privileged user cannot see deleted answers'
@@ -39,7 +39,7 @@ class PostsControllerTest < ActionController::TestCase
test 'show redirects parented to parent post' do
get :show, params: { id: posts(:answer_one).id }
- assert_response 302
+ assert_response(:found)
assert_redirected_to answer_post_path(posts(:answer_one).parent_id, answer: posts(:answer_one).id,
anchor: "answer-#{posts(:answer_one).id}")
end
@@ -47,6 +47,6 @@ class PostsControllerTest < ActionController::TestCase
test 'unprivileged user cannot see post in high trust level category' do
sign_in users(:standard_user)
get :show, params: { id: posts(:high_trust).id }
- assert_response 404
+ assert_response(:not_found)
end
end
diff --git a/test/controllers/posts/toggle_comments_test.rb b/test/controllers/posts/toggle_comments_test.rb
index 78601733b..ca4e3e911 100644
--- a/test/controllers/posts/toggle_comments_test.rb
+++ b/test/controllers/posts/toggle_comments_test.rb
@@ -6,25 +6,24 @@ class PostsControllerTest < ActionController::TestCase
test 'can toggle comments' do
sign_in users(:moderator)
post :toggle_comments, params: { id: posts(:question_one).id }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
assert assigns(:post).comments_disabled
end
test 'toggle comments requires authentication' do
post :toggle_comments, params: { id: posts(:question_one).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'regular users cannot toggle comments' do
sign_in users(:standard_user)
post :toggle_comments, params: { id: posts(:question_one).id }
- assert_response 404
+
+ assert_response(:not_found)
assert_not_nil assigns(:post)
assert_not assigns(:post).comments_disabled
end
@@ -32,11 +31,10 @@ class PostsControllerTest < ActionController::TestCase
test 'specifying delete all results in comments being deleted' do
sign_in users(:moderator)
post :toggle_comments, params: { id: posts(:question_one).id, delete_all_comments: true }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
assert assigns(:post).comments_disabled
assert assigns(:post).comments.all?(&:deleted?)
diff --git a/test/controllers/posts/unlock_test.rb b/test/controllers/posts/unlock_test.rb
index 087700791..68139b483 100644
--- a/test/controllers/posts/unlock_test.rb
+++ b/test/controllers/posts/unlock_test.rb
@@ -7,37 +7,33 @@ class PostsControllerTest < ActionController::TestCase
sign_in users(:deleter)
posts(:locked).update(locked_until: 2.days.from_now)
post :unlock, params: { id: posts(:locked).id, format: :json }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:post)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
test 'unlock requires authentication' do
post :unlock, params: { id: posts(:locked).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'unprivileged user cannot unlock' do
sign_in users(:standard_user)
post :unlock, params: { id: posts(:locked).id, format: :json }
- assert_response 404
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:not_found)
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'cannot unlock unlocked post' do
sign_in users(:deleter)
post :unlock, params: { id: posts(:question_one).id, format: :json }
- assert_response 404
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:not_found)
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
@@ -45,10 +41,9 @@ class PostsControllerTest < ActionController::TestCase
sign_in users(:deleter)
posts(:locked_mod).update(locked_until: 2.days.from_now)
post :unlock, params: { id: posts(:locked_mod).id, format: :json }
- assert_response 404
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:not_found)
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
assert_equal ['locked_by_mod'], JSON.parse(response.body)['errors']
end
diff --git a/test/controllers/posts/update_test.rb b/test/controllers/posts/update_test.rb
index 247de58ce..669ff4e30 100644
--- a/test/controllers/posts/update_test.rb
+++ b/test/controllers/posts/update_test.rb
@@ -10,7 +10,7 @@ class PostsControllerTest < ActionController::TestCase
post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown,
tags_cache: sample.edit.tags_cache } }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 302
+ assert_response(:found)
assert_redirected_to post_path(posts(:question_one))
assert_not_nil assigns(:post)
assert_equal sample.edit.body_markdown, assigns(:post).body_markdown
@@ -24,7 +24,7 @@ class PostsControllerTest < ActionController::TestCase
post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown,
tags_cache: sample.edit.tags_cache } }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 302
+ assert_response(:found)
assert_redirected_to post_path(posts(:question_one))
assert_not_nil assigns(:post)
assert_equal sample.edit.body_markdown, assigns(:post).body_markdown
@@ -35,8 +35,8 @@ class PostsControllerTest < ActionController::TestCase
patch :update, params: { id: posts(:question_one).id,
post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown,
tags_cache: sample.edit.tags_cache } }
- assert_response 302
- assert_redirected_to new_user_session_path
+
+ assert_redirected_to_sign_in
end
test 'update by unprivileged user generates suggested edit' do
@@ -49,7 +49,7 @@ class PostsControllerTest < ActionController::TestCase
tags_cache: sample.edit.tags_cache } }
after_history = PostHistory.where(post: posts(:question_one)).count
after_edits = SuggestedEdit.where(post: posts(:question_one)).count
- assert_response 302
+ assert_response(:found)
assert_redirected_to post_path(posts(:question_one))
assert_not_nil assigns(:post)
assert_equal before_body, assigns(:post).body_markdown, 'Suggested edit incorrectly applied immediately'
@@ -65,7 +65,7 @@ class PostsControllerTest < ActionController::TestCase
post: { title: post.title, body_markdown: post.body_markdown,
tags_cache: post.tags_cache } }
after_history = PostHistory.where(post: posts(:question_one)).count
- assert_response 302
+ assert_response(:found)
assert_redirected_to post_path(posts(:question_one))
assert_not_nil assigns(:post)
assert_not_nil flash[:danger]
@@ -79,7 +79,7 @@ class PostsControllerTest < ActionController::TestCase
post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown,
tags_cache: sample.edit.tags_cache } }
after_history = PostHistory.where(post: posts(:locked)).count
- assert_response 403
+ assert_response(:forbidden)
assert_equal before_history, after_history, 'PostHistory event incorrectly created on update'
end
@@ -90,7 +90,7 @@ class PostsControllerTest < ActionController::TestCase
post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown,
tags_cache: sample.edit.tags_cache } }
after_history = PostHistory.where(post: posts(:free_edit)).count
- assert_response 302
+ assert_response(:found)
assert_redirected_to post_path(posts(:free_edit))
assert_not_nil assigns(:post)
assert_equal sample.edit.body_markdown, assigns(:post).body_markdown
diff --git a/test/controllers/reactions_controller_test.rb b/test/controllers/reactions_controller_test.rb
index beef081a4..2422f9d1e 100644
--- a/test/controllers/reactions_controller_test.rb
+++ b/test/controllers/reactions_controller_test.rb
@@ -6,8 +6,7 @@ class ReactionsControllerTest < ActionController::TestCase
test 'add should require sign in' do
post :add, params: { reaction_id: reaction_types(:wfm).id, comment: nil, post_id: posts(:answer_two) }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'add should fail if no post id given' do
@@ -17,6 +16,12 @@ class ReactionsControllerTest < ActionController::TestCase
end
end
+ test 'add should fail if post is not accessible to user' do
+ sign_in users(:standard_user)
+ post :add, params: { reaction_id: reaction_types(:wfm).id, comment: nil, post_id: posts(:high_trust) }
+ assert_response(:not_found)
+ end
+
test 'add should fail if no reaction id given' do
sign_in users(:standard_user)
assert_raise ActiveRecord::RecordNotFound do
@@ -29,7 +34,7 @@ class ReactionsControllerTest < ActionController::TestCase
post :add, params: { reaction_id: reaction_types(:wfm).id, comment: nil, post_id: posts(:answer_two) }
assert_not_nil assigns(:post)
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response 200
+ assert_response(:success)
end
test 'add should fail if reaction type requires comment but none provided' do
@@ -37,7 +42,7 @@ class ReactionsControllerTest < ActionController::TestCase
post :add, params: { reaction_id: reaction_types(:bad).id, comment: nil, post_id: posts(:answer_two) }
assert_not_nil assigns(:post)
assert_equal 'failed', JSON.parse(response.body)['status']
- assert_response 403
+ assert_response(:forbidden)
end
test 'add should pass if reaction type requires comment and one provided' do
@@ -45,61 +50,63 @@ class ReactionsControllerTest < ActionController::TestCase
post :add, params: { reaction_id: reaction_types(:bad).id, comment: 'A' * 50, post_id: posts(:answer_two) }
assert_not_nil assigns(:post)
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response 200
+ assert_response(:success)
end
test 'add should pass if reaction type requires no comment but one provided' do
sign_in users(:standard_user)
post :add, params: { reaction_id: reaction_types(:old).id, comment: 'A' * 50, post_id: posts(:answer_two) }
+
assert_not_nil assigns(:post)
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response 200
+ assert_response(:success)
end
test 'add should allow adding second reaction of other type' do
sign_in users(:standard_user)
post :add, params: { reaction_id: reaction_types(:old).id, comment: nil, post_id: posts(:answer_one) }
+
assert_not_nil assigns(:post)
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response 200
+ assert_response(:success)
end
test 'add should prevent adding second reaction of same type' do
sign_in users(:standard_user)
post :add, params: { reaction_id: reaction_types(:wfm).id, comment: nil, post_id: posts(:answer_one) }
+
assert_not_nil assigns(:post)
assert_equal 'failed', JSON.parse(response.body)['status']
- assert_response 403
+ assert_response(:forbidden)
end
test 'add should fail if unprivileged user attempts to comment' do
sign_in users(:standard_user)
- posts(:answer_two).update comments_disabled: true
+ posts(:answer_two).update(comments_disabled: true)
post :add, params: { reaction_id: reaction_types(:wfm).id, comment: 'A' * 50, post_id: posts(:answer_two) }
assert_not_nil assigns(:post)
assert_equal 'failed', JSON.parse(response.body)['status']
- assert_response 403
+ assert_response(:forbidden)
- posts(:answer_two).update comments_disabled: false
+ posts(:answer_two).update(comments_disabled: false)
end
test 'add should pass if privileged user attempts to comment' do
sign_in users(:admin)
- posts(:answer_two).update comments_disabled: true
+ posts(:answer_two).update(comments_disabled: true)
post :add, params: { reaction_id: reaction_types(:wfm).id, comment: 'A' * 50, post_id: posts(:answer_two) }
assert_not_nil assigns(:post)
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response 200
+ assert_response(:success)
- posts(:answer_two).update comments_disabled: false
+ posts(:answer_two).update(comments_disabled: false)
end
test 'retract should require sign in' do
post :retract, params: { reaction_id: reaction_types(:wfm).id, post_id: posts(:answer_two) }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'retract should fail if no post id given' do
@@ -119,95 +126,93 @@ class ReactionsControllerTest < ActionController::TestCase
test 'retract should fail if no active reaction' do
sign_in users(:standard_user)
post :retract, params: { reaction_id: reaction_types(:wfm).id, post_id: posts(:answer_two) }
- assert_response 403
+ assert_response(:forbidden)
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'retract should pass if active reaction' do
sign_in users(:standard_user)
post :retract, params: { reaction_id: reaction_types(:wfm).id, post_id: posts(:answer_one) }
- assert_response 200
+ assert_response(:success)
assert_equal 'success', JSON.parse(response.body)['status']
end
test 'retract should fail if no active reaction of same type' do
sign_in users(:standard_user)
post :retract, params: { reaction_id: reaction_types(:bad).id, post_id: posts(:answer_one) }
- assert_response 403
+ assert_response(:forbidden)
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'index should fail for signed-out' do
get :index
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'index should fail for standard users' do
sign_in users(:standard_user)
get :index
- assert_response 404
+ assert_response(:not_found)
end
test 'index should fail for privileged standard users' do
sign_in users(:closer)
get :index
- assert_response 404
+ assert_response(:not_found)
end
test 'index should show for moderators' do
sign_in users(:moderator)
get :index
- assert_response 200
+ assert_response(:success)
end
test 'edit should fail for signed-out' do
get :edit, params: { id: reaction_types(:wfm).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'edit should fail for standard users' do
sign_in users(:standard_user)
get :edit, params: { id: reaction_types(:wfm).id }
- assert_response 404
+ assert_response(:not_found)
end
test 'edit should fail for privileged standard users' do
sign_in users(:closer)
get :edit, params: { id: reaction_types(:wfm).id }
- assert_response 404
+ assert_response(:not_found)
end
test 'edit should show for moderators' do
sign_in users(:moderator)
get :edit, params: { id: reaction_types(:wfm).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:reaction_type)
end
test 'update should fail for signed-out' do
patch :update, params: { id: reaction_types(:wfm).id, reaction_type: { name: 'WORKZ', active: false } }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'update should fail for standard users' do
sign_in users(:standard_user)
patch :update, params: { id: reaction_types(:wfm).id, reaction_type: { name: 'WORKZ', active: false } }
- assert_response 404
+ assert_response(:not_found)
end
test 'update should fail for privileged standard users' do
sign_in users(:closer)
patch :update, params: { id: reaction_types(:wfm).id, reaction_type: { name: 'WORKZ', active: false } }
- assert_response 404
+ assert_response(:not_found)
end
test 'update should pass for moderators' do
sign_in users(:moderator)
patch :update, params: { id: reaction_types(:wfm).id, reaction_type: { name: 'WORKZ', active: false } }
- assert_response 302
+
+ assert_response(:found)
assert_redirected_to reactions_path
assert_not_nil assigns(:reaction_type)
assert_equal false, assigns(:reaction_type).active
@@ -216,26 +221,25 @@ class ReactionsControllerTest < ActionController::TestCase
test 'new should fail for signed-out' do
get :new
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'new should fail for standard users' do
sign_in users(:standard_user)
get :new
- assert_response 404
+ assert_response(:not_found)
end
test 'new should fail for privileged standard users' do
sign_in users(:closer)
get :new
- assert_response 404
+ assert_response(:not_found)
end
test 'new should show for moderators' do
sign_in users(:moderator)
get :new
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:reaction_type)
end
@@ -244,8 +248,7 @@ class ReactionsControllerTest < ActionController::TestCase
icon: 'aaaaaah-icon', color: 'is-deeppurple is-orangegreen', requires_comment: false,
position: 42 }
post :create, params: { id: reaction_types(:wfm).id, reaction_type: data }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'create should fail for standard users' do
@@ -254,7 +257,7 @@ class ReactionsControllerTest < ActionController::TestCase
icon: 'aaaaaah-icon', color: 'is-deeppurple is-orangegreen', requires_comment: false,
position: 42 }
post :create, params: { id: reaction_types(:wfm).id, reaction_type: data }
- assert_response 404
+ assert_response(:not_found)
end
test 'create should fail for privileged standard users' do
@@ -263,7 +266,7 @@ class ReactionsControllerTest < ActionController::TestCase
icon: 'aaaaaah-icon', color: 'is-deeppurple is-orangegreen', requires_comment: false,
position: 42 }
post :create, params: { id: reaction_types(:wfm).id, reaction_type: data }
- assert_response 404
+ assert_response(:not_found)
end
test 'create should pass for moderators' do
@@ -272,7 +275,7 @@ class ReactionsControllerTest < ActionController::TestCase
icon: 'aaaaaah-icon', color: 'is-deeppurple is-orangegreen', requires_comment: false,
position: 42 }
post :create, params: { id: reaction_types(:wfm).id, reaction_type: data }
- assert_response 302
+ assert_response(:found)
assert_redirected_to reactions_path
end
end
diff --git a/test/controllers/reports_controller_test.rb b/test/controllers/reports_controller_test.rb
index 73949150c..edea291a6 100644
--- a/test/controllers/reports_controller_test.rb
+++ b/test/controllers/reports_controller_test.rb
@@ -6,8 +6,7 @@ class ReportsControllerTest < ActionController::TestCase
test 'should deny access to anonymous users' do
[:users, :posts, :subscriptions].each do |route|
get route
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
end
@@ -15,7 +14,7 @@ class ReportsControllerTest < ActionController::TestCase
sign_in users(:standard_user)
[:users, :posts, :subscriptions].each do |route|
get route
- assert_response 404
+ assert_response(:not_found)
end
end
@@ -23,7 +22,15 @@ class ReportsControllerTest < ActionController::TestCase
sign_in users(:moderator)
[:users, :posts, :subscriptions].each do |route|
get route
- assert_response 200
+ assert_response(:success)
+ end
+ end
+
+ test 'every global route should work for global moderators & admins' do
+ sign_in users(:global_admin)
+ [:users_global, :subs_global, :posts_global].each do |route|
+ get route
+ assert_response(:success)
end
end
end
diff --git a/test/controllers/search_controller_test.rb b/test/controllers/search_controller_test.rb
index 9beba194c..8eb453891 100644
--- a/test/controllers/search_controller_test.rb
+++ b/test/controllers/search_controller_test.rb
@@ -5,20 +5,20 @@ class SearchControllerTest < ActionController::TestCase
test 'get without a search term should result in all posts' do
get :search
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:posts)
end
test 'get with a search term should have results' do
get :search, params: { search: 'ABCDEF' }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:posts)
end
test 'all search orders should work' do
['relevance', 'score', 'age'].each do |so|
get :search, params: { search: 'ABCDEF', sort: so }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:posts)
end
end
@@ -26,7 +26,7 @@ class SearchControllerTest < ActionController::TestCase
test 'undefined search order should not error' do
assert_nothing_raised do
get :search, params: { search: 'ABCDEF', sort: 'abcdef' }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:posts)
end
end
@@ -34,7 +34,7 @@ class SearchControllerTest < ActionController::TestCase
test 'search with qualifiers should work' do
assert_nothing_raised do
get :search, params: { search: 'score:>=1 created:<1y abcdef' }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:posts)
end
end
diff --git a/test/controllers/site_settings_controller_test.rb b/test/controllers/site_settings_controller_test.rb
index cdb29e184..280158ca0 100644
--- a/test/controllers/site_settings_controller_test.rb
+++ b/test/controllers/site_settings_controller_test.rb
@@ -6,14 +6,16 @@ class SiteSettingsControllerTest < ActionController::TestCase
test 'should get index page' do
sign_in users(:admin)
get :index
+
assert_not_nil assigns(:settings)
- assert_response(200)
+ assert_response(:success)
end
test 'should update existing setting' do
sign_in users(:admin)
post :update, params: { community_id: RequestContext.community_id, name: site_settings(:one).name, site_setting: { value: 'ABCDEF' } }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:setting)
assert_equal 'ABCDEF', JSON.parse(response.body)['setting']['value']
assert_equal 'OK', JSON.parse(response.body)['status']
@@ -22,42 +24,42 @@ class SiteSettingsControllerTest < ActionController::TestCase
test 'should require authentication to access index' do
sign_out :user
get :index
- assert_response(404)
+ assert_response(:not_found)
end
test 'should require admin status to access index' do
sign_in users(:moderator)
get :index
- assert_response(404)
+ assert_response(:not_found)
end
test 'should require global admin to access global settings' do
sign_in users(:global_admin)
get :global
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:settings)
end
test 'should deny global access to local admins' do
sign_in users(:admin)
get :global
- assert_response 404
+ assert_response(:not_found)
end
test 'should allow global admin to update global setting' do
sign_in users(:global_admin)
post :update, params: { community_id: nil, name: site_settings(:one).name, site_setting: { value: 2 } }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:success)
+ assert_valid_json_response
assert_equal 'OK', JSON.parse(response.body)['status']
end
test 'should prevent local admin updating global setting' do
sign_in users(:admin)
post :update, params: { community_id: nil, name: site_settings(:one).name, site_setting: { value: 2 } }
- assert_response 404
+
+ assert_response(:not_found)
end
test 'editing site setting should leave global alone' do
@@ -65,10 +67,9 @@ class SiteSettingsControllerTest < ActionController::TestCase
pre_value = site_settings(:one).value
pre_count = SiteSetting.unscoped.count
post :update, params: { community_id: RequestContext.community_id, name: site_settings(:one).name, site_setting: { value: 'ABC' } }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:success)
+ assert_valid_json_response
assert_equal 'OK', JSON.parse(response.body)['status']
assert_equal pre_value, site_settings(:one).value
assert_equal pre_count + 1, SiteSetting.unscoped.count
diff --git a/test/controllers/subscriptions_controller_test.rb b/test/controllers/subscriptions_controller_test.rb
index 898730b31..6d582119f 100644
--- a/test/controllers/subscriptions_controller_test.rb
+++ b/test/controllers/subscriptions_controller_test.rb
@@ -5,20 +5,18 @@ class SubscriptionsControllerTest < ActionController::TestCase
test 'should require authentication to access new' do
get :new, params: { type: 'all' }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'should require authentication to access index' do
get :index
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'should get index when logged in' do
sign_in users(:standard_user)
get :index
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:subscriptions)
assert_not assigns(:subscriptions).empty?,
'@subscriptions instance variable expected size > 0, got <= 0'
@@ -27,7 +25,7 @@ class SubscriptionsControllerTest < ActionController::TestCase
test 'should get new when logged in' do
sign_in users(:standard_user)
get :new, params: { type: 'all' }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:phrasing)
assert_not_nil assigns(:subscription)
end
@@ -36,7 +34,7 @@ class SubscriptionsControllerTest < ActionController::TestCase
sign_in users(:standard_user)
post :create, params: { return_to: user_path(users(:moderator)),
subscription: { type: 'user', qualifier: users(:moderator).id, name: 'test', frequency: 7 } }
- assert_response 302
+ assert_response(:found)
assert_not_nil assigns(:subscription)
assert_not_nil flash[:success]
assert_redirected_to user_path(users(:moderator))
@@ -45,7 +43,8 @@ class SubscriptionsControllerTest < ActionController::TestCase
test 'should refuse to create tag subscription to nonexistent tag' do
sign_in users(:standard_user)
post :create, params: { subscription: { type: 'tag', qualifier: 'nope', name: 'test', frequency: 7 } }
- assert_response 500
+
+ assert_response(:internal_server_error)
assert_not_nil assigns(:subscription)
assert assigns(:subscription).errors.any?,
'@subscription instance variable has no errors attached but failed to save'
@@ -54,33 +53,30 @@ class SubscriptionsControllerTest < ActionController::TestCase
test 'should prevent users updating subscriptions belonging to others' do
sign_in users(:editor)
post :enable, params: { id: subscriptions(:all).id, enabled: true }
- assert_response 403
+
+ assert_response(:forbidden)
assert_not_nil assigns(:subscription)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'should prevent users removing subscriptions belonging to others' do
sign_in users(:editor)
post :destroy, params: { id: subscriptions(:all).id }
- assert_response 403
+
+ assert_response(:forbidden)
assert_not_nil assigns(:subscription)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'failed', JSON.parse(response.body)['status']
end
test 'should allow users to update their own subscriptions' do
sign_in users(:standard_user)
post :enable, params: { id: subscriptions(:all).id } # no enabled param should default to false
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:subscription)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
assert_equal false, assigns(:subscription).enabled
end
@@ -88,22 +84,20 @@ class SubscriptionsControllerTest < ActionController::TestCase
test 'should allow users to remove their own subscriptions' do
sign_in users(:standard_user)
post :destroy, params: { id: subscriptions(:all).id }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:subscription)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
test 'should allow admins to update others subscriptions' do
sign_in users(:admin)
post :enable, params: { id: subscriptions(:all).id } # no enabled param should default to false
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:subscription)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
assert_equal false, assigns(:subscription).enabled
end
@@ -111,11 +105,10 @@ class SubscriptionsControllerTest < ActionController::TestCase
test 'should allow admins to remove others subscriptions' do
sign_in users(:admin)
post :destroy, params: { id: subscriptions(:all).id }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:subscription)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
end
diff --git a/test/controllers/sudo_controller_test.rb b/test/controllers/sudo_controller_test.rb
new file mode 100644
index 000000000..4ad9f95f3
--- /dev/null
+++ b/test/controllers/sudo_controller_test.rb
@@ -0,0 +1,53 @@
+require 'test_helper'
+
+class SudoControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+
+ test 'should require auth before sudo mode' do
+ get :sudo
+ assert_response(:found)
+ assert_redirected_to new_user_session_path
+ end
+
+ test 'should show sudo mode page' do
+ sign_in users(:standard_user)
+ get :sudo
+ assert_response(:success)
+ end
+
+ test 'should fail sudo mode with wrong password' do
+ sign_in users(:standard_user)
+ try_enter_sudo('wrong')
+
+ assert_response(:success)
+ assert_equal 'The password you entered was incorrect.', flash[:danger]
+ end
+
+ test 'should enter sudo mode' do
+ set_password(users(:standard_user), 'test1234')
+ sign_in users(:standard_user)
+ session[:sudo_return] = users_me_path
+ try_enter_sudo('test1234')
+
+ assert_response(:found)
+ assert_redirected_to users_me_path
+ assert_not_nil session[:sudo]
+ assert_nothing_raised do
+ DateTime.iso8601(session[:sudo])
+ end
+ end
+
+ private
+
+ # Attempts to enter sudo mode for the current user
+ # @param password [String] password of the user entering sudo mode
+ def try_enter_sudo(password)
+ post :enter_sudo, params: { password: password }
+ end
+
+ def set_password(user, password)
+ user.password = password
+ user.skip_reconfirmation!
+ user.save!
+ end
+end
diff --git a/test/controllers/suggested_edit_controller_test.rb b/test/controllers/suggested_edit_controller_test.rb
index c19a1b957..cec0b14df 100644
--- a/test/controllers/suggested_edit_controller_test.rb
+++ b/test/controllers/suggested_edit_controller_test.rb
@@ -5,14 +5,14 @@ class SuggestedEditControllerTest < ActionController::TestCase
test 'should get page with all pending edits' do
get :category_index, params: { category: categories(:main).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:category)
assert_not_nil assigns(:edits)
end
test 'should get page with all decided edits' do
get :category_index, params: { category: categories(:main).id, show_decided: 1 }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:category)
assert_not_nil assigns(:edits)
end
@@ -21,7 +21,7 @@ class SuggestedEditControllerTest < ActionController::TestCase
get :show, params: { id: suggested_edits(:pending_suggested_edit).id }
assert_not_nil assigns(:edit)
assert_equal assigns(:edit).active, true
- assert_response(200)
+ assert_response(:success)
end
test 'should get approved suggested edit page' do
@@ -29,7 +29,7 @@ class SuggestedEditControllerTest < ActionController::TestCase
assert_not_nil assigns(:edit)
assert_equal assigns(:edit).active, false
assert_equal assigns(:edit).accepted, true
- assert_response(200)
+ assert_response(:success)
end
test 'should get rejected suggested edit page' do
@@ -37,7 +37,7 @@ class SuggestedEditControllerTest < ActionController::TestCase
assert_not_nil assigns(:edit)
assert_equal assigns(:edit).active, false
assert_equal assigns(:edit).accepted, false
- assert_response(200)
+ assert_response(:success)
end
test 'signed-out shouldn\'t be able to approve' do
@@ -45,7 +45,7 @@ class SuggestedEditControllerTest < ActionController::TestCase
suggested_edit.update(active: true, accepted: false)
post :approve, params: { id: suggested_edit.id }
- assert_response(400)
+ assert_response(:forbidden)
end
test 'signed-out shouldn\'t be able to reject' do
@@ -53,7 +53,37 @@ class SuggestedEditControllerTest < ActionController::TestCase
suggested_edit.update(active: true, accepted: false)
post :reject, params: { id: suggested_edit.id, rejection_comment: 'WHY NOT?' }
- assert_response(400)
+ assert_response(:forbidden)
+ end
+
+ test 'users without the ability to edit posts shouldn\'t be able to approve' do
+ sign_in users(:moderator)
+
+ edit = suggested_edits(:pending_high_trust)
+
+ post :approve, params: { id: edit.id, format: 'json' }
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+
+ res_body = JSON.parse(response.body)
+ assert_equal 'error', res_body['status']
+ assert_not_empty res_body['message']
+ end
+
+ test 'users without the ability to edit posts shouldn\'t be able to reject' do
+ sign_in users(:moderator)
+
+ edit = suggested_edits(:pending_high_trust)
+
+ post :reject, params: { id: edit.id, format: 'json' }
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+
+ res_body = JSON.parse(response.body)
+ assert_equal 'error', res_body['status']
+ assert_not_empty res_body['message']
end
test 'already decided edit shouldn\'t be able to be approved' do
@@ -63,7 +93,7 @@ class SuggestedEditControllerTest < ActionController::TestCase
suggested_edit.update(active: false, accepted: false)
post :approve, params: { id: suggested_edit.id }
- assert_response(409)
+ assert_response(:conflict)
end
test 'already decided edit shouldn\'t be able to be rejected' do
@@ -73,7 +103,7 @@ class SuggestedEditControllerTest < ActionController::TestCase
suggested_edit.update(active: false, accepted: true)
post :reject, params: { id: suggested_edit.id, rejection_comment: 'WHY NOT?' }
- assert_response(409)
+ assert_response(:conflict)
end
test 'approving edit should change status and apply it' do
@@ -85,7 +115,7 @@ class SuggestedEditControllerTest < ActionController::TestCase
post :approve, params: { id: suggested_edit.id }
suggested_edit.reload
- assert_response(200)
+ assert_response(:success)
assert_not_nil assigns(:edit)
assert_equal suggested_edit.active, false
@@ -104,7 +134,7 @@ class SuggestedEditControllerTest < ActionController::TestCase
post :reject, params: { id: suggested_edit.id, rejection_comment: 'WHY NOT?' }
suggested_edit.reload
- assert_response(200)
+ assert_response(:success)
assert_not_nil assigns(:edit)
assert_equal suggested_edit.active, false
diff --git a/test/controllers/tag_sets_controller_test.rb b/test/controllers/tag_sets_controller_test.rb
index 7cbd5d70a..92378d9e4 100644
--- a/test/controllers/tag_sets_controller_test.rb
+++ b/test/controllers/tag_sets_controller_test.rb
@@ -6,7 +6,7 @@ class TagSetsControllerTest < ActionController::TestCase
test 'should deny access to non-admins' do
[:index, :global].each do |route|
get route
- assert_response 404
+ assert_response(:not_found)
end
rescue => e
puts e.backtrace
@@ -15,7 +15,7 @@ class TagSetsControllerTest < ActionController::TestCase
test 'should allow admins to access index' do
sign_in users(:admin)
get :index
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tag_sets)
assert_not_nil assigns(:counts)
end
@@ -23,13 +23,13 @@ class TagSetsControllerTest < ActionController::TestCase
test 'should deny admins access to global' do
sign_in users(:admin)
get :global
- assert_response 404
+ assert_response(:not_found)
end
test 'should allow global admins to access global' do
sign_in users(:global_admin)
get :global
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tag_sets)
assert_not_nil assigns(:counts)
end
@@ -37,22 +37,20 @@ class TagSetsControllerTest < ActionController::TestCase
test 'should allow admins to access show' do
sign_in users(:global_admin)
get :show, params: { id: tag_sets(:main).id, format: 'json' }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:tag_set)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
end
test 'should update tag set' do
sign_in users(:global_admin)
post :update, params: { id: tag_sets(:main).id, name: 'Test' }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:tag_set)
assert_equal 'Test', assigns(:tag_set).name
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assert_equal 'success', JSON.parse(response.body)['status']
end
end
diff --git a/test/controllers/tags_controller_test.rb b/test/controllers/tags_controller_test.rb
index cdb3983d9..cb45dfa0c 100644
--- a/test/controllers/tags_controller_test.rb
+++ b/test/controllers/tags_controller_test.rb
@@ -5,20 +5,19 @@ class TagsControllerTest < ActionController::TestCase
test 'index with json format should return JSON list of tags' do
get :index, params: { format: 'json' }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:success)
+ assert_valid_json_response
assert_not_nil assigns(:tags)
end
test 'index with search params should return tags including search term' do
get :index, params: { format: 'json', term: 'dis' }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:success)
+ assert_valid_json_response
assert_not_nil assigns(:tags)
+
JSON.parse(response.body).each do |tag|
assert_equal true, tag['name'].include?('dis') || tag['tag_synonyms'].any? { |ts| ts['name'].include?('syn') }
end
@@ -26,11 +25,11 @@ class TagsControllerTest < ActionController::TestCase
test 'index with search params should return tags whose synonyms include search term' do
get :index, params: { format: 'json', term: 'syn' }
- assert_response 200
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+
+ assert_response(:success)
+ assert_valid_json_response
assert_not_nil assigns(:tags)
+
JSON.parse(response.body).each do |tag|
assert_equal true, tag['name'].include?('syn') || tag['tag_synonyms'].any? { |ts| ts['name'].include?('syn') }
end
@@ -38,40 +37,40 @@ class TagsControllerTest < ActionController::TestCase
test 'should get category tags list' do
get :category, params: { id: categories(:main).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tags)
assert_not_nil assigns(:category)
sign_in users(:standard_user)
get :category, params: { id: categories(:main).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tags)
assert_not_nil assigns(:category)
end
test 'should get children list' do
get :children, params: { id: categories(:main).id, tag_id: tags(:topic).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tags)
assert_not_nil assigns(:category)
sign_in users(:standard_user)
get :children, params: { id: categories(:main).id, tag_id: tags(:topic).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tags)
assert_not_nil assigns(:category)
end
test 'should get tag page and RSS' do
get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tag)
assert_not_nil assigns(:category)
assert_not_nil assigns(:posts)
sign_in users(:standard_user)
get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tag)
assert_not_nil assigns(:category)
assert_not_nil assigns(:posts)
@@ -79,14 +78,14 @@ class TagsControllerTest < ActionController::TestCase
test 'should get tag RSS feed' do
get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id, format: :rss }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tag)
assert_not_nil assigns(:category)
assert_not_nil assigns(:posts)
sign_in users(:standard_user)
get :show, params: { id: categories(:main).id, tag_id: tags(:topic).id, format: :rss }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tag)
assert_not_nil assigns(:category)
assert_not_nil assigns(:posts)
@@ -94,20 +93,19 @@ class TagsControllerTest < ActionController::TestCase
test 'should deny edit to anonymous user' do
get :edit, params: { id: categories(:main).id, tag_id: tags(:topic).id }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'should deny edit to unprivileged user' do
sign_in users(:standard_user)
get :edit, params: { id: categories(:main).id, tag_id: tags(:topic).id }
- assert_response 403
+ assert_response(:forbidden)
end
test 'should get edit' do
sign_in users(:deleter)
get :edit, params: { id: categories(:main).id, tag_id: tags(:topic).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:tag)
assert_not_nil assigns(:category)
end
@@ -115,22 +113,21 @@ class TagsControllerTest < ActionController::TestCase
test 'should deny update to anonymous user' do
patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
tag: { parent_id: tags(:discussion).id, excerpt: 'things' } }
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'should deny update to unprivileged user' do
sign_in users(:standard_user)
patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
tag: { parent_id: tags(:discussion).id, excerpt: 'things' } }
- assert_response 403
+ assert_response(:forbidden)
end
test 'should update tag' do
sign_in users(:deleter)
patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
tag: { parent_id: tags(:discussion).id, excerpt: 'things' } }
- assert_response 302
+ assert_response(:found)
assert_redirected_to tag_path(id: categories(:main).id, tag_id: tags(:topic).id)
assert_not_nil assigns(:tag)
assert_equal tags(:discussion).id, assigns(:tag).parent_id
@@ -141,7 +138,7 @@ class TagsControllerTest < ActionController::TestCase
sign_in users(:deleter)
patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
tag: { tag_synonyms_attributes: { '1': { name: 'conversation' } } } }
- assert_response 302
+ assert_response(:found)
assert_redirected_to tag_path(id: categories(:main).id, tag_id: tags(:topic).id)
assert_not_nil assigns(:tag)
assert_equal 'conversation', assigns(:tag).tag_synonyms.first&.name
@@ -151,7 +148,7 @@ class TagsControllerTest < ActionController::TestCase
sign_in users(:deleter)
patch :update, params: { id: categories(:main).id, tag_id: tags(:base).id,
tag: { tag_synonyms_attributes: { '1': { id: tag_synonyms(:base_synonym).id, _destroy: 'true' } } } }
- assert_response 302
+ assert_response(:found)
assert_redirected_to tag_path(id: categories(:main).id, tag_id: tags(:base).id)
assert_not_nil assigns(:tag)
assert_equal true, (assigns(:tag).tag_synonyms.none? { |ts| ts.name == 'synonym' })
@@ -161,7 +158,7 @@ class TagsControllerTest < ActionController::TestCase
sign_in users(:deleter)
patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
tag: { parent_id: tags(:topic).id, excerpt: 'things' } }
- assert_response 400
+ assert_response(:bad_request)
assert_not_nil assigns(:tag)
assert_equal ['A tag cannot be its own parent.'], assigns(:tag).errors.full_messages
end
@@ -170,8 +167,61 @@ class TagsControllerTest < ActionController::TestCase
sign_in users(:deleter)
patch :update, params: { id: categories(:main).id, tag_id: tags(:topic).id,
tag: { parent_id: tags(:child).id, excerpt: 'things' } }
- assert_response 400
+ assert_response(:bad_request)
assert_not_nil assigns(:tag)
assert_equal ["The #{tags(:child).name} tag is already a child of this tag."], assigns(:tag).errors.full_messages
end
+
+ test 'should correctly rename a tag' do
+ sign_in users(:moderator)
+
+ tag = tags(:base)
+
+ new_tag_name = 'renamed'
+
+ post :rename, params: {
+ format: :json,
+ id: categories(:main).id,
+ name: new_tag_name,
+ tag_id: tag.id,
+ tag: tag
+ }
+
+ assert_response(:success)
+ assert_valid_json_response
+
+ res_body = JSON.parse(response.body)
+
+ assert_equal true, res_body['success']
+ assert_equal new_tag_name, res_body['tag']['name']
+
+ log_entry = AuditLog.last
+ assert_equal 'tag_rename', log_entry['event_type']
+ assert_equal tag.id, log_entry['related_id']
+ end
+
+ test 'should prevent renaming a tag to an invalid name' do
+ sign_in users(:moderator)
+
+ tag = tags(:base)
+
+ old_tag_name = tag.name
+
+ post :rename, params: {
+ format: :json,
+ id: categories(:main).id,
+ name: '',
+ tag_id: tag.id,
+ tag: tag
+ }
+
+ assert_response(:success)
+ assert_valid_json_response
+
+ res_body = JSON.parse(response.body)
+
+ assert_equal false, res_body['success']
+ tag.reload
+ assert_equal tag.name, old_tag_name
+ end
end
diff --git a/test/controllers/users/registrations_controller_test.rb b/test/controllers/users/registrations_controller_test.rb
new file mode 100644
index 000000000..bc90cc5d6
--- /dev/null
+++ b/test/controllers/users/registrations_controller_test.rb
@@ -0,0 +1,125 @@
+require 'test_helper'
+
+class Users::RegistrationsControllerTest < ActionController::TestCase
+ include Devise::Test::ControllerHelpers
+ include ApplicationHelper
+
+ setup :devise_setup
+
+ test 'should register user' do
+ try_register_user('test', 'test@example.com', 'testtest')
+
+ assert_response(:found)
+ assert_not_nil assigns(:user).id
+ assert_redirected_to root_path
+ end
+
+ test 'should prevent rapid registrations from same IP' do
+ User.create(username: 'test', email: 'test2@example.com', password: 'testtest', current_sign_in_ip: '0.0.0.0')
+ try_register_user('test', 'test@example.com', 'testtest')
+
+ assert_response(:found)
+ assert_redirected_to users_path
+ assert_not_nil flash[:danger]
+ end
+
+ test 'ensure Devise errors are handled properly' do
+ existing_user = users(:standard_user)
+ try_register_user(existing_user.username, existing_user.email, 'testtest')
+
+ assert_response(:success)
+ assert_not_empty assigns(:user).errors
+ end
+
+ test 'should show deletion information page' do
+ sign_in users(:standard_user)
+ session[:sudo] = DateTime.now.iso8601
+ get :delete
+ assert_response(:success)
+ end
+
+ test 'should require authentication for deletion information' do
+ get :delete
+ assert_response(:found)
+ assert_redirected_to new_user_session_path
+ end
+
+ test 'should require sudo for deletion information' do
+ sign_in users(:standard_user)
+ get :delete
+ assert_response(:found)
+ assert_redirected_to user_sudo_path
+ end
+
+ test 'should delete user account' do
+ sign_in users(:standard_user)
+ session[:sudo] = DateTime.now.iso8601
+ try_do_delete_user(users(:standard_user))
+
+ assert_response(:found)
+ assert_redirected_to root_path
+ assert_equal 'Sorry to see you go!', flash[:info]
+ assert assigns(:user).deleted
+ end
+
+ test 'should require authentication to delete user account' do
+ post :do_delete, params: { username: 'anything' }
+
+ assert_response(:found)
+ assert_redirected_to new_user_session_path
+ end
+
+ test 'should require sudo to delete user account' do
+ sign_in users(:standard_user)
+ post :do_delete, params: { username: 'anything' }
+
+ assert_response(:found)
+ assert_redirected_to user_sudo_path
+ end
+
+ test 'should prevent deletion if username is incorrect' do
+ sign_in users(:standard_user)
+ session[:sudo] = DateTime.now.iso8601
+ post :do_delete, params: { username: 'wrong' }
+
+ assert_response(:success)
+ assert_equal [I18n.t('users.errors.self_delete_wrong_username')], assigns(:user).errors.full_messages
+ assert_not assigns(:user).deleted
+ end
+
+ test 'should prevent self-deletion if the user is at least a moderator' do
+ locale_string_map = {
+ moderator: 'users.errors.no_mod_self_delete',
+ admin: 'users.errors.no_admin_self_delete',
+ enabled_2fa: 'users.errors.no_2fa_self_delete'
+ }
+
+ [:moderator, :admin, :enabled_2fa].each do |name|
+ sign_in users(name)
+ session[:sudo] = DateTime.now.iso8601
+
+ try_do_delete_user(users(name))
+
+ assert_response(:success)
+ assert_equal [I18n.t(locale_string_map[name])], assigns(:user).errors.full_messages
+ assert_not assigns(:user).deleted
+ end
+ end
+
+ private
+
+ # Attempts to sudo delete a given user
+ # @param user [User] user to delete
+ def try_do_delete_user(user)
+ post :do_delete, params: { username: user.username }
+ end
+
+ def try_register_user(username, email, password)
+ post :create, params: { user: { username: username, email: email, password: password,
+ password_confirmation: password } }
+ end
+
+ def devise_setup
+ @request.env['devise.mapping'] = Devise.mappings[:user]
+ end
+end
diff --git a/test/controllers/users/sessions_controller_test.rb b/test/controllers/users/sessions_controller_test.rb
index b06e1b602..21cb88f44 100644
--- a/test/controllers/users/sessions_controller_test.rb
+++ b/test/controllers/users/sessions_controller_test.rb
@@ -8,7 +8,8 @@ class Users::SessionsControllerTest < ActionController::TestCase
@request.env['devise.mapping'] = Devise.mappings[:user]
Users::SessionsController.first_factor << users(:enabled_2fa).id
post :verify_code, params: { uid: users(:enabled_2fa).id, code: 'M8lENyehyCvo9F9MbyTl1aOL' }
- assert_response 302
+
+ assert_response(:found)
assert_not_nil flash[:warning]
assert_not_nil current_user
assert_nil current_user.backup_2fa_code
diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb
index 0aa5d0863..5df2d395e 100644
--- a/test/controllers/users_controller_test.rb
+++ b/test/controllers/users_controller_test.rb
@@ -1,61 +1,63 @@
require 'test_helper'
+require_relative 'concerns/users/users_abilities_test'
class UsersControllerTest < ActionController::TestCase
include Devise::Test::ControllerHelpers
include ApplicationHelper
+ include UsersAbilitiesTest
test 'should get index' do
get :index
assert_not_nil assigns(:users)
- assert_response(200)
+ assert_response(:success)
end
test 'should not include users not in current community' do
@other_user = create_other_user
get :index
assert_not_includes assigns(:users), @other_user
- assert_response(200)
+ assert_response(:success)
end
test 'should get show user page' do
sign_in users(:standard_user)
get :show, params: { id: users(:standard_user).id }
assert_not_nil assigns(:user)
- assert_response(200)
+ assert_response(:success)
end
test 'should get show user unauthenticated' do
get :show, params: { id: users(:standard_user).id }
assert_not_nil assigns(:user)
- assert_response 200
+ assert_response(:success)
end
test 'should not show user page for non-community users' do
@other_user = create_other_user
sign_in users(:standard_user)
get :show, params: { id: @other_user.id }
- assert_response(404)
+ assert_response(:not_found)
end
test 'should get mod tools page' do
sign_in users(:moderator)
get :mod, params: { id: users(:standard_user).id }
assert_not_nil assigns(:user)
- assert_response(200)
+ assert_response(:success)
end
test 'should require authentication to access mod tools' do
sign_out :user
get :mod, params: { id: users(:standard_user).id }
assert_nil assigns(:user)
- assert_response(404)
+ assert_response(:not_found)
end
test 'should require moderator status to access mod tools' do
sign_in users(:standard_user)
get :mod, params: { id: users(:standard_user).id }
assert_nil assigns(:user)
- assert_response(404)
+ assert_response(:not_found)
end
test 'should destroy user' do
@@ -63,27 +65,27 @@ class UsersControllerTest < ActionController::TestCase
delete :destroy, params: { id: users(:standard_user).id }
assert_not_nil assigns(:user)
assert_equal 'success', JSON.parse(response.body)['status']
- assert_response(200)
+ assert_response(:success)
end
test 'should require authentication to destroy user' do
sign_out :user
delete :destroy, params: { id: users(:standard_user).id }
assert_nil assigns(:user)
- assert_response(404)
+ assert_response(:not_found)
end
test 'should require moderator status to destroy user' do
sign_in users(:standard_user)
delete :destroy, params: { id: users(:standard_user).id }
assert_nil assigns(:user)
- assert_response(404)
+ assert_response(:not_found)
end
test 'should soft-delete user' do
sign_in users(:global_admin)
delete :soft_delete, params: { id: users(:standard_user).id, type: 'user' }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:user)
assert_equal true, assigns(:user).deleted
end
@@ -92,31 +94,32 @@ class UsersControllerTest < ActionController::TestCase
sign_out :user
delete :soft_delete, params: { id: users(:standard_user).id, transfer: users(:editor).id }
assert_nil assigns(:user)
- assert_response(404)
+ assert_response(:not_found)
end
test 'should require admin status to soft-delete user' do
sign_in users(:standard_user)
delete :soft_delete, params: { id: users(:standard_user).id, transfer: users(:editor).id }
assert_nil assigns(:user)
- assert_response(404)
+ assert_response(:not_found)
end
test 'should require authentication to get edit profile page' do
get :edit_profile
- assert_response 302
+ assert_response(:found)
end
test 'should get edit profile page' do
sign_in users(:standard_user)
get :edit_profile
- assert_response 200
+ assert_response(:success)
end
test 'should redirect & show success notice on profile update' do
sign_in users(:standard_user)
patch :update_profile, params: { user: { username: 'std' } }
- assert_response 302
+
+ assert_response(:found)
assert_not_nil flash[:success]
assert_not_nil assigns(:user)
assert_equal users(:standard_user).id, assigns(:user).id
@@ -152,29 +155,28 @@ class UsersControllerTest < ActionController::TestCase
test 'should get full posts list for a user' do
get :posts, params: { id: users(:standard_user).id }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:user)
assert_not_nil assigns(:posts)
end
test 'should get full posts list in JSON format' do
get :posts, params: { id: users(:standard_user).id, format: 'json' }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:user)
assert_not_nil assigns(:posts)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
end
test 'should sort full posts lists correctly' do
get :posts, params: { id: users(:standard_user).id, format: :json, sort: 'age' }
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:user)
assert_not_nil assigns(:posts)
- assert_nothing_raised do
- JSON.parse(response.body)
- end
+ assert_valid_json_response
assigns(:posts).each_with_index do |post, idx|
next if idx.zero?
@@ -186,14 +188,14 @@ class UsersControllerTest < ActionController::TestCase
test 'should require authentication to get mobile login' do
get :qr_login_code
- assert_response 302
- assert_redirected_to new_user_session_path
+ assert_redirected_to_sign_in
end
test 'should allow signed in users to get mobile login' do
sign_in users(:standard_user)
get :qr_login_code
- assert_response 200
+
+ assert_response(:success)
assert_not_nil assigns(:token)
assert_not_nil assigns(:qr_code)
assert_equal 1, User.where(login_token: assigns(:token)).count
@@ -203,7 +205,8 @@ class UsersControllerTest < ActionController::TestCase
test 'should sign in user in response to valid mobile login request' do
get :do_qr_login, params: { token: 'abcdefghijklmnopqrstuvwxyz01' }
- assert_response 302
+
+ assert_response(:found)
assert_equal 'You are now signed in.', flash[:success]
assert_equal users(:closer).id, current_user.id
assert_nil current_user.login_token
@@ -212,7 +215,8 @@ class UsersControllerTest < ActionController::TestCase
test 'should refuse to sign in user using expired token' do
get :do_qr_login, params: { token: 'abcdefghijklmnopqrstuvwxyz02' }
- assert_response 404
+
+ assert_response(:not_found)
assert_not_nil flash[:danger]
assert_equal true, flash[:danger].start_with?("That login link isn't valid.")
assert_nil current_user&.id
@@ -220,51 +224,51 @@ class UsersControllerTest < ActionController::TestCase
test 'should deny anonymous users access to annotations' do
get :annotations, params: { id: users(:standard_user).id }
- assert_response 404
+ assert_response(:not_found)
end
test 'should deny non-mods access to annotations' do
sign_in users(:standard_user)
get :annotations, params: { id: users(:standard_user).id }
- assert_response 404
+ assert_response(:not_found)
end
test 'should get annotations' do
sign_in users(:admin)
get :annotations, params: { id: users(:standard_user).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:logs)
end
test 'should annotate user' do
sign_in users(:admin)
post :annotate, params: { id: users(:standard_user).id, comment: 'some words' }
- assert_response 302
+ assert_response(:found)
assert_redirected_to user_annotations_path(users(:standard_user))
end
test 'should deny access to deleted account' do
get :show, params: { id: users(:deleted_account).id }
- assert_response 404
+ assert_response(:not_found)
end
test 'should deny access to deleted profile' do
get :show, params: { id: users(:deleted_profile).id }
- assert_response 404
+ assert_response(:not_found)
assert_not_nil assigns(:user)
end
test 'should allow moderator access to deleted account' do
sign_in users(:moderator)
get :show, params: { id: users(:deleted_account).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:user)
end
test 'should allow moderator access to deleted profile' do
sign_in users(:moderator)
get :show, params: { id: users(:deleted_profile).id }
- assert_response 200
+ assert_response(:success)
assert_not_nil assigns(:user)
end
@@ -296,25 +300,231 @@ class UsersControllerTest < ActionController::TestCase
test 'vote summary rendered for all users, signed in or out, own or others' do
sign_out :user
get :vote_summary, params: { id: users(:standard_user).id }
- assert_response 200
+ assert_response(:success)
get :vote_summary, params: { id: users(:closer).id }
- assert_response 200
+ assert_response(:success)
sign_in users(:editor)
get :vote_summary, params: { id: users(:editor).id }
- assert_response 200
+ assert_response(:success)
get :vote_summary, params: { id: users(:deleter).id }
- assert_response 200
+ assert_response(:success)
+ end
+
+ test 'me should redirect to currently signed in user' do
+ std = users(:standard_user)
+
+ sign_in std
+ get :me, format: 'html'
+ assert_redirected_to user_path(std)
+ end
+
+ test "me should return currently signed in user's data for JSON format" do
+ mod = users(:moderator)
+
+ sign_in mod
+ get :me, format: 'json'
+ assert_response(:success)
+
+ data = JSON.parse(response.body)
+
+ assert_equal data['id'], mod.id
+ assert_equal data['username'], mod.username
+ end
+
+ test 'role toggle should correctly grant & revoke moderator role' do
+ sign_in users(:global_admin)
+
+ mod = users(:moderator)
+
+ post :role_toggle, params: { id: mod.id, role: 'mod' }
+ assert_response(:success)
+
+ mod.reload
+ assert_equal mod.moderator?, false
+
+ post :role_toggle, params: { id: mod.id, role: 'mod' }
+ assert_response(:success)
+
+ mod.reload
+ assert_equal mod.moderator?, true
+ end
+
+ test 'role toggle should correctly grant & revoke admin role' do
+ sign_in users(:global_admin)
+
+ admin = users(:admin)
+
+ post :role_toggle, params: { id: admin.id, role: 'admin' }
+ assert_response(:success)
+
+ admin.reload
+ assert_equal admin.admin?, false
+
+ post :role_toggle, params: { id: admin.id, role: 'admin' }
+ assert_response(:success)
+
+ admin.reload
+ assert_equal admin.admin?, true
+ end
+
+ test 'role toggle should correctly grant & revoke global moderator role' do
+ sign_in users(:global_admin)
+
+ mod = users(:moderator)
+
+ post :role_toggle, params: { id: mod.id, role: 'mod_global' }
+ assert_response(:success)
+
+ mod.reload
+ assert_equal mod.global_moderator?, true
+
+ post :role_toggle, params: { id: mod.id, role: 'mod_global' }
+ assert_response(:success)
+
+ mod.reload
+ assert_equal mod.global_moderator?, false
+ end
+
+ test 'role toggle should correctly grant & revoke global admin role' do
+ sign_in users(:global_admin)
+
+ admin = users(:admin)
+
+ post :role_toggle, params: { id: admin.id, role: 'admin_global' }
+ assert_response(:success)
+
+ admin.reload
+ assert_equal admin.global_admin?, true
+
+ post :role_toggle, params: { id: admin.id, role: 'admin_global' }
+ assert_response(:success)
+
+ admin.reload
+ assert_equal admin.global_admin?, false
+ end
+
+ test 'full_log should only be accessible to mods or admins' do
+ mod = users(:moderator)
+ std = users(:standard_user)
+
+ sign_in mod
+ get :full_log, params: { id: std.id }
+ assert_response(:success)
+
+ sign_in std
+ get :full_log, params: { id: std.id }
+ assert_response(:not_found)
+ end
+
+ test 'activity should correctly apply single-type items filter' do
+ std = users(:standard_user)
+
+ sign_in std
+
+ model_map = {
+ 'posts' => Post,
+ 'comments' => Comment,
+ 'edits' => SuggestedEdit
+ }
+
+ model_map.each do |filter, model|
+ get :activity, params: { id: std.id, filter: filter }
+ assert_response(:success)
+ items = assigns(:items)
+
+ assert(items.all? { |x| x.instance_of?(model) })
+ end
+ end
+
+ test 'default activity filter should include items of all types' do
+ std = users(:standard_user)
+
+ sign_in std
+
+ get :activity, params: { id: std.id }
+
+ assert_response(:success)
+ items = assigns(:items)
+
+ edit_type = PostHistoryType.find_by(name: 'post_edited')
+
+ [Post, Comment, SuggestedEdit, Flag, PostHistory, ModWarning].each do |model|
+ assert(items.any? do |item|
+ is_valid = if item.instance_of?(model)
+ case model
+ when Post
+ item.deleted == false
+ when Comment
+ item.comment_thread.deleted == false &&
+ item.deleted == false &&
+ item.post.deleted == false
+ when PostHistory
+ item.post_history_type == edit_type
+ when SuggestedEdit
+ item.post.deleted == false
+ end
+ else
+ true
+ end
+
+ is_valid && item.user.same_as?(std)
+ end)
+ end
+ end
+
+ test 'full_log should correctly apply single-type items filter' do
+ sign_in users(:moderator)
+
+ model_map = {
+ 'posts' => Post,
+ 'comments' => Comment,
+ 'edits' => SuggestedEdit,
+ 'flags' => Flag,
+ 'warnings' => ModWarning
+ }
+
+ model_map.each do |filter, model|
+ get :full_log, params: { id: users(:standard_user).id, filter: filter }
+ assert_response(:success)
+ items = assigns(:items)
+
+ assert(items.all? { |x| x.instance_of?(model) })
+ end
+ end
+
+ test 'full_log\'s \'interesting\' filter should include deleted comments' do
+ sign_in users(:moderator)
+
+ get :full_log, params: { id: users(:standard_user).id, filter: 'interesting' }
+ assert_response(:success)
+ items = assigns(:items)
+
+ deleted_comment = comments(:deleted)
+
+ assert(items.any? { |x| x.instance_of?(Comment) && x.id == deleted_comment.id })
+ end
+
+ test 'full_log\'s \'interesting\' filter should include declined flags' do
+ sign_in users(:moderator)
+
+ get :full_log, params: { id: users(:standard_user).id, filter: 'interesting' }
+ assert_response(:success)
+ items = assigns(:items)
+
+ declined_flag = flags(:declined)
+
+ assert(items.any? { |x| x.instance_of?(Flag) && x.id == declined_flag.id })
end
private
def create_other_user
other_community = Community.create(host: 'other.qpixel.com', name: 'Other')
- RequestContext.redis.hset 'network/community_registrations', 'other@example.com', other_community.id
+ RequestContext.redis.hset('network/community_registrations', 'other@example.com', other_community.id)
other_user = User.create!(email: 'other@example.com', password: 'abcdefghijklmnopqrstuvwxyz', username: 'other_user')
other_user.community_users.create!(community: other_community)
other_user
diff --git a/test/controllers/votes_controller_test.rb b/test/controllers/votes_controller_test.rb
index 81d1b5009..fe4c3cb38 100644
--- a/test/controllers/votes_controller_test.rb
+++ b/test/controllers/votes_controller_test.rb
@@ -5,84 +5,117 @@ class VotesControllerTest < ActionController::TestCase
test 'should cast upvote' do
sign_in users(:standard_user)
- post :create, params: { post_id: posts(:question_two).id, vote_type: 1 }
+
+ post :create, params: { post_id: posts(:question_without_votes).id, vote_type: 1 }
+
+ assert_response(:success)
+ assert_valid_json_response
assert_equal 'OK', JSON.parse(response.body)['status']
- assert_response(200)
end
test 'should cast downvote' do
sign_in users(:standard_user)
- post :create, params: { post_id: posts(:question_two).id, vote_type: -1 }
+
+ post :create, params: { post_id: posts(:question_without_votes).id, vote_type: -1 }
+
+ assert_response(:success)
+ assert_valid_json_response
assert_equal 'OK', JSON.parse(response.body)['status']
- assert_response(200)
end
test 'should return correct modified status' do
- sign_in users(:editor)
- post :create, params: { post_id: posts(:question_one).id, vote_type: -1 }
+ post_id = posts(:question_without_votes).id
+
+ sign_in users(:standard_user)
+
+ post :create, params: { post_id: post_id, vote_type: 1 }
+ post :create, params: { post_id: post_id, vote_type: -1 }
+
+ assert_response(:success)
+ assert_valid_json_response
assert_equal 'modified', JSON.parse(response.body)['status']
- assert_response(200)
end
test 'should silently accept duplicate votes' do
- sign_in users(:editor)
- post :create, params: { post_id: posts(:question_one).id, vote_type: 1 }
+ post_id = posts(:question_without_votes).id
+
+ sign_in users(:standard_user)
+
+ post :create, params: { post_id: post_id, vote_type: 1 }
+ post :create, params: { post_id: post_id, vote_type: 1 }
+
+ assert_response(:success)
+ assert_valid_json_response
assert_equal 'modified', JSON.parse(response.body)['status']
- assert_response 200
end
test 'should prevent self voting' do
sign_in users(:editor)
- post :create, params: { post_id: posts(:question_two).id, vote_type: 1 }
- assert_equal 'You may not vote on your own posts.', JSON.parse(response.body)['message']
- assert_response(403)
+
+ post :create, params: { post_id: posts(:question_without_votes).id, vote_type: 1 }
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+ assert_json_response_message('You may not vote on your own posts.')
end
test 'should remove existing vote' do
sign_in users(:editor)
+
delete :destroy, params: { id: votes(:one).id }
+
+ assert_response(:success)
+ assert_valid_json_response
assert_equal 'OK', JSON.parse(response.body)['status']
- assert_response(200)
end
test 'should prevent users removing others votes' do
sign_in users(:standard_user)
+
delete :destroy, params: { id: votes(:one).id }
- assert_equal 'You are not authorized to remove this vote.', JSON.parse(response.body)['message']
- assert_response(403)
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+ assert_json_response_message('You are not authorized to remove this vote.')
end
test 'should require authentication to create a vote' do
sign_out :user
+
post :create
- assert_equal 'You must be logged in to vote.', JSON.parse(response.body)['message']
- assert_response(403)
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+ assert_json_response_message('You must be logged in to vote.')
end
test 'should require authentication to remove a vote' do
sign_out :user
+
delete :destroy, params: { id: votes(:one).id }
- assert_equal 'You must be logged in to vote.', JSON.parse(response.body)['message']
- assert_response(403)
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+ assert_json_response_message('You must be logged in to vote.')
end
test 'should prevent deleted account casting votes' do
sign_in users(:deleted_account)
- post :create, params: { post_id: posts(:question_two).id, vote_type: 1 }
- assert_response 403
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'You must be logged in to vote.', JSON.parse(response.body)['message']
+
+ post :create, params: { post_id: posts(:question_without_votes).id, vote_type: 1 }
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+ assert_json_response_message('You must be logged in to vote.')
end
test 'should prevent deleted profile casting votes' do
sign_in users(:deleted_profile)
- post :create, params: { post_id: posts(:question_two).id, vote_type: 1 }
- assert_response 403
- assert_nothing_raised do
- JSON.parse(response.body)
- end
- assert_equal 'You must be logged in to vote.', JSON.parse(response.body)['message']
+
+ post :create, params: { post_id: posts(:question_without_votes).id, vote_type: 1 }
+
+ assert_response(:forbidden)
+ assert_valid_json_response
+ assert_json_response_message('You must be logged in to vote.')
end
end
diff --git a/test/fixtures/blocked_items.yml b/test/fixtures/blocked_items.yml
index 80aed36e3..6070be6b2 100644
--- a/test/fixtures/blocked_items.yml
+++ b/test/fixtures/blocked_items.yml
@@ -1,11 +1,13 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
-# This model initially had no columns defined. If you add columns to the
-# model remove the '{}' from the fixture names and add the columns immediately
-# below each fixture, per the syntax in the comments below
-#
-one: {}
-# column: value
-#
-two: {}
-# column: value
+email:
+ item_type: email
+ value: blocked@mail.com
+ automatic: true
+ reason: got fed up with them
+
+ip:
+ item_type: ip
+ value: 8.8.8.8
+ automatic: false
+ reason: Why are we accessed by a DNS server?
diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml
index b59b61265..f44432b70 100644
--- a/test/fixtures/categories.yml
+++ b/test/fixtures/categories.yml
@@ -29,13 +29,13 @@ high_trust:
admin_only:
community: sample
- name: High Trust
- short_wiki: High Trust
+ name: Admin Only
+ short_wiki: Admin Only
display_post_types:
- <%= Question.post_type_id %>
tag_set: main
- min_trust_level: 6
- min_view_trust_level: 6
+ min_trust_level: 5
+ min_view_trust_level: 5
license: cc_by_sa
articles_only:
@@ -46,3 +46,12 @@ articles_only:
- <%= Article.post_type_id %>
tag_set: main
license: cc_by_sa
+
+moderator_tags:
+ community: sample
+ name: Moderator Tags
+ short_wiki: Category with a couple of moderator tags
+ tag_set: main
+ license: cc_by_sa
+ moderator_tags:
+ - feature-request
diff --git a/test/fixtures/category_post_types.yml b/test/fixtures/category_post_types.yml
index b2da00fdc..82d3d598c 100644
--- a/test/fixtures/category_post_types.yml
+++ b/test/fixtures/category_post_types.yml
@@ -63,3 +63,15 @@ articles_only_article:
post_type: article
upvote_rep: 10
downvote_rep: -2
+
+moderator_tags_question:
+ category: moderator_tags
+ post_type: question
+ upvote_rep: 0
+ downvote_rep: 0
+
+moderator_tags_answer:
+ category: moderator_tags
+ post_type: answer
+ upvote_rep: 0
+ downvote_rep: 0
diff --git a/test/fixtures/close_reasons.yml b/test/fixtures/close_reasons.yml
index af52bcd22..30886897a 100644
--- a/test/fixtures/close_reasons.yml
+++ b/test/fixtures/close_reasons.yml
@@ -10,4 +10,11 @@ not_good:
description: not good
active: true
requires_other_post: false
- community: sample
\ No newline at end of file
+ community: sample
+
+global:
+ name: global
+ description: global
+ active: true
+ requires_other_post: false
+ community: ~
diff --git a/test/fixtures/comment_threads.yml b/test/fixtures/comment_threads.yml
index 49f8adbcb..0948a1c35 100644
--- a/test/fixtures/comment_threads.yml
+++ b/test/fixtures/comment_threads.yml
@@ -56,4 +56,10 @@ on_answer:
title: on answer
reply_count: 0
post: answer_one
- community: sample
\ No newline at end of file
+ community: sample
+
+new_user_on_own_post:
+ title: new user on own post
+ reply_count: 0
+ post: new_user_question
+ community: sample
diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml
index ace48142e..0de66ed0a 100644
--- a/test/fixtures/comments.yml
+++ b/test/fixtures/comments.yml
@@ -47,3 +47,10 @@ on_answer:
content: ABCDEF GHIJKL MNOPQR
community: sample
comment_thread: on_answer
+
+new_user_on_own_post:
+ user: basic_user
+ post: new_user_question
+ content: ABCDEF GHIJKL MNOPQR
+ community: sample
+ comment_thread: new_user_on_own_post
diff --git a/test/fixtures/community_users.yml b/test/fixtures/community_users.yml
index 4d7ac6963..ea81fce1e 100644
--- a/test/fixtures/community_users.yml
+++ b/test/fixtures/community_users.yml
@@ -61,6 +61,13 @@ sample_global_admin:
is_moderator: false
reputation: 1
+sample_staff:
+ user: staff
+ community: sample
+ is_admin: false
+ is_moderator: false
+ reputation: 1
+
sample_deleted_account:
user: deleted_account
community: sample
@@ -77,3 +84,10 @@ sample_deleted_profile:
deleted: true
deleted_at: 2020-01-02T00:00:00.000000Z
deleted_by: global_admin
+
+sample_enabled_2fa:
+ user: enabled_2fa
+ community: sample
+ is_admin: false
+ is_moderator: false
+ reputation: 1
diff --git a/test/fixtures/flags.yml b/test/fixtures/flags.yml
index 64a57be06..83109e6fb 100644
--- a/test/fixtures/flags.yml
+++ b/test/fixtures/flags.yml
@@ -3,3 +3,34 @@ one:
post: answer_two (Post)
user: standard_user
community: sample
+
+declined:
+ reason: Please decline it
+ post: question_one (Post)
+ user: standard_user
+ community: sample
+ status: declined
+
+on_deleter:
+ reason: For science!
+ post: deleted (Post)
+ post_flag_type: not_confidential
+ user: deleter
+ community: sample
+
+confidential_on_deleter:
+ reason: TOP SECRET DO NOT SHARE
+ post: deleted (Post)
+ post_flag_type: confidential
+ user: deleter
+ community: sample
+
+escalated:
+ reason: because I said so
+ post: question_one (Post)
+ user: standard_user
+ community: sample
+ escalated: true
+ escalation_comment: what do I do with this
+ escalated_at: 2025-01-01T00:00:00.000000Z
+ escalated_by: moderator
diff --git a/test/fixtures/micro_auth/apps.yml b/test/fixtures/micro_auth/apps.yml
index 9cdc9b49b..777583bc8 100644
--- a/test/fixtures/micro_auth/apps.yml
+++ b/test/fixtures/micro_auth/apps.yml
@@ -1,17 +1,19 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
-one:
- name: MyString
- app_id: MyString
- public_key: MyString
- secret_key: MyString
+owned_by_standard:
+ name: App owned by the standard user
+ app_id: StdApp
+ public_key: KF2zSr1dgedXNgUfwmWtjjrN7MNpSWAh
+ secret_key: FHWv7FJ9ZrJ2AJbriQELXFv8J6JXmK2K
description: MyText
auth_domain: MyString
+ user: standard_user
-two:
- name: MyString
- app_id: MyString
- public_key: MyString
- secret_key: MyString
+owned_by_admin:
+ name: App owned by a community admin
+ app_id: AdminApp
+ public_key: dQUptrgdqiLKZHSKm2ArZXJRwRh9yLXD
+ secret_key: PWT7h32gnQC6Sam32iTNoTHD5DnbTTRU
description: MyText
auth_domain: MyString
+ user: admin
diff --git a/test/fixtures/micro_auth/tokens.yml b/test/fixtures/micro_auth/tokens.yml
index 876354468..03103865d 100644
--- a/test/fixtures/micro_auth/tokens.yml
+++ b/test/fixtures/micro_auth/tokens.yml
@@ -1,13 +1,13 @@
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
- app: one
- user: one
- token: MyString
+ app: owned_by_standard
+ user: standard_user
+ token: q9sYGB637q
expires_at: 2021-11-21 19:51:00
two:
- app: two
- user: two
- token: MyString
+ app: owned_by_admin
+ user: admin
+ token: KXYnnCSgEM
expires_at: 2021-11-21 19:51:00
diff --git a/test/fixtures/pinned_links.yml b/test/fixtures/pinned_links.yml
index 80aed36e3..c13a0ac35 100644
--- a/test/fixtures/pinned_links.yml
+++ b/test/fixtures/pinned_links.yml
@@ -1,11 +1,16 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
+active_with_label:
+ community: sample
+ label: test link
+ link: https://example.com
+ active: true
-# This model initially had no columns defined. If you add columns to the
-# model remove the '{}' from the fixture names and add the columns immediately
-# below each fixture, per the syntax in the comments below
-#
-one: {}
-# column: value
-#
-two: {}
-# column: value
+active_with_post:
+ community: sample
+ post: question_one
+ active: true
+
+inactive:
+ community: sample
+ label: test link
+ link: https://example.com
+ active: false
diff --git a/test/fixtures/post_flag_types.yml b/test/fixtures/post_flag_types.yml
index 80aed36e3..35522487a 100644
--- a/test/fixtures/post_flag_types.yml
+++ b/test/fixtures/post_flag_types.yml
@@ -9,3 +9,13 @@ one: {}
#
two: {}
# column: value
+#
+confidential:
+ community: sample
+ confidential: true
+ name: For your eyes only
+
+not_confidential:
+ community: sample
+ confidential: false
+ name: Definitely not confidential
diff --git a/test/fixtures/post_histories.yml b/test/fixtures/post_histories.yml
index e69de29bb..cf6495c63 100644
--- a/test/fixtures/post_histories.yml
+++ b/test/fixtures/post_histories.yml
@@ -0,0 +1,16 @@
+question_one_revision:
+ post_history_type: post_edited
+ user: editor
+ post: question_one
+ before_title: Q1 - This is test question number one
+ after_title: Revised title for question one
+ comment: A simple revision made by an editor user
+ community: sample
+
+question_one_hidden_revision:
+ post_history_type: post_edited
+ user: admin
+ post: question_one
+ comment: 'A hidden revision made by an admin user'
+ community: sample
+ hidden: true
diff --git a/test/fixtures/post_history_types.yml b/test/fixtures/post_history_types.yml
index 330742f43..069e50608 100644
--- a/test/fixtures/post_history_types.yml
+++ b/test/fixtures/post_history_types.yml
@@ -1,3 +1,7 @@
initial_revision:
name: initial_revision
- description: initial revision
\ No newline at end of file
+ description: initial revision
+
+post_edited:
+ name: post_edited
+ description: normal revision
diff --git a/test/fixtures/posts.yml b/test/fixtures/posts.yml
index 3af917cd4..e4cf4cace 100644
--- a/test/fixtures/posts.yml
+++ b/test/fixtures/posts.yml
@@ -327,6 +327,32 @@ deleted_answer:
upvote_count: 0
downvote_count: 0
+good_answer:
+ post_type: answer
+ body: A3GA - This is the seventh answer to question number 1 (Q1). It has a good score. Posted by standard_user.
+ body_markdown: A7 - This is the seventh answer to question number 1 (Q1). It has a good score. Posted by standard_user.
+ score: 0.6
+ parent: question_one
+ user: standard_user
+ community: sample
+ category: main
+ license: cc_by_sa
+ upvote_count: 1
+ downvote_count: 0
+
+divisive_answer:
+ post_type: answer
+ body: A3DA - This is the eighth answer to question number 1 (Q1). It has divisive votes. Posted by standard_user.
+ body_markdown: A8 - This is the eighth answer to question number 1 (Q1). It has divisive votes. Posted by standard_user.
+ score: 0.6
+ parent: question_one
+ user: standard_user
+ community: sample
+ category: main
+ license: cc_by_sa
+ upvote_count: 4
+ downvote_count: 2
+
policy_doc:
post_type: policy_doc
body: PD - This is a policy document called "Terms of Service", or "tos" for short.
@@ -440,3 +466,17 @@ blog_post:
category: main
license: cc_by_sa
+question_without_votes:
+ post_type: question
+ title: This is a question for testing votes without affecting existing ones
+ body: This is the body of test question for votes.
+ body_markdown: This is the body of test question for votes.
+ tags_cache:
+ - support
+ tags:
+ - support
+ score: 0.5
+ user: editor
+ community: sample
+ category: main
+ license: cc_by_sa
diff --git a/test/fixtures/site_settings.yml b/test/fixtures/site_settings.yml
index 73c3eb4cc..1d9b79515 100644
--- a/test/fixtures/site_settings.yml
+++ b/test/fixtures/site_settings.yml
@@ -7,3 +7,19 @@ sanitize:
name: AskingGuidance
value: ""
value_type: string
+
+int:
+ name: SettingWithIntegerValue
+ value: 42
+ value_type: integer
+
+text:
+ name: SettingWithTextValue
+ value: "
a paragraph of text
"
+ value_type: text
+
+rl_new_user_comments:
+ community: sample
+ name: RL_NewUserComments
+ value: 1
+ value_type: integer
diff --git a/test/fixtures/subscriptions.yml b/test/fixtures/subscriptions.yml
index aa955833e..3c5949f60 100644
--- a/test/fixtures/subscriptions.yml
+++ b/test/fixtures/subscriptions.yml
@@ -35,3 +35,13 @@ interesting:
last_sent_at: 2020-01-01T00:00:00
name: Interesting things
community: sample
+
+category:
+ type: category
+ qualifier: <%= ActiveRecord::FixtureSet.identify :main %>
+ user: standard_user
+ enabled: true
+ frequency: 1
+ last_sent_at: 2020-01-01T00:00:00
+ name: Meta subscription
+ community: sample
diff --git a/test/fixtures/suggested_edits.yml b/test/fixtures/suggested_edits.yml
index 8e57bf630..5f79e9b6d 100644
--- a/test/fixtures/suggested_edits.yml
+++ b/test/fixtures/suggested_edits.yml
@@ -60,4 +60,15 @@ rejected_suggested_edit:
accepted: false
decided_at: 2000-01-01T00:00:00.000000Z
decided_by: editor
- rejected_comment: NO WAY
\ No newline at end of file
+ rejected_comment: NO WAY
+
+pending_high_trust:
+ post: high_trust
+ user: moderator
+ community: sample
+ body: This suggested edit requires high trust to be approved or rejected
+ title: Very important suggestion
+ body_markdown:
This suggested edit requires high trust to be approved or rejected
+ comment: High trust only
+ active: true
+ accepted: false
diff --git a/test/fixtures/user_abilities.yml b/test/fixtures/user_abilities.yml
index 697d53b1d..edc8024b2 100644
--- a/test/fixtures/user_abilities.yml
+++ b/test/fixtures/user_abilities.yml
@@ -56,4 +56,19 @@ d_et:
b_eo:
community_user: sample_basic_user
- ability: everyone
\ No newline at end of file
+ ability: everyone
+
+e2_eo:
+ community_user: sample_enabled_2fa
+ ability: everyone
+
+e2_ur:
+ community_user: sample_enabled_2fa
+ ability: unrestricted
+
+e2_ep_susp:
+ community_user: sample_enabled_2fa
+ ability: edit_posts
+ is_suspended: true
+ suspension_end: ~
+ suspension_message: go away
diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml
index 82cd00ba7..ad5463bc6 100644
--- a/test/fixtures/users.yml
+++ b/test/fixtures/users.yml
@@ -93,6 +93,16 @@ global_admin:
is_global_moderator: false
confirmed_at: 2020-01-01T00:00:00.000000Z
+staff:
+ email: staff@qpixel-test.net
+ encrypted_password: '$2a$11$roUHXKxecjyQ72Qn7DWs3.9eRCCoRn176kX/UNb/xiue3aGqf7xEW'
+ sign_in_count: 42
+ username: staff
+ is_global_admin: true
+ is_global_moderator: true
+ staff: true
+ confirmed_at: 2020-01-01T00:00:00.000000Z
+
no_community_user:
email: no_community_user@qpixel-test.net
encrypted_password: '$2a$11$roUHXKxecjyQ72Qn7DWs3.9eRCCoRn176kX/UNb/xiue3aGqf7xEW'
diff --git a/test/fixtures/votes.yml b/test/fixtures/votes.yml
index 31627e73b..531e483ec 100644
--- a/test/fixtures/votes.yml
+++ b/test/fixtures/votes.yml
@@ -60,3 +60,10 @@ old_answer_2:
vote_type: 1
recv_user: standard_user
community: sample
+
+on_editor_by_standard:
+ user: standard_user
+ post: question_two
+ vote_type: 1
+ recv_user: editor
+ community: sample
diff --git a/test/helpers/comments_helper_test.rb b/test/helpers/comments_helper_test.rb
index 5d54e4fa1..dce3f4ec7 100644
--- a/test/helpers/comments_helper_test.rb
+++ b/test/helpers/comments_helper_test.rb
@@ -40,7 +40,7 @@ class CommentsHelperTest < ActionView::TestCase
expected = {
'you can go to [category:main]' => "you can go to Main",
'or [category:Meta]' => "or Meta",
- "maybe even to [category##{categories(:high_trust).id}]" => \
+ "maybe even to [category##{categories(:high_trust).id}]" =>
"maybe even to High Trust",
'but not to [category:blah]' => 'but not to [category:blah]'
}
@@ -49,15 +49,63 @@ class CommentsHelperTest < ActionView::TestCase
end
end
- test 'comment_rate_limited prevents new users commenting on others posts' do
- rate_limited, limit_message = comment_rate_limited? users(:basic_user), posts(:question_one)
- assert_equal true, rate_limited
- assert_equal 'As a new user, you can only comment on your own posts and on answers to them.', limit_message
+ test 'comment_rate_limited? should prevent users that reached the daily limit from commenting' do
+ basic = users(:basic_user)
+ std = users(:standard_user)
+ own_post = posts(:question_one)
+ other_post = posts(:question_two)
+
+ SiteSetting['RL_NewUserComments'] = 0
+ SiteSetting['RL_NewUserCommentsOwnPosts'] = 0
+ SiteSetting['RL_Comments'] = 0
+ SiteSetting['RL_CommentsOwnPosts'] = 0
+
+ [basic, std].each do |user|
+ [own_post, other_post].each do |post|
+ rate_limited, limit_message = comment_rate_limited?(user, post)
+ assert_equal true, rate_limited
+
+ if user.same_as?(basic)
+ assert_equal I18n.t('comments.errors.new_user_rate_limited'), limit_message
+ else
+ assert_equal rate_limited_error_msg(user, post), limit_message
+ end
+
+ log = AuditLog.of_type('comment').where(related: post, user: user).order(created_at: :desc).first
+ assert log.present?, 'Expected audit log for attempting to comment on a post while rate-limited to be created'
+ end
+ end
end
- test 'comment_rate_limited allows new user to comment on own post' do
- rate_limited, limit_message = comment_rate_limited? users(:basic_user), posts(:new_user_question)
+ test 'comment_rate_limited? should allow users to comment on their own posts' do
+ basic = users(:basic_user)
+ std = users(:standard_user)
+
+ SiteSetting['RL_NewUserCommentsOwnPosts'] = 50
+
+ rate_limited, limit_message = comment_rate_limited?(basic, posts(:new_user_question))
assert_equal false, rate_limited
- assert_equal nil, limit_message
+ assert_nil limit_message
+
+ SiteSetting['RL_CommentsOwnPosts'] = 50
+
+ rate_limited, limit_message = comment_rate_limited?(std, posts(:question_one))
+ assert_equal false, rate_limited
+ assert_nil limit_message
+ end
+
+ test 'comment_rate_limited? should allow users to comment on posts of others' do
+ basic = users(:basic_user)
+ std = users(:standard_user)
+ other_post = posts(:question_two)
+
+ SiteSetting['RL_NewUserComments'] = 50
+ SiteSetting['RL_Comments'] = 50
+
+ [basic, std].each do |user|
+ rate_limited, limit_message = comment_rate_limited?(user, other_post)
+ assert_equal false, rate_limited
+ assert_nil limit_message
+ end
end
end
diff --git a/test/helpers/search_helper_test.rb b/test/helpers/search_helper_test.rb
index a82e92f6f..11f30c8f1 100644
--- a/test/helpers/search_helper_test.rb
+++ b/test/helpers/search_helper_test.rb
@@ -35,4 +35,168 @@ class SearchHelperTest < ActionView::TestCase
assert_equal expect, date_value_sql(input)
end
end
+
+ test 'qualifiers_to_sql should correctly narrow by :category qualifier' do
+ main = categories(:main)
+ admin_only = categories(:admin_only)
+
+ std_user = users(:standard_user)
+ adm_user = users(:admin)
+
+ posts_query_std = Post.accessible_to(std_user)
+ posts_query_adm = Post.accessible_to(adm_user)
+
+ std_post = [{ param: :category, operator: '=', category_id: main.id }]
+ adm_post = [{ param: :category, operator: '=', category_id: admin_only.id }]
+
+ std_posts_query_standard = qualifiers_to_sql(std_post, posts_query_std, std_user)
+ adm_posts_query_standard = qualifiers_to_sql(adm_post, posts_query_std, std_user)
+ adm_posts_query_admin = qualifiers_to_sql(adm_post, posts_query_adm, adm_user)
+
+ assert_not_equal posts_query_std.size, std_posts_query_standard.size
+ assert_not_equal std_posts_query_standard.size, 0
+
+ assert_not_equal posts_query_adm.size, adm_posts_query_admin.size
+ assert_not_equal adm_posts_query_admin.size, 0
+
+ assert_equal adm_posts_query_standard.size, 0
+ end
+
+ test 'qualifiers_to_sql should correctly narrow by :user qualifier' do
+ std_user = users(:standard_user)
+ edt_user = users(:editor)
+
+ posts_query = Post.accessible_to(std_user)
+ edt_post = [{ param: :user, operator: '=', user_id: edt_user.id }]
+ edt_query = qualifiers_to_sql(edt_post, posts_query, std_user)
+
+ only_editor_posts = edt_query.to_a.all? { |p| p.user.id == edt_user.id }
+
+ assert_not_equal posts_query.size, edt_query.size
+ assert_not_equal edt_query.size, 0
+ assert only_editor_posts
+ end
+
+ test 'qualifiers_to_sql should correctly narrow by :score qualifier' do
+ std_user = users(:standard_user)
+
+ posts_query = Post.accessible_to(std_user)
+ bad_post = [{ param: :score, operator: '<', value: 0.5 }]
+ good_post = [{ param: :score, operator: '>', value: 0.5 }]
+ neut_post = [{ param: :score, operator: '=', value: 0.5 }]
+
+ bad_posts_query = qualifiers_to_sql(bad_post, posts_query, std_user)
+ good_posts_query = qualifiers_to_sql(good_post, posts_query, std_user)
+ neut_posts_query = qualifiers_to_sql(neut_post, posts_query, std_user)
+
+ only_bad_posts = bad_posts_query.to_a.all? { |p| p.score < 0.5 }
+ only_good_posts = good_posts_query.to_a.all? { |p| p.score > 0.5 }
+ only_neut_posts = neut_posts_query.to_a.all? { |p| p.score.to_d == 0.5.to_d }
+
+ assert_not_equal posts_query.size, bad_posts_query.size
+ assert_not_equal bad_posts_query.size, 0
+ assert only_bad_posts
+
+ assert_not_equal posts_query.size, good_posts_query.size
+ assert_not_equal good_posts_query.size, 0
+ assert only_good_posts
+
+ assert_not_equal posts_query.size, neut_posts_query.size
+ assert_not_equal neut_posts_query.size, 0
+ assert only_neut_posts
+ end
+
+ test 'qualifiers_to_sql should correctly narrow by :status qualifier' do
+ std_user = users(:standard_user)
+
+ posts_query = Post.accessible_to(std_user)
+ open_post = [{ param: :status, value: 'open' }]
+ closed_post = [{ param: :status, value: 'closed' }]
+
+ open_query = qualifiers_to_sql(open_post, posts_query, std_user)
+ closed_query = qualifiers_to_sql(closed_post, posts_query, std_user)
+
+ only_open_posts = open_query.to_a.none?(&:closed)
+ only_closed_posts = closed_query.to_a.all?(&:closed)
+
+ assert_not_equal posts_query.size, open_query.size
+ assert_not_equal open_query.size, 0
+ assert only_open_posts
+
+ assert_not_equal posts_query.size, closed_query.size
+ assert_not_equal closed_query.size, 0
+ assert only_closed_posts
+ end
+
+ test 'qualifiers_to_sql should correctly narrow by :upvotes qualifier' do
+ std_user = users(:standard_user)
+
+ posts_query = Post.accessible_to(std_user)
+ upvoted_post = [{ param: :upvotes, operator: '>', value: 0 }]
+ neutral_post = [{ param: :upvotes, operator: '=', value: 0 }]
+
+ upvoted_query = qualifiers_to_sql(upvoted_post, posts_query, std_user)
+ neutral_query = qualifiers_to_sql(neutral_post, posts_query, std_user)
+
+ only_upvoted_posts = upvoted_query.to_a.all? { |p| p[:upvote_count].positive? }
+ only_neutral_posts = neutral_query.to_a.all? { |p| p[:upvote_count].zero? }
+
+ assert_not_equal posts_query.size, upvoted_query.size
+ assert_not_equal upvoted_query.size, 0
+ assert only_upvoted_posts
+
+ assert_not_equal posts_query.size, neutral_query.size
+ assert_not_equal neutral_query.size, 0
+ assert only_neutral_posts
+ end
+
+ test 'qualifiers_to_sql should correctly narrow by :downvotes qualifier' do
+ std_user = users(:standard_user)
+
+ posts_query = Post.accessible_to(std_user)
+ downvoted_post = [{ param: :downvotes, operator: '>', value: 0 }]
+ neutral_post = [{ param: :downvotes, operator: '=', value: 0 }]
+
+ downvoted_query = qualifiers_to_sql(downvoted_post, posts_query, std_user)
+ neutral_query = qualifiers_to_sql(neutral_post, posts_query, std_user)
+
+ only_downvoted_posts = downvoted_query.to_a.all? { |p| p[:downvote_count].positive? }
+ only_neutral_posts = neutral_query.to_a.all? { |p| p[:downvote_count].zero? }
+
+ assert_not_equal posts_query.size, downvoted_query.size
+ assert_not_equal downvoted_query.size, 0
+ assert only_downvoted_posts
+
+ assert_not_equal posts_query.size, neutral_query.size
+ assert_not_equal neutral_query.size, 0
+ assert only_neutral_posts
+ end
+
+ test 'qualifiers_to_sql should correctly narrow by :net_votes qualifier' do
+ std_user = users(:standard_user)
+
+ posts_query = Post.accessible_to(std_user)
+ divisive_post = [{ param: :net_votes, operator: '=', value: 2 }]
+
+ divisive_query = qualifiers_to_sql(divisive_post, posts_query, std_user)
+
+ only_divisive_posts = divisive_query.to_a.all? do |p|
+ (p[:upvote_count] - p[:downvote_count]) == 2
+ end
+
+ assert_not_equal posts_query.size, divisive_query.size
+ assert_not_equal divisive_query.size, 0
+ assert only_divisive_posts
+ end
+
+ test 'search_posts should not show posts in categories that a user cannot view' do
+ std_user = users(:standard_user)
+
+ params = ActionController::Parameters.new({ search: 'high trust' })
+ posts, _qualifiers = search_posts(std_user, params)
+
+ admin_category = categories(:admin_only)
+
+ assert_not(posts.any? { |p| p.category.id == admin_category.id })
+ end
end
diff --git a/test/integration/tasks_integration_test.rb b/test/integration/tasks_integration_test.rb
index f4d9f72ed..3adb67256 100644
--- a/test/integration/tasks_integration_test.rb
+++ b/test/integration/tasks_integration_test.rb
@@ -5,18 +5,18 @@ class TasksIntegrationTest < ActionDispatch::IntegrationTest
test 'should deny access to anonymous users' do
get '/maintenance'
- assert_response 403
+ assert_response(:forbidden)
end
test 'should deny access to non-developers' do
sign_in users(:admin)
get '/maintenance'
- assert_response 403
+ assert_response(:forbidden)
end
test 'should grant access to developers' do
sign_in users(:developer)
get '/maintenance'
- assert_response 200
+ assert_response(:success)
end
end
diff --git a/test/jobs/send_summary_emails_job_test.rb b/test/jobs/send_summary_emails_job_test.rb
new file mode 100644
index 000000000..3207ff816
--- /dev/null
+++ b/test/jobs/send_summary_emails_job_test.rb
@@ -0,0 +1,21 @@
+require 'test_helper'
+
+class SendSummaryEmailsJobTest < ActiveJob::TestCase
+ include ActionMailer::TestCase::ClearTestDeliveries
+
+ test 'should correctly send summary emails' do
+ perform_enqueued_jobs do
+ SendSummaryEmailsJob.perform_later
+ end
+
+ assert_performed_jobs(2)
+
+ delivered = SummaryMailer.deliveries.first
+
+ to_email = users(:staff).email
+
+ assert_equal 1, delivered.recipients.size
+ assert delivered.recipients.include?(to_email),
+ "Expected #{to_email} to be a recipient, actual: #{delivered.recipients.join(', ')}"
+ end
+end
diff --git a/test/mailers/admin_mailer_test.rb b/test/mailers/admin_mailer_test.rb
index 4121d891f..e13b553d1 100644
--- a/test/mailers/admin_mailer_test.rb
+++ b/test/mailers/admin_mailer_test.rb
@@ -1,7 +1,21 @@
require 'test_helper'
class AdminMailerTest < ActionMailer::TestCase
- # test "the truth" do
- # assert true
- # end
+ test 'to_moderators' do
+ email = AdminMailer.with(body_markdown: 'test', subject: 'test', community: communities(:sample)).to_moderators
+ assert_emails 1 do
+ email.deliver_later
+ end
+ assert_operator email.from[0].length, :>, 3, 'Sender appears to be empty or default value'
+ end
+
+ test 'to_all_users' do
+ email = AdminMailer.with(body_markdown: 'test', subject: 'test', users: ['test1@example.com'],
+ community: communities(:sample))
+ .to_all_users
+ assert_emails 1 do
+ email.deliver_later
+ end
+ assert_operator email.from[0].length, :>, 3, 'Sender appears to be empty or default value'
+ end
end
diff --git a/test/mailers/donation_mailer_test.rb b/test/mailers/donation_mailer_test.rb
index d87953c8c..baf6b0003 100644
--- a/test/mailers/donation_mailer_test.rb
+++ b/test/mailers/donation_mailer_test.rb
@@ -1,7 +1,20 @@
require 'test_helper'
class DonationMailerTest < ActionMailer::TestCase
- # test "the truth" do
- # assert true
- # end
+ test 'donation_successful should correctly send donation emails' do
+ sender_email = SiteSetting['DonationSenderEmail']
+ pi = Stripe::PaymentIntent.new.to_json
+ user = users(:standard_user)
+
+ job = DonationMailer.with(currency: '£', amount: 1000, email: user.email, name: user.username, intent: pi)
+ .donation_uncaptured
+ .deliver_later
+
+ job.perform_now
+
+ delivered = DonationMailer.deliveries.first
+
+ assert_equal 1, delivered.from.length
+ assert delivered.from.include?(sender_email)
+ end
end
diff --git a/test/mailers/flag_mailer_test.rb b/test/mailers/flag_mailer_test.rb
index ea9a8de37..4115e3c95 100644
--- a/test/mailers/flag_mailer_test.rb
+++ b/test/mailers/flag_mailer_test.rb
@@ -1,7 +1,22 @@
require 'test_helper'
class FlagMailerTest < ActionMailer::TestCase
- # test "the truth" do
- # assert true
- # end
+ test 'flag_escalated should correctly send flag escalation emails' do
+ assert_emails 1 do
+ flag = flags(:escalated)
+
+ FlagMailer.with(flag: flag).flag_escalated.deliver_now
+ end
+ end
+
+ test 'flag_escalated should not fail if the flagged post has been updated before escalation' do
+ assert_emails 1 do
+ flag = flags(:escalated)
+
+ # forces 'post modified after flag' to be rendered
+ flag.post.update(updated_at: DateTime.now)
+
+ FlagMailer.with(flag: flag).flag_escalated.deliver_now
+ end
+ end
end
diff --git a/test/mailers/previews/devise_mailer_preview.rb b/test/mailers/previews/devise_mailer_preview.rb
index bb090c90a..f4b468fd6 100644
--- a/test/mailers/previews/devise_mailer_preview.rb
+++ b/test/mailers/previews/devise_mailer_preview.rb
@@ -2,4 +2,8 @@ class DeviseMailerPreview < ActionMailer::Preview
def confirmation_instructions
Devise::Mailer.confirmation_instructions(User.first, 'faketoken')
end
+
+ def password_change
+ Devise::Mailer.password_change(User.first)
+ end
end
diff --git a/test/mailers/previews/flag_mailer_preview.rb b/test/mailers/previews/flag_mailer_preview.rb
index ac93c4c7e..ee7fe75f8 100644
--- a/test/mailers/previews/flag_mailer_preview.rb
+++ b/test/mailers/previews/flag_mailer_preview.rb
@@ -1,3 +1,6 @@
# Preview all emails at http://localhost:3000/rails/mailers/flag_mailer
class FlagMailerPreview < ActionMailer::Preview
+ def flag_escalated
+ FlagMailer.with(flag: Flag.escalated.last || Flag.last).flag_escalated
+ end
end
diff --git a/test/mailers/previews/summary_mailer_preview.rb b/test/mailers/previews/summary_mailer_preview.rb
new file mode 100644
index 000000000..6bf5e4afd
--- /dev/null
+++ b/test/mailers/previews/summary_mailer_preview.rb
@@ -0,0 +1,17 @@
+# Preview all emails at http://localhost:3000/rails/mailers/summary_mailer
+class SummaryMailerPreview < ActionMailer::Preview
+ def content_summary
+ test_timeframe = 1.year
+ staff = User.where(staff: true)
+ posts = Post.unscoped.qa_only.where(created_at: test_timeframe.ago..DateTime.now)
+ .includes(:community, :user)
+ flags = Flag.unscoped.where(created_at: test_timeframe.ago..DateTime.now)
+ .includes(:post, :community, :user)
+ comments = Comment.unscoped.where(created_at: test_timeframe.ago..DateTime.now)
+ .includes(:user, :post, :comment_thread, post: :community)
+ users = User.where(created_at: test_timeframe.ago..DateTime.now).includes(:community_users)
+
+ SummaryMailer.with(to: staff.first.email, posts: posts.to_a, flags: flags.to_a, comments: comments.to_a, users: users.to_a)
+ .content_summary
+ end
+end
diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb
new file mode 100644
index 000000000..140260392
--- /dev/null
+++ b/test/mailers/previews/user_mailer_preview.rb
@@ -0,0 +1,7 @@
+# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
+class UserMailerPreview < ActionMailer::Preview
+ def deletion_confirmation
+ @user = User.last
+ UserMailer.with(user: @user).deletion_confirmation
+ end
+end
diff --git a/test/mailers/summary_mailer_test.rb b/test/mailers/summary_mailer_test.rb
new file mode 100644
index 000000000..c52ffc1a6
--- /dev/null
+++ b/test/mailers/summary_mailer_test.rb
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class SummaryMailerTest < ActionMailer::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb
new file mode 100644
index 000000000..3fed9bb33
--- /dev/null
+++ b/test/mailers/user_mailer_test.rb
@@ -0,0 +1,12 @@
+require 'test_helper'
+
+class UserMailerTest < ActionMailer::TestCase
+ test 'deletion_confirmation' do
+ email = UserMailer.with(user: users(:standard_user), host: communities(:sample).host)
+ .deletion_confirmation
+ assert_emails 1 do
+ email.deliver_later
+ end
+ assert_operator email.from[0].length, :>, 3, 'Sender for deletion confirmation appears empty or default value'
+ end
+end
diff --git a/test/models/community_user_test.rb b/test/models/community_user_test.rb
new file mode 100644
index 000000000..b775496f9
--- /dev/null
+++ b/test/models/community_user_test.rb
@@ -0,0 +1,23 @@
+require 'test_helper'
+
+class CommunityUserTest < ActiveSupport::TestCase
+ test 'score getters should correctly calculate scores' do
+ std = community_users(:sample_standard_user)
+
+ [:edit_score, :flag_score, :post_score].each do |name|
+ next unless std.respond_to?(name)
+
+ score = std.send(name)
+ assert score.positive? && score < 1
+ end
+ end
+
+ test 'latest_warning should return the timestamp of the latest warning, if any' do
+ std = community_users(:sample_standard_user)
+
+ latest = mod_warnings.select { |mw| mw.community_user == std }
+ .min { |a, b| a.created_at > b.created_at ? 1 : -1 }
+
+ assert_equal std.latest_warning, latest&.created_at
+ end
+end
diff --git a/test/models/concerns/identity_test.rb b/test/models/concerns/identity_test.rb
new file mode 100644
index 000000000..60f5788ef
--- /dev/null
+++ b/test/models/concerns/identity_test.rb
@@ -0,0 +1,49 @@
+require 'test_helper'
+
+class IdentityTest < ActiveSupport::TestCase
+ def setup
+ @klass1 = Class.new do
+ include Identity
+ def initialize(id)
+ super()
+ @id = id
+ end
+ attr_accessor :id
+ end
+
+ @klass2 = Class.new do
+ include Identity
+ def initialize(id)
+ super()
+ @id = id
+ end
+ attr_accessor :id
+ end
+ end
+
+ test 'same_as? should correctly determine identity' do
+ first = @klass1.new(42)
+ second = @klass1.new(42)
+ third = @klass1.new(777)
+
+ assert first.same_as?(second)
+ assert_not first.same_as?(third)
+ end
+
+ test 'same_as? should ensure compared models are the same' do
+ first = @klass1.new(42)
+ second = @klass2.new(42)
+
+ assert_not first.same_as?(second)
+ end
+
+ test 'same_as? should not fail if the compared model is nil' do
+ first = @klass1.new(42)
+
+ assert_nothing_raised do
+ first.same_as?(nil)
+ end
+
+ assert_not first.same_as?(nil)
+ end
+end
diff --git a/test/models/flag_test.rb b/test/models/flag_test.rb
index 34aac66b3..09d62f85c 100644
--- a/test/models/flag_test.rb
+++ b/test/models/flag_test.rb
@@ -6,4 +6,12 @@ class FlagTest < ActiveSupport::TestCase
test 'is post related' do
assert_post_related(Flag)
end
+
+ test 'confidential?' do
+ normal = flags(:one)
+ secret = flags(:confidential_on_deleter)
+
+ assert_equal normal.confidential?, false
+ assert_equal secret.confidential?, true
+ end
end
diff --git a/test/models/post_history_test.rb b/test/models/post_history_test.rb
index 156f729d2..2a6b504a1 100644
--- a/test/models/post_history_test.rb
+++ b/test/models/post_history_test.rb
@@ -36,4 +36,19 @@ class PostHistoryTest < ActiveSupport::TestCase
assert_equal post.body_markdown, event.after_state
assert_nil event.before_state
end
+
+ test 'allowed_to_see_details? should correctly check if a given user can acces a revision' do
+ editor = users(:editor)
+ mod = users(:moderator)
+ admin = users(:admin)
+
+ hidden = post_histories(:question_one_hidden_revision)
+
+ assert_equal hidden.allowed_to_see_details?(editor), false
+ assert_equal hidden.allowed_to_see_details?(mod), false
+ assert_equal hidden.allowed_to_see_details?(admin), true
+
+ # post author should always see history items
+ assert_equal hidden.allowed_to_see_details?(hidden.post.user), true
+ end
end
diff --git a/test/models/post_test.rb b/test/models/post_test.rb
index e4b0105bc..befb2b3a9 100644
--- a/test/models/post_test.rb
+++ b/test/models/post_test.rb
@@ -47,6 +47,24 @@ class PostTest < ActiveSupport::TestCase
end
end
+ test 'accessible_to should correctly check user access' do
+ adm_user = users(:admin)
+ mod_user = users(:moderator)
+ std_user = users(:standard_user)
+
+ adm_posts = Post.accessible_to(adm_user)
+ mod_posts = Post.accessible_to(mod_user)
+ std_posts = Post.accessible_to(std_user)
+
+ can_admin_get_deleted_posts = adm_posts.any?(&:deleted)
+ can_mod_get_deleted_posts = mod_posts.any?(&:deleted)
+ can_user_get_deleted_posts = std_posts.any?(&:deleted)
+
+ assert can_admin_get_deleted_posts
+ assert can_mod_get_deleted_posts
+ assert_not can_user_get_deleted_posts
+ end
+
test 'should allow specified post types in a category' do
category = categories(:main)
post_type = post_types(:question)
@@ -81,7 +99,27 @@ class PostTest < ActiveSupport::TestCase
post_with_reactions = posts(:answer_one)
reaction_list = post_with_reactions.reaction_list
refute reaction_list.empty?
- assert reaction_list.key? reaction_types(:wfm)
+ assert reaction_list.key?(reaction_types(:wfm))
assert_equal 1, reaction_list[reaction_types(:wfm)].count
end
+
+ test 'moderator_tags should correctly validate if the user can use moderator tags on posts' do
+ category = categories(:moderator_tags)
+ post_type = post_types(:question)
+
+ users.each do |user|
+ # moderator_tags validation requires request context user to be set
+ RequestContext.user = user
+
+ post = Post.new(body_markdown: 'm' * category.min_body_length, body: "
#{'b' * category.min_body_length}
",
+ title: 't' * category.min_title_length, tags_cache: ['feature-request'], license: licenses(:cc_by_sa),
+ score: 0, user: user, post_type: post_type, category: category)
+
+ if post.valid?
+ assert_equal user.at_least_moderator?, true
+ else
+ assert_not_empty post.errors[:mod_tags]
+ end
+ end
+ end
end
diff --git a/test/models/request_context_test.rb b/test/models/request_context_test.rb
index 6a666dfc3..cf04401d9 100644
--- a/test/models/request_context_test.rb
+++ b/test/models/request_context_test.rb
@@ -36,7 +36,7 @@ class RequestContextTest < ActiveSupport::TestCase
worker1 = Thread.new do
RequestContext.community = @community2
- sleep 0.5
+ sleep(0.5)
# this check runs second
assert_equal RequestContext.community, @community2
diff --git a/test/models/site_setting_test.rb b/test/models/site_setting_test.rb
index 053c54009..5a72d80d2 100644
--- a/test/models/site_setting_test.rb
+++ b/test/models/site_setting_test.rb
@@ -53,4 +53,12 @@ class SiteSettingTest < ActiveSupport::TestCase
SiteSetting.create(community_id: nil, name: 'test', value: 'bar', value_type: 'string')
assert_equal SiteSetting.where(name: 'test').first, setting1
end
+
+ test 'text? should correctly check if the setting accepts long text values' do
+ text_setting = site_settings(:text)
+ int_setting = site_settings(:int)
+
+ assert_equal text_setting.text?, true
+ assert_equal int_setting.text?, false
+ end
end
diff --git a/test/models/subscription_test.rb b/test/models/subscription_test.rb
index da38c7544..899318c56 100644
--- a/test/models/subscription_test.rb
+++ b/test/models/subscription_test.rb
@@ -9,16 +9,14 @@ class SubscriptionTest < ActiveSupport::TestCase
test 'subscription to all should return some questions' do
questions = subscriptions(:all).questions
- assert_not_nil questions
- assert_not questions.empty?, 'No questions returned'
- assert questions.size <= 100, 'Too many questions returned'
+
+ assert_questions_valid(questions)
end
test 'tag subscription should return only tag questions' do
questions = subscriptions(:tag).questions
- assert_not_nil questions
- assert_not questions.empty?, 'No questions returned'
- assert questions.size <= 100, 'Too many questions returned'
+
+ assert_questions_valid(questions)
questions.each do |question|
assert question.tags.map(&:name).include?(subscriptions(:tag).qualifier),
"Tag subscription returned question #{question.id} without specified tag"
@@ -27,12 +25,45 @@ class SubscriptionTest < ActiveSupport::TestCase
test 'user subscription should return only user questions' do
questions = subscriptions(:user).questions
- assert_not_nil questions
- assert_not questions.empty?, 'No questions returned'
- assert questions.size <= 100, 'Too many questions returned'
+
+ assert_questions_valid(questions)
questions.each do |question|
assert question.user_id == subscriptions(:user).qualifier.to_i,
"User subscription returned question #{question.id} not from specified user"
end
end
+
+ test 'interesting subscription should return only questions with score higher than the threshold' do
+ threshold = 0.5
+
+ SiteSetting['InterestingSubscriptionScoreThreshold'] = threshold
+
+ questions = subscriptions(:interesting).questions
+
+ assert_questions_valid(questions)
+ questions.each do |question|
+ assert question.score >= threshold,
+ "Expected question #{question.id} with a score of #{question.score} to be excluded"
+ end
+ end
+
+ test 'category subscription should return only questions from a specific category' do
+ category = categories(:main)
+ questions = subscriptions(:category).questions
+
+ assert_questions_valid(questions)
+ questions.each do |question|
+ assert question.category == category,
+ "Expected quesiton #{question.id} to be from the #{category.name} category," \
+ "actual: #{question.category.name || 'no category'}"
+ end
+ end
+
+ private
+
+ def assert_questions_valid(questions)
+ assert_not_nil(questions)
+ assert_not(questions.empty?, 'No questions returned')
+ assert(questions.size <= 100, 'Too many questions returned')
+ end
end
diff --git a/test/models/user_test.rb b/test/models/user_test.rb
index 9d8cfd64b..290f267b7 100644
--- a/test/models/user_test.rb
+++ b/test/models/user_test.rb
@@ -1,6 +1,22 @@
require 'test_helper'
class UserTest < ActiveSupport::TestCase
+ test 'search should correctly narrow down users by username' do
+ users = User.search('deleted')
+
+ users.each do |u|
+ assert_equal true, u.username.include?('deleted')
+ end
+ end
+
+ test 'search should match any substring in usernames' do
+ users = User.search('oderat')
+
+ users.each do |u|
+ assert_equal true, u.username.include?('oderat')
+ end
+ end
+
test 'users should be destructible in a single call' do
assert_nothing_raised do
users(:standard_user).destroy!
@@ -18,14 +34,14 @@ class UserTest < ActiveSupport::TestCase
end
test 'has_post_privilege should grant all to OP' do
- assert_equal true, users(:standard_user).has_post_privilege?('flag_curate', posts(:question_one))
+ assert_equal true, users(:standard_user).post_privilege?('flag_curate', posts(:question_one))
end
test 'website_domain should strip out everything but domain' do
assert_equal 'example.com', users(:closer).website_domain
end
- test 'can_update should determine if the user can update a given post' do
+ test 'can_update? should determine if the user can update a given post' do
basic_user = users(:basic_user)
post_owner = users(:standard_user)
category = categories(:main)
@@ -41,21 +57,21 @@ class UserTest < ActiveSupport::TestCase
post_type: post_type,
category: category)
- assert_equal true, post_owner.can_update(post, post_type)
- assert_equal false, basic_user.can_update(post, post_type)
- assert_equal true, users(:moderator).can_update(post, post_type)
- assert_equal true, users(:editor).can_update(post, post_type)
+ assert_equal true, post_owner.can_update?(post, post_type)
+ assert_equal false, basic_user.can_update?(post, post_type)
+ assert_equal true, users(:moderator).can_update?(post, post_type)
+ assert_equal true, users(:editor).can_update?(post, post_type)
basic_user.community_user.grant_privilege!('unrestricted')
- assert_equal false, basic_user.can_update(post, post_type)
- assert_equal true, basic_user.can_update(post, post_types(:free_edit))
+ assert_equal false, basic_user.can_update?(post, post_type)
+ assert_equal true, basic_user.can_update?(post, post_types(:free_edit))
end
- test 'can_push_to_network should determine if the user can push updates to network' do
+ test 'can_push_to_network? should determine if the user can push updates to network' do
post_type = post_types(:help_doc)
- assert_equal false, users(:standard_user).can_push_to_network(post_type)
- assert_equal true, users(:global_moderator).can_push_to_network(post_type)
- assert_equal true, users(:global_admin).can_push_to_network(post_type)
+ assert_equal false, users(:standard_user).can_push_to_network?(post_type)
+ assert_equal true, users(:global_moderator).can_push_to_network?(post_type)
+ assert_equal true, users(:global_admin).can_push_to_network?(post_type)
end
test 'community_user is based on context' do
@@ -67,30 +83,42 @@ class UserTest < ActiveSupport::TestCase
assert_equal user.community_user.reload, cu1
end
- test 'is_moderator for community moderator' do
- assert_equal users(:moderator).is_moderator, true
+ test 'at_least_moderator? for community moderator' do
+ assert_equal users(:moderator).at_least_moderator?, true
end
- test 'is_moderator for community moderator in another context' do
+ test 'at_least_moderator? for community moderator in another context' do
RequestContext.community = Community.create(host: 'other', name: 'Other')
- assert_equal users(:moderator).is_moderator, false
+ assert_equal users(:moderator).at_least_moderator?, false
+ end
+
+ test 'at_least_moderator? for global moderator' do
+ assert_equal users(:global_moderator).at_least_moderator?, true
end
- test 'is_moderator for global moderator' do
- assert_equal users(:global_moderator).is_moderator, true
+ test 'at_least_global_moderator?' do
+ admin = users(:admin)
+ mod = users(:moderator)
+ global_admin = users(:global_admin)
+ global_mod = users(:global_moderator)
+
+ assert_equal admin.at_least_global_moderator?, false
+ assert_equal mod.at_least_global_moderator?, false
+ assert_equal global_mod.at_least_global_moderator?, true
+ assert_equal global_admin.at_least_global_moderator?, true
end
- test 'is_admin for community admin' do
- assert_equal users(:admin).is_admin, true
+ test 'admin? for community admin' do
+ assert_equal users(:admin).admin?, true
end
- test 'is_admin for community admin in another context' do
+ test 'admin? for community admin in another context' do
RequestContext.community = Community.create(host: 'other', name: 'Other')
- assert_equal users(:admin).is_admin, false
+ assert_equal users(:admin).admin?, false
end
- test 'is_admin for global admin' do
- assert_equal users(:global_admin).is_admin, true
+ test 'admin? for global admin' do
+ assert_equal users(:global_admin).admin?, true
end
test 'ensure_community_user! does not alter existing community_user' do
@@ -121,4 +149,155 @@ class UserTest < ActiveSupport::TestCase
assert_equal cu, current_cu
assert_equal user.community_users.count, original_count + 1
end
+
+ test 'moderator_on? should only be true for users that are moderators or admins on a community' do
+ community = communities(:sample)
+ basic = users(:basic_user)
+ std = users(:standard_user)
+ mod = users(:moderator)
+ admin = users(:admin)
+
+ assert_equal basic.moderator_on?(community.id), false
+ assert_equal std.moderator_on?(community.id), false
+ assert_equal mod.moderator_on?(community.id), true
+ assert_equal admin.moderator_on?(community.id), true
+ end
+
+ test 'moderator_on? should always be true for global moderators and admins with profile on a community' do
+ global_mod = users(:global_moderator)
+ global_admin = users(:global_admin)
+
+ communities.each do |c|
+ assert_equal global_mod.moderator_on?(c.id), global_mod.profile_on?(c.id)
+ assert_equal global_admin.moderator_on?(c.id), global_admin.profile_on?(c.id)
+ end
+ end
+
+ test 'ability_on? should be false for users that do not have a profile on a community' do
+ fake = communities(:fake)
+ basic = users(:basic_user)
+ std = users(:standard_user)
+ mod = users(:moderator)
+ admin = users(:admin)
+
+ abilities.each do |ability|
+ assert_equal basic.ability_on?(fake.id, ability.internal_id), false
+ assert_equal std.ability_on?(fake.id, ability.internal_id), false
+ assert_equal mod.ability_on?(fake.id, ability.internal_id), false
+ assert_equal admin.ability_on?(fake.id, ability.internal_id), false
+ end
+ end
+
+ test 'ability_on? should always be true for moderators and admins with profile on a community' do
+ community = communities(:sample)
+ mod = users(:moderator)
+ admin = users(:admin)
+
+ abilities.each do |ability|
+ assert_equal mod.ability_on?(community.id, ability.internal_id), true
+ assert_equal admin.ability_on?(community.id, ability.internal_id), true
+ end
+ end
+
+ test 'ability_on? should return true for every undeleted user with profile on a community' do
+ everyone = abilities(:everyone)
+
+ communities.each do |community|
+ CommunityUser.unscoped.undeleted.where(community_id: community.id).each do |cu|
+ unless cu.user.deleted
+ assert_equal cu.user.ability_on?(community.id, everyone.internal_id), true
+ end
+ end
+ end
+ end
+
+ test 'ability_on? should correctly check for unrestricted ability' do
+ community = communities(:sample)
+ basic = users(:basic_user)
+ system = users(:system)
+
+ unrestricted = abilities(:unrestricted)
+
+ [basic, system].each do |user|
+ assert_equal user.ability_on?(community.id, unrestricted.internal_id), false
+ end
+
+ CommunityUser.unscoped.undeleted.where(community_id: community.id).where.not(user_id: [basic.id, system.id]).each do |cu|
+ assert_equal cu.user.ability_on?(community.id, unrestricted.internal_id), !cu.user.deleted
+ end
+ end
+
+ test 'metric should correctly return user stats' do
+ std = users(:editor)
+
+ ['p', '1', '2', 's', 'v', 'V', 'E'].each do |name|
+ count = std.metric(name)
+ assert count.positive?, "Expected metric #{name} to be positive, actual: #{count}"
+ end
+ end
+
+ test 'no_blank_unicode_in_username validation should fail if the username contains blank Unicode chars' do
+ user = User.new(id: 42, username: "\u200BWhy\u200Bso\u200Bmuch\u200Bspace?")
+
+ assert_equal false, user.valid?
+ assert(user.errors[:username]&.any? { |m| m.include?('blank unicode') })
+ end
+
+ test 'no_links_in_username validation should fail if the username contains URLs' do
+ user = User.new(id: 42, username: 'Visit our https://example.com site!')
+
+ assert_equal false, user.valid?
+ assert(user.errors[:username]&.any? { |m| m.include?('links') })
+ end
+
+ test 'username_not_fake_admin validation should fail if the username contains a resticted badge' do
+ admin_badge = SiteSetting['AdminBadgeCharacter']
+ mod_badge = SiteSetting['ModBadgeCharacter']
+
+ [admin_badge, mod_badge].each do |badge|
+ user = User.new(id: 42, username: "I am totally a #{badge}")
+
+ assert_equal false, user.valid?
+ assert(user.errors[:username]&.any? { |m| m.include?(badge) })
+ end
+ end
+
+ test 'inspect should work with the model' do
+ std = users(:standard_user)
+
+ assert_nothing_raised do
+ std.inspect
+ end
+ end
+
+ test 'moderator_communities should correctly list mod communities' do
+ Community.create(name: 'Test', host: 'test.host')
+
+ global_result = users(:global_moderator).moderator_communities
+ assert_equal Community.all.size, global_result.size
+
+ local_result = users(:moderator).moderator_communities
+ assert_equal 1, local_result.size
+ end
+
+ test 'admin_communities should correctly list admin communities' do
+ Community.create(name: 'Test', host: 'test.host')
+
+ global_result = users(:global_admin).admin_communities
+ assert_equal Community.all.size, global_result.size
+
+ local_result = users(:admin).admin_communities
+ assert_equal 1, local_result.size
+ end
+
+ test 'not_blocklisted? should correctly determine if the user is blocklisted' do
+ std = users(:standard_user)
+
+ std.skip_reconfirmation!
+ std.update(email: blocked_items(:email).value)
+ std.valid?
+
+ assert_equal false, std.changed?
+ assert std.errors[:base].intersect?(ApplicationRecord.useful_err_msg)
+ end
end
diff --git a/test/models/vote_test.rb b/test/models/vote_test.rb
index 69cffe196..40e13e9ee 100644
--- a/test/models/vote_test.rb
+++ b/test/models/vote_test.rb
@@ -26,21 +26,28 @@ class VoteTest < ActiveSupport::TestCase
rep_change_down = cpt.downvote_rep
expected_rep_change = (3 * rep_change_up) + (2 * rep_change_down)
- post.votes.create([
- { user: users(:standard_user), recv_user: author, vote_type: 1 },
- { user: users(:closer), recv_user: author, vote_type: 1 },
- { user: users(:deleter), recv_user: author, vote_type: 1 },
- { user: users(:moderator), recv_user: author, vote_type: -1 },
- { user: users(:admin), recv_user: author, vote_type: -1 }
- ])
-
- assert_equal post.votes.count, 5
- assert_equal post.upvote_count, 3
- assert_equal post.downvote_count, 2
+ new_votes = [
+ { user: users(:standard_user), recv_user: author, vote_type: 1 },
+ { user: users(:closer), recv_user: author, vote_type: 1 },
+ { user: users(:deleter), recv_user: author, vote_type: 1 },
+ { user: users(:moderator), recv_user: author, vote_type: -1 },
+ { user: users(:admin), recv_user: author, vote_type: -1 }
+ ]
+
+ post.votes.create(new_votes)
+
+ num_votes = new_votes.length
+ num_upvotes = new_votes.inject(0) { |a, c| a + (c[:vote_type] == 1 ? 1 : 0) }
+ num_downvotes = new_votes.inject(0) { |a, c| a + (c[:vote_type] == -1 ? 1 : 0) }
+
+ # NB: fixtures can & should be able to add more votes than created here
+ assert post.votes.count >= num_votes, "Expected more than #{num_votes} votes, actual: #{post.votes.count}"
+ assert post.upvote_count >= num_upvotes, "Expected more than #{num_upvotes} upvotes, actual: #{post.upvote_count}"
+ assert post.downvote_count >= num_downvotes, "Expected more than #{num_downvotes} downvotes, actual: #{post.downvote_count}"
assert_equal author.reputation, previous_rep + expected_rep_change
end
- test 'Vote.total_rep_change should result in correct rep change for given votes' do
+ test 'total_rep_change should result in correct rep change for given votes' do
post = posts(:answer_one)
cpt = CategoryPostType.find_by(category: posts(:answer_one).category, post_type: posts(:answer_one).post_type)
rep_change_up = cpt.upvote_rep
diff --git a/test/support/application_test_helper.rb b/test/support/application_test_helper.rb
index 5050aa883..a1c512180 100644
--- a/test/support/application_test_helper.rb
+++ b/test/support/application_test_helper.rb
@@ -1,8 +1,8 @@
module ApplicationTestHelper
def assert_array_equal(expected, object)
[:include?, :each, :size].each do |method|
- assert expected.respond_to? method, "Expected `expected' to be array-like, got #{expected.class}"
- assert object.respond_to? method, "Expected `object' to be array-like, got #{object.class}"
+ assert expected.respond_to?(method, "Expected `expected' to be array-like, got #{expected.class}")
+ assert object.respond_to?(method, "Expected `object' to be array-like, got #{object.class}")
end
assert expected.size == object.size, "Array sizes are unequal\n+++#{object}\n---#{expected}"
diff --git a/test/system/post_test.rb b/test/system/post_system_test.rb
similarity index 94%
rename from test/system/post_test.rb
rename to test/system/post_system_test.rb
index d99cd266c..ddc6f31ed 100644
--- a/test/system/post_test.rb
+++ b/test/system/post_system_test.rb
@@ -1,6 +1,8 @@
require 'application_system_test_case'
-class PostTest < ApplicationSystemTestCase
+# Renamed from PostTest to PostSystemTest to avoid clash with test/models/post_test.rb
+# when running rails test:all
+class PostSystemTest < ApplicationSystemTestCase
# -------------------------------------------------------
# Create
# -------------------------------------------------------
@@ -126,7 +128,7 @@ class PostTest < ApplicationSystemTestCase
# Check that answers are displayed somewhere on the page
assert post.children.any?, 'The post for this system test should have answers'
- post.children.where(deleted: false).each do |child|
+ post.children.undeleted.each do |child|
assert_text child.body
end
end
@@ -137,6 +139,8 @@ class PostTest < ApplicationSystemTestCase
click_on 'Active'
+ assert post.children.count > 1, 'Answer buttons are only shown for posts with more than one answer'
+
assert_current_path post_url(post, sort: 'active')
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index db21f1264..63ef1fa97 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,13 +1,14 @@
require 'simplecov'
require 'simplecov_json_formatter'
SimpleCov.formatter = SimpleCov::Formatter::JSONFormatter
-SimpleCov.start 'rails'
+SimpleCov.start('rails')
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)
require 'rails/test_help'
require 'minitest/ci'
+require 'minitest/mock'
Minitest::Ci.report_dir = Rails.root.join('test/reports/minitest').to_s
# cleanup seeds after all tests are run (can't use teardown callbacks as they run after each test)
@@ -49,6 +50,8 @@
EmailLog,
ErrorLog,
Subscription,
+ MicroAuth::Token,
+ MicroAuth::App,
User,
Notification,
SiteSetting,
@@ -71,15 +74,20 @@
end
end
-Dir.glob(Rails.root.join('test/support/**/*.rb')).sort.each { |f| require f }
+Dir.glob(Rails.root.join('test/support/**/*.rb')).each { |f| require f }
class ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
setup :set_request_context
- teardown :clear_cache
+ teardown do
+ clear_enqueued_jobs
+ clear_cache
+ end
protected
@@ -87,7 +95,7 @@ class ActiveSupport::TestCase
# This means that we can leverage it's smart transaction behavior to significantly speed up our tests (by a factor of 6).
def load_fixtures(config)
# Loading a fixture deletes all data in the same tables, so it has to happen before we load our normal seeds.
- fixture_data = super(config)
+ fixture_data = super
load_tags_paths
load_seeds
@@ -107,7 +115,8 @@ def load_seeds
end
def load_tags_paths
- ActiveRecord::Base.connection.execute File.read(Rails.root.join('db/scripts/create_tags_path_view.sql'))
+ sql = File.read(Rails.root.join('db/scripts/create_tags_path_view.sql'))
+ ActiveRecord::Base.connection.execute(sql)
end
def clear_cache
@@ -120,6 +129,22 @@ def copy_abilities(community_id)
end
end
+ def assert_valid_json_response
+ assert_nothing_raised do
+ parsed = JSON.parse(response.body)
+ assert_not_nil(parsed)
+ end
+ end
+
+ def assert_json_response_message(expected)
+ assert_equal expected, JSON.parse(response.body)['message']
+ end
+
+ def assert_redirected_to_sign_in
+ assert_response(:found)
+ assert_redirected_to(new_user_session_path)
+ end
+
PostMock = Struct.new(:title, :body_markdown, :body, :tags_cache, :edit, keyword_init: true)
def sample
diff --git a/tsconfig.json b/tsconfig.json
index a1b8adeed..5556dbb7b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -6,7 +6,7 @@
"module": "none",
"noEmit": true,
"target": "ES2021",
- "types": ["./global.d.ts", "jquery", "select2"]
+ "types": ["./global.d.ts", "@types/jquery", "@types/select2"]
},
"include": ["./app/assets/javascripts"],
"exclude": [