Improve speed of model tests by around 50/60 seconds (#5016)
This commit is contained in:
parent
b9734583bb
commit
9c05e618db
60 changed files with 1846 additions and 1619 deletions
|
|
@ -44,7 +44,7 @@ class ChatChannel < ApplicationRecord
|
|||
end
|
||||
|
||||
def clear_channel
|
||||
messages.delete_all
|
||||
messages.destroy_all
|
||||
Pusher.trigger(pusher_channels, "channel-cleared", { chat_channel_id: id }.to_json)
|
||||
true
|
||||
rescue Pusher::Error => e
|
||||
|
|
|
|||
|
|
@ -97,6 +97,19 @@ class Comment < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def self.users_with_number_of_comments(user_ids, before_date)
|
||||
joins(:user).
|
||||
select("users.username, COUNT(comments.user_id) AS number_of_comments").
|
||||
where(user_id: user_ids).
|
||||
where(arel_table[:created_at].gt(before_date)).
|
||||
group(User.arel_table[:username]).
|
||||
order("number_of_comments DESC")
|
||||
end
|
||||
|
||||
def self.tree_for(commentable, limit = 0)
|
||||
commentable.comments.includes(:user).arrange(order: "score DESC").to_a[0..limit - 1].to_h
|
||||
end
|
||||
|
||||
def self.trigger_index(record, remove)
|
||||
# record is removed from index synchronously in before_destroy_actions
|
||||
return if remove
|
||||
|
|
@ -108,28 +121,12 @@ class Comment < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def self.users_with_number_of_comments(user_ids, before_date)
|
||||
joins(:user).
|
||||
select("users.username, COUNT(comments.user_id) AS number_of_comments").
|
||||
where(user_id: user_ids).
|
||||
where(arel_table[:created_at].gt(before_date)).
|
||||
group(User.arel_table[:username]).
|
||||
order("number_of_comments DESC")
|
||||
end
|
||||
|
||||
# this should remain public because it's called by AlgoliaSearch::AlgoliaJob in .trigger_index
|
||||
def remove_algolia_index
|
||||
remove_from_index!
|
||||
Search::RemoveFromIndexJob.perform_now("ordered_comments_#{Rails.env}", index_id)
|
||||
end
|
||||
|
||||
def index_id
|
||||
"comments-#{id}"
|
||||
end
|
||||
|
||||
def self.tree_for(commentable, limit = 0)
|
||||
commentable.comments.includes(:user).arrange(order: "score DESC").to_a[0..limit - 1].to_h
|
||||
end
|
||||
|
||||
def path
|
||||
"/#{user.username}/comment/#{id_code_generated}"
|
||||
rescue StandardError
|
||||
|
|
@ -151,6 +148,8 @@ class Comment < ApplicationRecord
|
|||
end
|
||||
|
||||
def id_code_generated
|
||||
# 26 is the conversion base
|
||||
# eg. 1000.to_s(26) would be "1cc"
|
||||
id.to_s(26)
|
||||
end
|
||||
|
||||
|
|
@ -164,7 +163,8 @@ class Comment < ApplicationRecord
|
|||
return "[deleted]" if deleted
|
||||
|
||||
text = ActionController::Base.helpers.strip_tags(processed_html).strip
|
||||
HTMLEntities.new.decode ActionController::Base.helpers.truncate(text, length: length).gsub("'", "'").gsub("&", "&")
|
||||
truncated_text = ActionController::Base.helpers.truncate(text, length: length).gsub("'", "'").gsub("&", "&")
|
||||
HTMLEntities.new.decode(truncated_text)
|
||||
end
|
||||
|
||||
def video
|
||||
|
|
@ -185,6 +185,10 @@ class Comment < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def index_id
|
||||
"comments-#{id}"
|
||||
end
|
||||
|
||||
def update_notifications
|
||||
Notification.update_notifications(self)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ class DisplayAd < ApplicationRecord
|
|||
scope :approved_and_published, -> { where(approved: true, published: true) }
|
||||
|
||||
def self.for_display(area)
|
||||
relation = approved_and_published.where(placement_area: area).order(success_rate: :desc)
|
||||
|
||||
if rand(8) == 1
|
||||
approved_and_published.where(placement_area: area).order("success_rate DESC").sample
|
||||
relation.sample
|
||||
else
|
||||
approved_and_published.where(placement_area: area).order("success_rate DESC").limit(rand(1..15)).sample
|
||||
relation.limit(rand(1..15)).sample
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
class JobOpportunity < ApplicationRecord
|
||||
REMOTENESS_PHRASES = {
|
||||
"on_premise" => "In Office",
|
||||
"fully_remote" => "Fully Remote",
|
||||
"remote_optional" => "Remote Optional",
|
||||
"on_premise_flexible" => "Mostly in Office but Flexible"
|
||||
}.freeze
|
||||
|
||||
has_many :articles
|
||||
validates :remoteness,
|
||||
inclusion: { in: %w[on_premise fully_remote remote_optional on_premise_flexible] }
|
||||
|
||||
validates :remoteness, inclusion: { in: REMOTENESS_PHRASES.keys }
|
||||
|
||||
def remoteness_in_words
|
||||
phrases = {
|
||||
"on_premise" => "In Office",
|
||||
"fully_remote" => "Fully Remote",
|
||||
"remote_optional" => "Remote Optional",
|
||||
"on_premise_flexible" => "Mostly in Office but Flexible"
|
||||
}
|
||||
phrases[remoteness]
|
||||
REMOTENESS_PHRASES[remoteness]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,14 +8,15 @@ class Mention < ApplicationRecord
|
|||
validates :mentionable_id, presence: true
|
||||
validates :mentionable_type, presence: true
|
||||
validate :permission
|
||||
|
||||
after_create :send_email_notification
|
||||
|
||||
class << self
|
||||
def create_all(notifiable)
|
||||
Mentions::CreateAllJob.perform_later(notifiable.id, notifiable.class.name)
|
||||
end
|
||||
def self.create_all(notifiable)
|
||||
Mentions::CreateAllJob.perform_later(notifiable.id, notifiable.class.name)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_email_notification
|
||||
user = User.find(user_id)
|
||||
return unless user.email.present? && user.email_mention_notifications
|
||||
|
|
|
|||
|
|
@ -165,12 +165,12 @@ class Notification < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
# instance methods
|
||||
|
||||
def aggregated?
|
||||
action == "Reaction" || action == "Follow"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mark_notified_at_time
|
||||
self.notified_at = Time.current
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ class Page < ApplicationRecord
|
|||
end
|
||||
|
||||
def unique_slug_including_users_and_orgs
|
||||
errors.add(:slug, "is taken.") if User.find_by(username: slug) || Organization.find_by(slug: slug) || Podcast.find_by(slug: slug)
|
||||
slug_exists = User.exists?(username: slug) || Organization.exists?(slug: slug) || Podcast.exists?(slug: slug)
|
||||
errors.add(:slug, "is taken.") if slug_exists
|
||||
end
|
||||
|
||||
def bust_cache
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ class Podcast < ApplicationRecord
|
|||
private
|
||||
|
||||
def unique_slug_including_users_and_orgs
|
||||
errors.add(:slug, "is taken.") if User.find_by(username: slug) || Organization.find_by(slug: slug) || Page.find_by(slug: slug)
|
||||
slug_exists = User.exists?(username: slug) || Organization.exists?(slug: slug) || Page.exists?(slug: slug)
|
||||
errors.add(:slug, "is taken.") if slug_exists
|
||||
end
|
||||
|
||||
def bust_cache
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class PodcastEpisode < ApplicationRecord
|
|||
after_destroy :purge, :purge_all
|
||||
after_save :bust_cache
|
||||
|
||||
before_validation :prefix_all_images
|
||||
before_validation :process_html_and_prefix_all_images
|
||||
|
||||
scope :reachable, -> { where(reachable: true) }
|
||||
scope :published, -> { joins(:podcast).where(podcasts: { published: true }) }
|
||||
|
|
@ -68,10 +68,6 @@ class PodcastEpisode < ApplicationRecord
|
|||
comments.pluck(:body_markdown).join(" ")
|
||||
end
|
||||
|
||||
def index_id
|
||||
"podcast_episodes-#{id}"
|
||||
end
|
||||
|
||||
def path
|
||||
return nil unless podcast&.slug
|
||||
|
||||
|
|
@ -117,10 +113,6 @@ class PodcastEpisode < ApplicationRecord
|
|||
alias search_score zero_method
|
||||
alias positive_reactions_count zero_method
|
||||
|
||||
def bust_cache
|
||||
PodcastEpisodes::BustCacheJob.perform_later(id, path, podcast_slug)
|
||||
end
|
||||
|
||||
def class_name
|
||||
self.class.name
|
||||
end
|
||||
|
|
@ -143,7 +135,15 @@ class PodcastEpisode < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def prefix_all_images
|
||||
def index_id
|
||||
"podcast_episodes-#{id}"
|
||||
end
|
||||
|
||||
def bust_cache
|
||||
PodcastEpisodes::BustCacheJob.perform_later(id, path, podcast_slug)
|
||||
end
|
||||
|
||||
def process_html_and_prefix_all_images
|
||||
return if body.blank?
|
||||
|
||||
self.processed_html = body.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class PollSkip < ApplicationRecord
|
|||
private
|
||||
|
||||
def one_vote_per_poll_per_user
|
||||
errors.add(:base, "cannot vote more than once in one poll") if poll.poll_votes.where(user_id: user_id).any? || poll.poll_skips.where(user_id: user_id).any?
|
||||
already_voted = poll.poll_votes.where(user_id: user_id).any? || poll.poll_skips.where(user_id: user_id).any?
|
||||
errors.add(:base, "cannot vote more than once in one poll") if already_voted
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,15 +6,19 @@ class RatingVote < ApplicationRecord
|
|||
validates :group, inclusion: { in: %w[experience_level] }
|
||||
validates :rating, numericality: { greater_than: 0.0, less_than_or_equal_to: 10.0 }
|
||||
validate :permissions
|
||||
|
||||
counter_culture :article
|
||||
counter_culture :user
|
||||
|
||||
def assign_article_rating
|
||||
ratings = article.rating_votes.where(group: group).pluck(:rating)
|
||||
average = ratings.sum / ratings.size
|
||||
article.update_column(:experience_level_rating, average)
|
||||
article.update_column(:experience_level_rating_distribution, ratings.sort.max - ratings.sort.min)
|
||||
article.update_column(:last_experience_level_rating_at, Time.current)
|
||||
|
||||
article.update_columns(
|
||||
experience_level_rating: average,
|
||||
experience_level_rating_distribution: ratings.max - ratings.min,
|
||||
last_experience_level_rating_at: Time.current,
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
class Reaction < ApplicationRecord
|
||||
include AlgoliaSearch
|
||||
|
||||
CATEGORIES = %w[like readinglist unicorn thinking hands thumbsdown vomit].freeze
|
||||
|
||||
belongs_to :reactable, polymorphic: true
|
||||
|
|
@ -78,10 +79,6 @@ class Reaction < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def cache_buster
|
||||
CacheBuster
|
||||
end
|
||||
|
||||
def touch_user
|
||||
Users::TouchJob.perform_later(user_id)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,20 @@
|
|||
class Role < ApplicationRecord
|
||||
ROLES = %w[
|
||||
admin
|
||||
banned
|
||||
chatroom_beta_tester
|
||||
comment_banned
|
||||
podcast_admin
|
||||
pro
|
||||
single_resource_admin
|
||||
super_admin
|
||||
tag_moderator
|
||||
tech_admin
|
||||
trusted
|
||||
warned
|
||||
workshop_pass
|
||||
].freeze
|
||||
|
||||
has_and_belongs_to_many :users, join_table: :users_roles
|
||||
|
||||
belongs_to :resource,
|
||||
|
|
@ -9,22 +25,7 @@ class Role < ApplicationRecord
|
|||
allow_nil: true
|
||||
|
||||
validates :name,
|
||||
inclusion: {
|
||||
in: %w[
|
||||
super_admin
|
||||
admin
|
||||
single_resource_admin
|
||||
tech_admin
|
||||
tag_moderator
|
||||
trusted
|
||||
banned
|
||||
warned
|
||||
workshop_pass
|
||||
chatroom_beta_tester
|
||||
comment_banned
|
||||
pro
|
||||
podcast_admin
|
||||
]
|
||||
}
|
||||
inclusion: { in: ROLES }
|
||||
|
||||
scopify
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ class TagAdjustment < ApplicationRecord
|
|||
end
|
||||
|
||||
def has_privilege_to_adjust?
|
||||
user&.has_role?(:tag_moderator, tag) ||
|
||||
user&.has_role?(:admin) ||
|
||||
user&.has_role?(:super_admin)
|
||||
return false unless user
|
||||
|
||||
user.has_role?(:tag_moderator, tag) ||
|
||||
user.has_role?(:admin) ||
|
||||
user.has_role?(:super_admin)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -211,19 +211,11 @@ class User < ApplicationRecord
|
|||
summary
|
||||
end
|
||||
|
||||
def index_id
|
||||
"users-#{id}"
|
||||
end
|
||||
|
||||
def set_remember_fields
|
||||
self.remember_token ||= self.class.remember_token if respond_to?(:remember_token)
|
||||
self.remember_created_at ||= Time.now.utc
|
||||
end
|
||||
|
||||
def estimate_default_language
|
||||
Users::EstimateDefaultLanguageJob.perform_later(id)
|
||||
end
|
||||
|
||||
def calculate_score
|
||||
score = (articles.where(featured: true).size * 100) + comments.sum(:score)
|
||||
update_column(:score, score)
|
||||
|
|
@ -461,6 +453,14 @@ class User < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def index_id
|
||||
"users-#{id}"
|
||||
end
|
||||
|
||||
def estimate_default_language
|
||||
Users::EstimateDefaultLanguageJob.perform_later(id)
|
||||
end
|
||||
|
||||
def set_default_language
|
||||
language_settings["preferred_languages"] ||= ["en"]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ class UserBlock < ApplicationRecord
|
|||
exists?(blocker_id: blocker_id, blocked_id: blocked_id)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def blocker_cannot_be_same_as_blocked
|
||||
errors.add(:blocker_id, "can't be the same as the blocked_id") if blocker_id == blocked_id
|
||||
end
|
||||
|
|
|
|||
101
spec/decorators/user_decorator_spec.rb
Normal file
101
spec/decorators/user_decorator_spec.rb
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe UserDecorator, type: :decorator do
|
||||
let(:user) { build_stubbed(:user) }
|
||||
|
||||
describe "#cached_followed_tags" do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let(:tag1) { create(:tag) }
|
||||
let(:tag2) { create(:tag) }
|
||||
let(:tag3) { create(:tag) }
|
||||
|
||||
it "returns empty if no tags followed" do
|
||||
expect(user.decorate.cached_followed_tags.size).to eq(0)
|
||||
end
|
||||
|
||||
it "returns array of tags if user follows them" do
|
||||
user.follow(tag1)
|
||||
user.follow(tag2)
|
||||
user.follow(tag3)
|
||||
expect(user.decorate.cached_followed_tags.size).to eq(3)
|
||||
end
|
||||
|
||||
it "returns tag object with name" do
|
||||
user.follow(tag1)
|
||||
expect(user.decorate.cached_followed_tags.first.name).to eq(tag1.name)
|
||||
end
|
||||
|
||||
it "returns follow points for tag" do
|
||||
user.follow(tag1)
|
||||
expect(user.decorate.cached_followed_tags.first.points).to eq(1.0)
|
||||
end
|
||||
|
||||
it "returns adjusted points for tag" do
|
||||
follow = user.follow(tag1)
|
||||
follow.update(points: 0.1)
|
||||
expect(user.decorate.cached_followed_tags.first.points).to eq(0.1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#config_body_class" do
|
||||
it "creates proper body class with defaults" do
|
||||
expect(user.decorate.config_body_class).to eq("default default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
|
||||
end
|
||||
|
||||
it "creates proper body class with sans serif config" do
|
||||
user.config_font = "sans_serif"
|
||||
expect(user.decorate.config_body_class).to eq("default sans-serif-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
|
||||
end
|
||||
|
||||
it "creates proper body class with night theme" do
|
||||
user.config_theme = "night_theme"
|
||||
expect(user.decorate.config_body_class).to eq("night-theme default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
|
||||
end
|
||||
|
||||
it "creates proper body class with pink theme" do
|
||||
user.config_theme = "pink_theme"
|
||||
expect(user.decorate.config_body_class).to eq("pink-theme default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
|
||||
end
|
||||
|
||||
it "creates proper body class with minimal light theme" do
|
||||
user.config_theme = "minimal_light_theme"
|
||||
expect(user.decorate.config_body_class).to eq("minimal-light-theme default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
|
||||
end
|
||||
|
||||
it "works with static navbar" do
|
||||
user.config_navbar = "static"
|
||||
expect(user.decorate.config_body_class).to eq("default default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} static-navbar-config")
|
||||
end
|
||||
|
||||
context "when user with roles" do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it "creates proper body class with pro user" do
|
||||
user.add_role(:pro)
|
||||
expect(user.decorate.config_body_class).to eq("default default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
|
||||
end
|
||||
|
||||
it "creates proper body class with trusted user" do
|
||||
user.add_role(:trusted)
|
||||
expect(user.decorate.config_body_class).to eq("default default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#dark_theme?" do
|
||||
it "determines dark theme if night theme" do
|
||||
user.config_theme = "night_theme"
|
||||
expect(user.decorate.dark_theme?).to eq(true)
|
||||
end
|
||||
|
||||
it "determines dark theme if ten x hacker" do
|
||||
user.config_theme = "ten_x_hacker_theme"
|
||||
expect(user.decorate.dark_theme?).to eq(true)
|
||||
end
|
||||
|
||||
it "determines not dark theme if not one of the dark themes" do
|
||||
user.config_theme = "default"
|
||||
expect(user.decorate.dark_theme?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,10 +1,4 @@
|
|||
FactoryBot.define do
|
||||
factory :poll_skip do
|
||||
# prompt_markdown { Faker::Hipster.words(number: 5) }
|
||||
# factory :poll_with_options do
|
||||
# after(:create) do |poll|
|
||||
# create_list(:poll_option, poll: poll)
|
||||
# end
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ RSpec.describe Article, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe "published_timestamp" do
|
||||
describe "#published_timestamp" do
|
||||
it "returns empty string if the article is not published" do
|
||||
article.published = false
|
||||
expect(article.published_timestamp).to be_empty
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe AuditLog, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:audit_log) { create(:audit_log, user_id: user.id) }
|
||||
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to validate_presence_of(:user_id) }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe BadgeAchievement, type: :model do
|
||||
describe "validations" do
|
||||
subject { create(:badge_achievement) }
|
||||
let_it_be(:achievement) { create(:badge_achievement) }
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to belong_to(:badge) }
|
||||
it { is_expected.to belong_to(:rewarder).class_name("User").optional }
|
||||
|
|
@ -11,12 +11,10 @@ RSpec.describe BadgeAchievement, type: :model do
|
|||
end
|
||||
|
||||
it "turns rewarding_context_message_markdown into rewarding_context_message HTML" do
|
||||
achievement = create(:badge_achievement)
|
||||
expect(achievement.rewarding_context_message).to include("</a>")
|
||||
end
|
||||
|
||||
it "awards credits after create" do
|
||||
achievement = create(:badge_achievement)
|
||||
expect(achievement.user.credits.size).to eq(5)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Badge, type: :model do
|
||||
describe "validations" do
|
||||
subject { create(:badge) }
|
||||
let_it_be(:badge) { create(:badge) }
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to have_many(:users).through(:badge_achievements) }
|
||||
it { is_expected.to have_many(:badge_achievements) }
|
||||
it { is_expected.to validate_presence_of(:title) }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Block, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:block) { described_class.new(user: user, input_html: "hello") }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:block) { described_class.new(user: user, input_html: "hello") }
|
||||
|
||||
it "creates processed_html after published!" do
|
||||
user.add_role(:super_admin)
|
||||
|
|
@ -11,8 +11,7 @@ RSpec.describe Block, type: :model do
|
|||
end
|
||||
|
||||
it "is not valid without user" do
|
||||
block.user = nil
|
||||
expect(block).not_to be_valid
|
||||
expect(described_class.new(user: nil)).not_to be_valid
|
||||
end
|
||||
|
||||
it "is not valid with non-admin user" do
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe BufferUpdate, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:article) { create(:article, user_id: user.id) }
|
||||
let_it_be(:article) { create(:article) }
|
||||
|
||||
it "creates update" do
|
||||
described_class.buff!(article.id, "twitter_buffer_text", "CODE", "twitter")
|
||||
|
|
|
|||
|
|
@ -2,44 +2,56 @@ require "rails_helper"
|
|||
|
||||
RSpec.describe ChatChannel, type: :model do
|
||||
let(:chat_channel) { create(:chat_channel) }
|
||||
let(:message) { create(:chat_channel, message_id: chat_channel.id) }
|
||||
|
||||
let_it_be(:users) { create_list(:user, 2) }
|
||||
|
||||
it { is_expected.to have_many(:messages) }
|
||||
it { is_expected.to validate_presence_of(:channel_type) }
|
||||
|
||||
it "clears chat" do
|
||||
allow(Pusher).to receive(:trigger)
|
||||
chat_channel.clear_channel
|
||||
expect(chat_channel.messages.size).to eq(0)
|
||||
describe "#clear_channel" do
|
||||
before { allow(Pusher).to receive(:trigger) }
|
||||
|
||||
it "clears chat" do
|
||||
create(:message, chat_channel: chat_channel, user: create(:user))
|
||||
chat_channel.reload
|
||||
expect(chat_channel.messages.size).to be_positive
|
||||
chat_channel.clear_channel
|
||||
expect(chat_channel.messages.size).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
it "creates channel with users" do
|
||||
chat_channel = described_class.create_with_users([create(:user), create(:user)])
|
||||
expect(chat_channel.users.size).to eq(2)
|
||||
expect(chat_channel.has_member?(User.first)).to eq(true)
|
||||
describe "#create_with_users" do
|
||||
it "creates channel with users" do
|
||||
chat_channel = described_class.create_with_users(users)
|
||||
expect(chat_channel.users.size).to eq(users.size)
|
||||
expect(chat_channel.has_member?(users.first)).to be(true)
|
||||
expect(chat_channel.has_member?(users.last)).to be(true)
|
||||
end
|
||||
|
||||
it "lists active memberships" do
|
||||
chat_channel = described_class.create_with_users(users)
|
||||
expect(chat_channel.active_users.size).to eq(users.size)
|
||||
expect(chat_channel.channel_users.size).to eq(users.size)
|
||||
end
|
||||
end
|
||||
|
||||
it "lists active memberships" do
|
||||
chat_channel = described_class.create_with_users([create(:user), create(:user)])
|
||||
expect(chat_channel.active_users.size).to eq(2)
|
||||
expect(chat_channel.channel_users.size).to eq(2)
|
||||
end
|
||||
|
||||
it "decreases active users if one leaves" do
|
||||
chat_channel = described_class.create_with_users([create(:user), create(:user)])
|
||||
ChatChannelMembership.last.update(status: "left_channel")
|
||||
expect(chat_channel.active_users.size).to eq(1)
|
||||
expect(chat_channel.channel_users.size).to eq(1)
|
||||
describe "#active_users" do
|
||||
it "decreases active users if one leaves" do
|
||||
chat_channel = described_class.create_with_users(users)
|
||||
expect(chat_channel.active_users.size).to eq(users.size)
|
||||
expect(chat_channel.channel_users.size).to eq(users.size)
|
||||
ChatChannelMembership.last.update(status: "left_channel")
|
||||
expect(chat_channel.active_users.size).to eq(users.size - 1)
|
||||
expect(chat_channel.channel_users.size).to eq(users.size - 1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#remove_user" do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it "removes a user from a channel" do
|
||||
chat_channel.add_users(user)
|
||||
expect(chat_channel.chat_channel_memberships.exists?(user_id: user.id)).to be(true)
|
||||
chat_channel.remove_user(user)
|
||||
expect(chat_channel.chat_channel_memberships.exists?(user_id: user.id)).to be(false)
|
||||
chat_channel.add_users(users.first)
|
||||
expect(chat_channel.chat_channel_memberships.exists?(user_id: users.first.id)).to be(true)
|
||||
chat_channel.remove_user(users.first)
|
||||
expect(chat_channel.chat_channel_memberships.exists?(user_id: users.first.id)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe ClassifiedListing, type: :model do
|
||||
let(:classified_listing) { create(:classified_listing, user_id: user.id) }
|
||||
let(:user) { create(:user) }
|
||||
let(:organization) { create(:organization) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:organization) { create(:organization) }
|
||||
let(:classified_listing) { create(:classified_listing, user: user) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:title) }
|
||||
it { is_expected.to validate_presence_of(:body_markdown) }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Collection, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:collection) { create(:collection, :with_articles, user: user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:collection) { create(:collection, :with_articles, user: user) }
|
||||
|
||||
describe "validations" do
|
||||
subject { described_class.new }
|
||||
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to belong_to(:organization).optional }
|
||||
it { is_expected.to have_many(:articles) }
|
||||
|
|
@ -17,11 +15,10 @@ RSpec.describe Collection, type: :model do
|
|||
end
|
||||
|
||||
describe ".find_series" do
|
||||
let(:user) { create(:user) }
|
||||
let(:series) { create(:collection, user: user) }
|
||||
let_it_be(:other_user) { create(:user) }
|
||||
let_it_be(:series) { collection }
|
||||
|
||||
it "returns an existing series" do
|
||||
series # the series has to be created before the following expect
|
||||
expect do
|
||||
expect(described_class.find_series(series.slug, series.user)).to eq(series)
|
||||
end.not_to change(described_class, :count)
|
||||
|
|
@ -29,13 +26,11 @@ RSpec.describe Collection, type: :model do
|
|||
|
||||
it "creates a new series for a user if an existing one is not found" do
|
||||
slug = Faker::Books::CultureSeries.book
|
||||
expect { described_class.find_series(slug, user) }.to change(described_class, :count).by(1)
|
||||
expect { described_class.find_series(slug, other_user) }.to change(described_class, :count).by(1)
|
||||
end
|
||||
|
||||
it "creates a new series with an existing slug for a new user" do
|
||||
user = create(:user)
|
||||
series # the series has to be created before the following expect
|
||||
expect { described_class.find_series(series.slug, user) }.to change(described_class, :count).by(1)
|
||||
expect { described_class.find_series(series.slug, other_user) }.to change(described_class, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -44,7 +39,7 @@ RSpec.describe Collection, type: :model do
|
|||
Timecop.freeze(DateTime.parse("2019/10/24")) do
|
||||
allow(collection.articles).to receive(:update_all)
|
||||
collection.touch_articles
|
||||
expect(collection.articles).to have_received(:update_all).with(updated_at: Time.zone.now)
|
||||
expect(collection.articles).to have_received(:update_all).with(updated_at: Time.current)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,26 +1,13 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Comment, type: :model do
|
||||
let(:user) { create(:user, created_at: 3.weeks.ago) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:article) { create(:article, user_id: user.id, published: true) }
|
||||
let(:article_with_video) { create(:article, :video, user_id: user.id, published: true) }
|
||||
let(:comment) { create(:comment, user_id: user2.id, commentable_id: article.id) }
|
||||
let(:video_comment) { create(:comment, user_id: user2.id, commentable_id: article_with_video.id) }
|
||||
let(:comment_2) { create(:comment, user_id: user2.id, commentable_id: article.id) }
|
||||
let(:child_comment) do
|
||||
build(:comment, user_id: user.id, commentable_id: article.id, parent_id: comment.id)
|
||||
end
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:article) { create(:article, user: user) }
|
||||
let_it_be(:comment) { create(:comment, user: user, commentable: article) }
|
||||
|
||||
include_examples "#sync_reactions_count", :article_comment
|
||||
|
||||
describe "validations" do
|
||||
subject { described_class.new(user: user, commentable: article) }
|
||||
|
||||
let(:article) { Article.new(user: user2) }
|
||||
|
||||
before do
|
||||
allow(article).to receive(:touch).and_return(true)
|
||||
end
|
||||
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to belong_to(:commentable) }
|
||||
it { is_expected.to have_many(:reactions).dependent(:destroy) }
|
||||
|
|
@ -28,135 +15,158 @@ RSpec.describe Comment, type: :model do
|
|||
it { is_expected.to have_many(:notifications).dependent(:delete_all) }
|
||||
it { is_expected.to have_many(:notification_subscriptions).dependent(:destroy) }
|
||||
it { is_expected.to validate_presence_of(:commentable_id) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:body_markdown) }
|
||||
it { is_expected.to validate_uniqueness_of(:body_markdown).scoped_to(:user_id, :ancestry, :commentable_id, :commentable_type) }
|
||||
|
||||
it do
|
||||
# rubocop:disable RSpec/NamedSubject
|
||||
subject.commentable = article
|
||||
subject.user = user
|
||||
expect(subject).to(
|
||||
validate_uniqueness_of(:body_markdown).scoped_to(:user_id, :ancestry, :commentable_id, :commentable_type),
|
||||
)
|
||||
# rubocop:enable RSpec/NamedSubject
|
||||
end
|
||||
|
||||
it { is_expected.to validate_length_of(:body_markdown).is_at_least(1).is_at_most(25_000) }
|
||||
it { is_expected.to validate_inclusion_of(:commentable_type).in_array(%w[Article PodcastEpisode]) }
|
||||
end
|
||||
|
||||
it "gets proper generated ID code" do
|
||||
comment = described_class.new(id: 1)
|
||||
expect(comment.id_code_generated).to eq(comment.id.to_s(26))
|
||||
end
|
||||
it "is invalid if commentable is unpublished article" do
|
||||
# rubocop:disable RSpec/NamedSubject
|
||||
subject.commentable = build(:article, published: false)
|
||||
expect(subject).not_to be_valid
|
||||
# rubocop:enable RSpec/NamedSubject
|
||||
end
|
||||
|
||||
it "generates character count before saving" do
|
||||
expect(comment.markdown_character_count).to eq(comment.body_markdown.size)
|
||||
end
|
||||
describe "#processed_html" do
|
||||
let(:comment) { build(:comment, user: user, commentable: article) }
|
||||
|
||||
describe "#processed_html" do
|
||||
let(:comment) { create(:comment, commentable: article, body_markdown: "# hello\n\nhy hey hey") }
|
||||
it "converts body_markdown to proper processed_html" do
|
||||
comment.body_markdown = "# hello\n\nhy hey hey"
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?("<h1>")).to be(true)
|
||||
end
|
||||
|
||||
it "converts body_markdown to proper processed_html" do
|
||||
expect(comment.processed_html.include?("<h1>")).to eq(true)
|
||||
it "adds rel=nofollow to links" do
|
||||
comment.body_markdown = "this is a comment with a link: http://dev.to"
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?('rel="nofollow"')).to be(true)
|
||||
end
|
||||
|
||||
it "adds a mention url if user is mentioned like @mention" do
|
||||
comment.body_markdown = "Hello @#{user.username}, you are cool."
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?("/#{user.username}")).to be(true)
|
||||
expect(comment.processed_html.include?("href")).to be(true)
|
||||
expect(comment.processed_html.include?("Hello <a")).to be(true)
|
||||
end
|
||||
|
||||
it "not double wrap an already-linked mention" do
|
||||
comment.body_markdown = "Hello <a href='/#{user.username}'>@#{user.username}</a>, you are cool."
|
||||
comment.validate!
|
||||
expect(comment.processed_html.scan(/href/).count).to eq(1)
|
||||
end
|
||||
|
||||
it "does not wrap email mention with username" do
|
||||
comment.body_markdown = "Hello hello@#{user.username}.com, you are cool."
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?("/#{user.username}")).to be(false)
|
||||
end
|
||||
|
||||
it "only mentions users who are actual users" do
|
||||
comment.body_markdown = "Hello @hooper, you are cool."
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?("/hooper")).to be(false)
|
||||
end
|
||||
|
||||
it "mentions people if it is the first word" do
|
||||
comment.body_markdown = "@#{user.username}, you are cool."
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?("/#{user.username}")).to be(true)
|
||||
end
|
||||
|
||||
it "does case insentive mention recognition" do
|
||||
comment.body_markdown = "Hello @#{user.username.titleize}, you are cool."
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?("/#{user.username}")).to be(true)
|
||||
expect(comment.processed_html.include?("href")).to be(true)
|
||||
expect(comment.processed_html.include?("Hello <a")).to be(true)
|
||||
end
|
||||
|
||||
it "shortens long urls" do
|
||||
comment.body_markdown = "Hello https://longurl.com/#{'x' * 100}?#{'y' * 100}"
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?("...</a>")).to be(true)
|
||||
expect(comment.processed_html.size < 450).to be(true)
|
||||
end
|
||||
|
||||
it "adds timestamp url if commentable has video and timestamp", :aggregate_failures do
|
||||
article.video = "https://example.com"
|
||||
|
||||
comment.body_markdown = "I like the part at 4:30"
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?(">4:30</a>")).to be(true)
|
||||
|
||||
comment.body_markdown = "I like the part at 4:30 and 5:50"
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?(">5:50</a>")).to eq(true)
|
||||
|
||||
comment.body_markdown = "I like the part at 5:30 and :55"
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?(">:55</a>")).to eq(true)
|
||||
|
||||
comment.body_markdown = "I like the part at 52:30"
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?(">52:30</a>")).to eq(true)
|
||||
|
||||
comment.body_markdown = "I like the part at 1:52:30 and 1:20"
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?(">1:52:30</a>")).to eq(true)
|
||||
expect(comment.processed_html.include?(">1:20</a>")).to eq(true)
|
||||
end
|
||||
|
||||
it "does not add timestamp if commentable does not have video" do
|
||||
article.video = nil
|
||||
|
||||
comment.body_markdown = "I like the part at 1:52:30 and 1:20"
|
||||
comment.validate!
|
||||
expect(comment.processed_html.include?(">1:52:30</a>")).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "adds timestamp url if commentable has video and timestamp" do
|
||||
video_comment.body_markdown = "I like the part at 4:30"
|
||||
video_comment.save
|
||||
expect(video_comment.processed_html.include?(">4:30</a>")).to eq(true)
|
||||
video_comment.body_markdown = "I like the part at 4:30 and 5:50"
|
||||
video_comment.save
|
||||
expect(video_comment.processed_html.include?(">5:50</a>")).to eq(true)
|
||||
video_comment.body_markdown = "I like the part at 5:30 and :55"
|
||||
video_comment.save
|
||||
expect(video_comment.processed_html.include?(">:55</a>")).to eq(true)
|
||||
video_comment.body_markdown = "I like the part at 52:30"
|
||||
video_comment.save
|
||||
expect(video_comment.processed_html.include?(">52:30</a>")).to eq(true)
|
||||
video_comment.body_markdown = "I like the part at 1:52:30 and 1:20"
|
||||
video_comment.save
|
||||
expect(video_comment.processed_html.include?(">1:52:30</a>")).to eq(true)
|
||||
expect(video_comment.processed_html.include?(">1:20</a>")).to eq(true)
|
||||
describe "#id_code_generated" do
|
||||
it "gets proper generated ID code" do
|
||||
expect(described_class.new(id: 1000).id_code_generated).to eq("1cc")
|
||||
end
|
||||
end
|
||||
|
||||
it "does not add timestamp if commentable does not have video" do
|
||||
comment.body_markdown = "I like the part at 1:52:30 and 1:20"
|
||||
comment.save
|
||||
expect(comment.processed_html.include?(">1:52:30</a>")).to eq(false)
|
||||
describe "#readable_publish_date" do
|
||||
it "does not show year in readable time if not current year" do
|
||||
expect(comment.readable_publish_date).to eq(comment.created_at.strftime("%b %e"))
|
||||
end
|
||||
|
||||
it "shows year in readable time if not current year" do
|
||||
comment.created_at = 1.year.ago
|
||||
last_year = 1.year.ago.year % 100
|
||||
expect(comment.readable_publish_date.include?("'#{last_year}")).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
it "adds rel=nofollow to links" do
|
||||
comment.body_markdown = "this is a comment with a link: http://dev.to"
|
||||
comment.save
|
||||
expect(comment.processed_html.include?('rel="nofollow"')).to eq(true)
|
||||
describe "#path" do
|
||||
it "returns the properly formed path" do
|
||||
expect(comment.path).to eq("/#{comment.user.username}/comment/#{comment.id_code_generated}")
|
||||
end
|
||||
end
|
||||
|
||||
it "adds a mention url if user is mentioned like @mention" do
|
||||
comment.body_markdown = "Hello @#{user.username}, you are cool."
|
||||
comment.save
|
||||
expect(comment.processed_html.include?("/#{user.username}")).to eq(true)
|
||||
expect(comment.processed_html.include?("href")).to eq(true)
|
||||
expect(comment.processed_html.include?("Hello <a")).to eq(true)
|
||||
end
|
||||
describe "#parent_or_root_article" do
|
||||
it "returns root article if no parent comment" do
|
||||
expect(comment.parent_or_root_article).to eq(comment.commentable)
|
||||
end
|
||||
|
||||
it "not double wrap an already-linked mention" do
|
||||
comment.body_markdown = "Hello <a href='/#{user.username}'>@#{user.username}</a>, you are cool."
|
||||
comment.save
|
||||
expect(comment.processed_html.scan(/href/).count).to eq(1)
|
||||
end
|
||||
|
||||
it "does not wrap email mention with username" do
|
||||
comment.body_markdown = "Hello hello@#{user.username}.com, you are cool."
|
||||
comment.save
|
||||
expect(comment.processed_html.include?("/#{user.username}")).to eq(false)
|
||||
end
|
||||
|
||||
it "only mentions users who are actual users" do
|
||||
comment.body_markdown = "Hello @hooper, you are cool."
|
||||
comment.save
|
||||
expect(comment.processed_html.include?("/hooper")).to eq(false)
|
||||
end
|
||||
|
||||
it "mentions people if it is the first word" do
|
||||
comment.body_markdown = "@#{user.username}, you are cool."
|
||||
comment.save
|
||||
expect(comment.processed_html.include?("/#{user.username}")).to eq(true)
|
||||
end
|
||||
|
||||
it "does case insentive mention recognition" do
|
||||
comment.body_markdown = "Hello @#{user.username.titleize}, you are cool."
|
||||
comment.save
|
||||
expect(comment.processed_html.include?("/#{user.username}")).to eq(true)
|
||||
expect(comment.processed_html.include?("href")).to eq(true)
|
||||
expect(comment.processed_html.include?("Hello <a")).to eq(true)
|
||||
end
|
||||
|
||||
it "shortens long urls" do
|
||||
comment.update(body_markdown: "Hello https://longurl.com/#{'x' * 100}?#{'y' * 100}")
|
||||
expect(comment.processed_html.include?("...</a>")).to eq(true)
|
||||
expect(comment.processed_html.size).to be < 450
|
||||
end
|
||||
|
||||
it "does not show year in readable time if not current year" do
|
||||
expect(comment.readable_publish_date).to eq(comment.created_at.strftime("%b %e"))
|
||||
end
|
||||
|
||||
it "shows year in readable time if not current year" do
|
||||
comment.created_at = 1.year.ago
|
||||
last_year = 1.year.ago.year % 100
|
||||
expect(comment.readable_publish_date.include?("'#{last_year}")).to eq(true)
|
||||
end
|
||||
|
||||
it "returns a path" do
|
||||
expect(comment.path).not_to be(nil)
|
||||
end
|
||||
|
||||
it "returns the properly formed path" do
|
||||
expect(comment.path).to eq("/#{comment.user.username}/comment/#{comment.id_code_generated}")
|
||||
end
|
||||
|
||||
it "returns root article if no parent comment" do
|
||||
expect(comment.parent_or_root_article).to eq(comment.commentable)
|
||||
end
|
||||
|
||||
it "returns root parent comment if exists" do
|
||||
expect(child_comment.parent_or_root_article).to eq(comment)
|
||||
end
|
||||
|
||||
it "properly indexes" do
|
||||
comment.index!
|
||||
it "returns root parent comment if exists" do
|
||||
child_comment = build(:comment, parent: comment)
|
||||
expect(child_comment.parent_or_root_article).to eq(comment)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#parent_user" do
|
||||
|
|
@ -165,7 +175,10 @@ RSpec.describe Comment, type: :model do
|
|||
end
|
||||
|
||||
it "returns the root parent comment's user if root parent comment exists" do
|
||||
expect(child_comment.parent_user).to eq(user2)
|
||||
child_comment_user = build(:user)
|
||||
child_comment = build(:comment, parent: comment, user: child_comment_user)
|
||||
expect(child_comment.parent_user).not_to eq(child_comment_user)
|
||||
expect(child_comment.parent_user).to eq(comment.user)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -179,129 +192,123 @@ RSpec.describe Comment, type: :model do
|
|||
end
|
||||
|
||||
it "retains content from #processed_html" do
|
||||
comment.update_column(:processed_html, "Hello this is a post.") # Remove randomness
|
||||
comment.processed_html = "Hello this is a post." # Remove randomness
|
||||
comment.validate!
|
||||
text = comment.title.gsub("...", "").delete("\n")
|
||||
expect(comment.processed_html).to include CGI.unescapeHTML(text)
|
||||
expect(comment.processed_html).to include(CGI.unescapeHTML(text))
|
||||
end
|
||||
|
||||
it "is converted to deleted if the comment is deleted" do
|
||||
comment.update_column(:deleted, true)
|
||||
expect(comment.title).to eq "[deleted]"
|
||||
comment.deleted = true
|
||||
expect(comment.title).to eq("[deleted]")
|
||||
end
|
||||
|
||||
it "does not contain the wrong encoding" do
|
||||
comment.body_markdown = "It's the best post ever. It's so great."
|
||||
comment.save
|
||||
expect(comment.title).not_to include "'"
|
||||
comment.validate!
|
||||
expect(comment.title).not_to include("'")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#index_id" do
|
||||
it "is equal to comments-ID" do
|
||||
# NOTE: we shouldn't test private things but cheating a bit for Algolia here
|
||||
expect(comment.send(:index_id)).to eq("comments-#{comment.id}")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#custom_css" do
|
||||
it "returns nothing when no ltag was used" do
|
||||
expect(comment.custom_css).to eq("")
|
||||
it "returns nothing when no liquid tag was used" do
|
||||
expect(comment.custom_css).to be_blank
|
||||
end
|
||||
|
||||
it "returns proper liquid tag classes if used" do
|
||||
text = "{% devcomment #{comment.id_code_generated} %}"
|
||||
ltag_comment = create(:comment, commentable_id: create(:article).id, body_markdown: text)
|
||||
expect(ltag_comment.custom_css).not_to eq("")
|
||||
comment.body_markdown = text
|
||||
expect(comment.custom_css).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe "validity" do
|
||||
it "is invalid if commentable is unpublished article" do
|
||||
article.update_column(:published, false)
|
||||
comment = build(:comment, user_id: user.id, commentable_id: article.id)
|
||||
expect(comment).not_to be_valid
|
||||
end
|
||||
end
|
||||
describe ".tree_for" do
|
||||
let_it_be(:other_comment) { create(:comment, commentable: article, user: user) }
|
||||
let_it_be(:child_comment) { create(:comment, commentable: article, parent: comment, user: user) }
|
||||
|
||||
describe "tree" do
|
||||
let(:article2) { create(:article) }
|
||||
let!(:tree_comment) { create(:comment, commentable: article2) }
|
||||
let!(:child) { create(:comment, commentable: article2, parent: tree_comment) }
|
||||
let!(:tree_comment2) { create(:comment, commentable: article2) }
|
||||
|
||||
before { tree_comment.update_column(:score, 1) }
|
||||
before { comment.update_column(:score, 1) }
|
||||
|
||||
it "returns a full tree" do
|
||||
comments = described_class.tree_for(article2)
|
||||
expect(comments).to eq(tree_comment => { child => {} }, tree_comment2 => {})
|
||||
comments = described_class.tree_for(article)
|
||||
expect(comments).to eq(comment => { child_comment => {} }, other_comment => {})
|
||||
end
|
||||
|
||||
it "returns part of the tree" do
|
||||
comments = described_class.tree_for(article2, 1)
|
||||
expect(comments).to eq(tree_comment => { child => {} })
|
||||
comments = described_class.tree_for(article, 1)
|
||||
expect(comments).to eq(comment => { child_comment => {} })
|
||||
end
|
||||
end
|
||||
|
||||
describe "deleted" do
|
||||
let(:child_comment) { create(:comment, commentable: article, parent: comment, user: user) }
|
||||
context "when callbacks are triggered before save" do
|
||||
it "generates character count before saving" do
|
||||
comment.save
|
||||
expect(comment.markdown_character_count).to eq(comment.body_markdown.size)
|
||||
end
|
||||
end
|
||||
|
||||
it "deletes the comment's notifications" do
|
||||
create(:notification, notifiable: comment, user: user2)
|
||||
create(:notification, notifiable: child_comment, user: user)
|
||||
context "when callbacks are triggered after save" do
|
||||
it "updates user last comment date" do
|
||||
expect { comment.save }.to change(user, :last_comment_at)
|
||||
end
|
||||
end
|
||||
|
||||
context "when callbacks are triggered after update" do
|
||||
it "deletes the comment's notifications when deleted is set to true" do
|
||||
create(:notification, notifiable: comment, user: user)
|
||||
perform_enqueued_jobs do
|
||||
child_comment.update(deleted: true)
|
||||
expect(child_comment.notifications).to be_empty
|
||||
comment.update(deleted: true)
|
||||
end
|
||||
expect(comment.notifications).to be_empty
|
||||
end
|
||||
|
||||
it "updates the notifications of the ancestors and descendants" do
|
||||
create(:notification, notifiable: comment, user: user2)
|
||||
it "updates the notifications of the descendants with [deleted]" do
|
||||
comment = create(:comment, commentable: article)
|
||||
child_comment = create(:comment, parent: comment, commentable: article, user: user)
|
||||
create(:notification, notifiable: child_comment, user: user)
|
||||
perform_enqueued_jobs do
|
||||
comment.update(deleted: true)
|
||||
expect(child_comment.notifications.first.json_data["comment"]["ancestors"][0]["title"]).to eq "[deleted]"
|
||||
end
|
||||
notification = child_comment.notifications.first
|
||||
expect(notification.json_data["comment"]["ancestors"][0]["title"]).to eq("[deleted]")
|
||||
end
|
||||
end
|
||||
|
||||
describe "when algolia auto-indexing/removal is triggered" do
|
||||
context "when callbacks are triggered after destroy" do
|
||||
it "updates user's last_comment_at" do
|
||||
expect { comment.destroy }.to change(user, :last_comment_at)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when indexing and deindexing" do
|
||||
let!(:comment) { create(:comment, commentable: article) }
|
||||
|
||||
context "when destroying" do
|
||||
it "doesn't schedule an ActiveJob on destroy" do
|
||||
comment = create(:comment, commentable: article)
|
||||
expect do
|
||||
comment.destroy
|
||||
end.not_to have_enqueued_job.on_queue("algoliasearch")
|
||||
it "doesn't trigger auto removal from index" do
|
||||
expect { comment.destroy }.not_to have_enqueued_job.on_queue("algoliasearch")
|
||||
end
|
||||
end
|
||||
|
||||
context "when record.deleted == false" do
|
||||
context "when deleted is false" do
|
||||
it "checks auto-indexing" do
|
||||
expect do
|
||||
create(:comment, user_id: user2.id, commentable_id: article.id)
|
||||
create(:comment, commentable: article)
|
||||
end.to have_enqueued_job.with(kind_of(described_class), "index!").on_queue("algoliasearch")
|
||||
end
|
||||
end
|
||||
|
||||
context "when record.deleted == true" do
|
||||
it "checks auto-indexing" do
|
||||
comment.deleted = true
|
||||
context "when deleted is true" do
|
||||
it "checks auto-deindexing" do
|
||||
expect do
|
||||
comment.save!
|
||||
comment.update(deleted: true)
|
||||
end.to have_enqueued_job.with(kind_of(described_class), "remove_algolia_index").on_queue("algoliasearch")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "when a comment is destroyed" do
|
||||
it "updates user last comment date" do
|
||||
expect do
|
||||
comment.destroy
|
||||
user2.reload
|
||||
end.to change(user2, :last_comment_at)
|
||||
end
|
||||
end
|
||||
|
||||
describe "when a comment is created" do
|
||||
it "updates user last comment date" do
|
||||
expect do
|
||||
comment.save
|
||||
user2.reload
|
||||
end.to change(user2, :last_comment_at)
|
||||
end
|
||||
end
|
||||
|
||||
include_examples "#sync_reactions_count", :article_comment
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,58 +1,63 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Credit, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:organization) { create(:organization) }
|
||||
let(:random_number) { rand(100) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:organization) { create(:organization) }
|
||||
|
||||
it { is_expected.to belong_to(:user).optional }
|
||||
it { is_expected.to belong_to(:organization).optional }
|
||||
it { is_expected.to belong_to(:purchase).optional }
|
||||
|
||||
xit "counts credits for user" do
|
||||
# See https://github.com/magnusvk/counter_culture/issues/259
|
||||
context "when caching counters" do
|
||||
let_it_be(:user_credits) { create_list(:credit, 2, user: user) }
|
||||
let_it_be(:org_credits) { create_list(:credit, 1, organization: organization) }
|
||||
|
||||
create_list(:credit, random_number, user: user)
|
||||
described_class.counter_culture_fix_counts
|
||||
expect(user.reload.credits_count).to eq(random_number)
|
||||
end
|
||||
describe "#credits_count" do
|
||||
it "counts credits for user" do
|
||||
# See https://github.com/magnusvk/counter_culture/issues/259
|
||||
described_class.counter_culture_fix_counts
|
||||
expect(user.reload.credits_count).to eq(user.credits.size)
|
||||
end
|
||||
|
||||
it "counts credits for organization" do
|
||||
create_list(:credit, random_number, organization: organization)
|
||||
described_class.counter_culture_fix_counts
|
||||
expect(organization.reload.credits_count).to eq(random_number)
|
||||
end
|
||||
it "counts credits for organization" do
|
||||
described_class.counter_culture_fix_counts
|
||||
expect(organization.reload.credits_count).to eq(organization.credits.size)
|
||||
end
|
||||
end
|
||||
|
||||
it "counts the number of unspent credits for a user" do
|
||||
create_list(:credit, random_number, user: user)
|
||||
expect(user.reload.unspent_credits_count).to eq(random_number)
|
||||
end
|
||||
describe "#unspent_credits_count" do
|
||||
it "counts the number of unspent credits for a user" do
|
||||
expect(user.reload.unspent_credits_count).to eq(user.credits.unspent.size)
|
||||
end
|
||||
|
||||
it "counts the number of spent credits for a user" do
|
||||
create_list(:credit, random_number, user: user, spent: true)
|
||||
expect(user.reload.spent_credits_count).to eq(random_number)
|
||||
end
|
||||
it "counts the number of unspent credits for an organization" do
|
||||
expect(organization.reload.unspent_credits_count).to eq(organization.credits.unspent.size)
|
||||
end
|
||||
end
|
||||
|
||||
it "counts the number of unspent credits for an organization" do
|
||||
create_list(:credit, random_number, organization: organization)
|
||||
expect(organization.reload.unspent_credits_count).to eq(random_number)
|
||||
end
|
||||
describe "#spent_credits_count" do
|
||||
it "counts the number of spent credits for a user" do
|
||||
create_list(:credit, 1, user: user, spent: true)
|
||||
expect(user.reload.spent_credits_count).to eq(user.credits.spent.size)
|
||||
end
|
||||
|
||||
it "counts the number of spent credits for an organization" do
|
||||
create_list(:credit, random_number, organization: organization, spent: true)
|
||||
expect(organization.reload.spent_credits_count).to eq(random_number)
|
||||
it "counts the number of spent credits for an organization" do
|
||||
create_list(:credit, 1, organization: organization, spent: true)
|
||||
expect(organization.reload.spent_credits_count).to eq(organization.credits.spent.size)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#purchase" do
|
||||
let(:listing) { create(:classified_listing) }
|
||||
let_it_be(:credit) { build(:credit) }
|
||||
|
||||
it "associates to a purchase" do
|
||||
credit = create(:credit, purchase: listing)
|
||||
expect(credit.purchase).to eq(listing)
|
||||
it "is valid with a purchase" do
|
||||
credit.purchase = build(:classified_listing)
|
||||
expect(credit).to be_valid
|
||||
end
|
||||
|
||||
it "is valid without a purchase" do
|
||||
credit = create(:credit, purchase: nil)
|
||||
credit.purchase = nil
|
||||
expect(credit).to be_valid
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe DisplayAdEvent, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:organization) { create(:organization) }
|
||||
let(:display_ad) { create(:display_ad, organization_id: organization.id) }
|
||||
let_it_be(:user) { build(:user) }
|
||||
let_it_be(:organization) { build(:organization) }
|
||||
let_it_be(:display_ad) { build(:display_ad, organization: organization) }
|
||||
|
||||
it "creates a click event" do
|
||||
event = build(:display_ad_event, category: "click", user_id: user.id, display_ad_id: display_ad.id)
|
||||
expect(event).to be_valid
|
||||
end
|
||||
describe "#category" do
|
||||
it "is valid with a click category" do
|
||||
event = build(:display_ad_event, category: "click", user: user, display_ad: display_ad)
|
||||
expect(event).to be_valid
|
||||
end
|
||||
|
||||
it "creates an impression event" do
|
||||
event = build(:display_ad_event, category: "impression", user_id: user.id, display_ad_id: display_ad.id)
|
||||
expect(event).to be_valid
|
||||
end
|
||||
it "is valid with an impression category" do
|
||||
event = build(:display_ad_event, category: "impression", user: user, display_ad: display_ad)
|
||||
expect(event).to be_valid
|
||||
end
|
||||
|
||||
it "does not create an invalid event" do
|
||||
event = build(:display_ad_event, category: "wazoo", user_id: user.id, display_ad_id: display_ad.id)
|
||||
expect(event).not_to be_valid
|
||||
it "is not valid with an uknown category" do
|
||||
event = build(:display_ad_event, category: "wazoo", user: user, display_ad: display_ad)
|
||||
expect(event).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,38 +1,52 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe DisplayAd, type: :model do
|
||||
let(:display_ad) { create(:display_ad, organization_id: organization.id) }
|
||||
let(:organization) { create(:organization) }
|
||||
let_it_be(:organization) { create(:organization) }
|
||||
let_it_be(:display_ad) { create(:display_ad, organization_id: organization.id) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:organization_id) }
|
||||
it { is_expected.to validate_presence_of(:placement_area) }
|
||||
it { is_expected.to validate_presence_of(:body_markdown) }
|
||||
|
||||
it "generates processed_html before save" do
|
||||
expect(display_ad.processed_html).to eq("Hello <em>hey</em> Hey hey")
|
||||
describe "validations" do
|
||||
it "allows sidebar_right" do
|
||||
display_ad.placement_area = "sidebar_right"
|
||||
expect(display_ad).to be_valid
|
||||
end
|
||||
|
||||
it "allows sidebar_left" do
|
||||
display_ad.placement_area = "sidebar_left"
|
||||
expect(display_ad).to be_valid
|
||||
end
|
||||
|
||||
it "disallows unacceptable placement_area" do
|
||||
display_ad.placement_area = "tsdsdsdds"
|
||||
expect(display_ad).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
it "only disallows unacceptable placement_area" do
|
||||
display_ad.placement_area = "tsdsdsdds"
|
||||
expect(display_ad).not_to be_valid
|
||||
context "when callbacks are triggered before save" do
|
||||
it "generates #processed_html from #body_markdown" do
|
||||
expect(display_ad.processed_html).to eq("Hello <em>hey</em> Hey hey")
|
||||
end
|
||||
end
|
||||
|
||||
it "allows sidebar_right" do
|
||||
display_ad.placement_area = "sidebar_right"
|
||||
expect(display_ad).to be_valid
|
||||
end
|
||||
describe ".for_display" do
|
||||
let!(:display_ad) { create(:display_ad, organization_id: organization.id) }
|
||||
|
||||
it "allows sidebar_left" do
|
||||
display_ad.placement_area = "sidebar_left"
|
||||
expect(display_ad).to be_valid
|
||||
end
|
||||
it "does not return unpublished ads" do
|
||||
display_ad.update!(published: false, approved: true)
|
||||
expect(described_class.for_display(display_ad.placement_area)).to be_nil
|
||||
end
|
||||
|
||||
it "displays published and approved posts" do
|
||||
create(:display_ad, organization_id: organization.id, published: true, approved: true)
|
||||
create(:display_ad, organization_id: organization.id, published: true, approved: true)
|
||||
create(:display_ad, organization_id: organization.id, published: false, approved: true)
|
||||
create(:display_ad, organization_id: organization.id, published: true, approved: false)
|
||||
expect(described_class.for_display(described_class.last.placement_area).published).to eq(true)
|
||||
expect(described_class.for_display(described_class.last.placement_area).approved).to eq(true)
|
||||
it "does not return unapproved ads" do
|
||||
display_ad.update!(published: true, approved: false)
|
||||
expect(described_class.for_display(display_ad.placement_area)).to be_nil
|
||||
end
|
||||
|
||||
it "returns published and approved ads" do
|
||||
display_ad.update!(published: true, approved: true)
|
||||
expect(described_class.for_display(display_ad.placement_area)).to eq(display_ad)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Event, type: :model do
|
||||
let(:event) { create(:event) }
|
||||
let(:event) { build(:event) }
|
||||
|
||||
it "rejects title with over 90 characters" do
|
||||
event.title = Faker::Lorem.characters(number: 100)
|
||||
|
|
@ -14,16 +14,18 @@ RSpec.describe Event, type: :model do
|
|||
end
|
||||
|
||||
it "rejects ends times that are earlier than start times" do
|
||||
event.ends_at = 14.hours.ago
|
||||
event.ends_at = event.starts_at - 1.minute
|
||||
expect(event).not_to be_valid
|
||||
end
|
||||
|
||||
it "creates slug for published events" do
|
||||
event.published = true
|
||||
expect(event).to be_valid
|
||||
event = build(:event, category: "ama", title: "yo", published: true)
|
||||
event.validate!
|
||||
expected_slug = "#{event.category}-#{event.title}-#{event.starts_at.strftime('%m-%d-%Y')}"
|
||||
expect(event.slug).to eq(expected_slug)
|
||||
end
|
||||
|
||||
it "triggers cache busting on save" do
|
||||
expect { build(:event).save }.to have_enqueued_job.on_queue("events_bust_cache")
|
||||
expect { event.save }.to have_enqueued_job.on_queue("events_bust_cache")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe FeedbackMessage, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:abuse_report) { create(:feedback_message, :abuse_report) }
|
||||
|
||||
describe "validations for an abuse report" do
|
||||
subject(:feedback_message) do
|
||||
described_class.new(
|
||||
|
|
|
|||
|
|
@ -8,11 +8,9 @@ RSpec.describe HtmlVariant, type: :model do
|
|||
it { is_expected.to belong_to(:user).optional }
|
||||
|
||||
it "calculates success rate" do
|
||||
HtmlVariantTrial.create!(html_variant_id: html_variant.id)
|
||||
HtmlVariantTrial.create!(html_variant_id: html_variant.id)
|
||||
HtmlVariantTrial.create!(html_variant_id: html_variant.id)
|
||||
HtmlVariantTrial.create!(html_variant_id: html_variant.id)
|
||||
4.times { HtmlVariantTrial.create!(html_variant_id: html_variant.id) }
|
||||
HtmlVariantSuccess.create!(html_variant_id: html_variant.id)
|
||||
|
||||
html_variant.calculate_success_rate!
|
||||
expect(html_variant.success_rate).to eq(0.025)
|
||||
end
|
||||
|
|
@ -35,14 +33,13 @@ RSpec.describe HtmlVariant, type: :model do
|
|||
end
|
||||
|
||||
it "finds if no tag targeted and tag given" do
|
||||
html_variant.save!
|
||||
html_variant.update(target_tag: nil)
|
||||
expect(described_class.find_for_test(["hello"]).id).to eq(html_variant.id)
|
||||
end
|
||||
|
||||
it "creates an html variant with img in it" do
|
||||
html_variant = create(:html_variant, approved: false, published: true)
|
||||
html_variant.html = "<div><img src='https://devimages.com/image.jpg' /></div>"
|
||||
html_variant.save!
|
||||
it "prefixes an image with cloudinary" do
|
||||
html = "<div><img src='https://devimages.com/image.jpg' /></div>"
|
||||
html_variant.update(approved: false, html: html)
|
||||
expect(html_variant.html).to include("cloudinary")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@ RSpec.describe Identity, type: :model do
|
|||
|
||||
it do
|
||||
# rubocop:disable RSpec/NamedSubject
|
||||
# see <https://github.com/thoughtbot/shoulda-matchers/issues/682>
|
||||
subject.user = create(:user)
|
||||
subject.user = build(:user)
|
||||
expect(subject).to validate_uniqueness_of(:user_id).scoped_to(:provider)
|
||||
# rubocop:enable RSpec/NamedSubject
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,56 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe JobOpportunity, type: :model do
|
||||
it "returns remoteness in words for remoteness" do
|
||||
jo = described_class.new
|
||||
jo.remoteness = "on_premise"
|
||||
expect(jo.remoteness_in_words).to eq("In Office")
|
||||
let(:job_opportunity) { described_class.new(remoteness: "on_premise") }
|
||||
|
||||
context "when validations" do
|
||||
describe "#remoteness" do
|
||||
it "is valid with on_premise" do
|
||||
job_opportunity.remoteness = "on_premise"
|
||||
expect(job_opportunity).to be_valid
|
||||
end
|
||||
|
||||
it "is valid with fully_remote" do
|
||||
job_opportunity.remoteness = "fully_remote"
|
||||
expect(job_opportunity).to be_valid
|
||||
end
|
||||
|
||||
it "is valid with remote_optional" do
|
||||
job_opportunity.remoteness = "remote_optional"
|
||||
expect(job_opportunity).to be_valid
|
||||
end
|
||||
|
||||
it "is valid with on_premise_flexible" do
|
||||
job_opportunity.remoteness = "on_premise_flexible"
|
||||
expect(job_opportunity).to be_valid
|
||||
end
|
||||
|
||||
it "is not valid an arbitrary word" do
|
||||
job_opportunity.remoteness = "foobar"
|
||||
expect(job_opportunity).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#remoteness_in_words" do
|
||||
it "returns remoteness in words for on_premise" do
|
||||
job_opportunity.remoteness = "on_premise"
|
||||
expect(job_opportunity.remoteness_in_words).to eq("In Office")
|
||||
end
|
||||
|
||||
it "returns remoteness in words for fully_remote" do
|
||||
job_opportunity.remoteness = "fully_remote"
|
||||
expect(job_opportunity.remoteness_in_words).to eq("Fully Remote")
|
||||
end
|
||||
|
||||
it "returns remoteness in words for remote_optional" do
|
||||
job_opportunity.remoteness = "remote_optional"
|
||||
expect(job_opportunity.remoteness_in_words).to eq("Remote Optional")
|
||||
end
|
||||
|
||||
it "returns remoteness in words for on_premise_flexible" do
|
||||
job_opportunity.remoteness = "on_premise_flexible"
|
||||
expect(job_opportunity.remoteness_in_words).to eq("Mostly in Office but Flexible")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Mention, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:article) { create(:article, user_id: user.id) }
|
||||
let(:comment) { create(:comment, user_id: user.id, commentable_id: article.id) }
|
||||
let(:comment) { create(:comment, commentable: create(:podcast_episode)) }
|
||||
|
||||
it "calls on Mentions::CreateAllJob" do
|
||||
described_class.create_all(comment) do
|
||||
expect(Mentions::CreateAllJob).to have_received(:perform_later).with(comment.id, "Comment")
|
||||
describe "#create_all" do
|
||||
it "enqueues a job to create mentions" do
|
||||
assert_enqueued_with(job: Mentions::CreateAllJob, args: [comment.id, "Comment"], queue: "mentions_create_all") do
|
||||
described_class.create_all(comment)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Replace this test with validation spec
|
||||
it "creates a valid mention" do
|
||||
expect(create(:mention)).to be_valid
|
||||
end
|
||||
|
||||
# TODO: Replace this test with validation spec
|
||||
it "doesn't raise undefined method for NilClass on valid?" do
|
||||
expect(described_class.new.valid?).to eq(false)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,84 +2,90 @@ require "rails_helper"
|
|||
|
||||
RSpec.describe Message, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:chat_channel) { create(:chat_channel) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:long_text) { Faker::Hipster.words(number: 1500) }
|
||||
let(:chat_channel) { create(:chat_channel) }
|
||||
let(:message) { create(:message, user: user) }
|
||||
|
||||
describe "validations" do
|
||||
before do
|
||||
allow(ChatChannel).to receive(:find).and_return(ChatChannel.new)
|
||||
context "with automatic validations" do
|
||||
before do
|
||||
allow(ChatChannel).to receive(:find).and_return(ChatChannel.new)
|
||||
end
|
||||
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to belong_to(:chat_channel) }
|
||||
it { is_expected.to validate_presence_of(:message_html) }
|
||||
it { is_expected.to validate_presence_of(:message_markdown) }
|
||||
end
|
||||
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to belong_to(:chat_channel) }
|
||||
it { is_expected.to validate_presence_of(:message_html) }
|
||||
it { is_expected.to validate_presence_of(:message_markdown) }
|
||||
it "is invalid without channel permission for invite only channels" do
|
||||
chat_channel.update(channel_type: "invite_only")
|
||||
message = build(:message, chat_channel: chat_channel, user: user)
|
||||
expect(message).not_to be_valid
|
||||
end
|
||||
|
||||
it "is valid with channel permission" do
|
||||
chat_channel.add_users([user])
|
||||
message = build(:message, chat_channel: chat_channel, user: user)
|
||||
expect(message).to be_valid
|
||||
end
|
||||
|
||||
it "is invalid with text over 1024 chars" do
|
||||
message = build(:message, chat_channel: chat_channel, user: user, message_markdown: "x" * 1025)
|
||||
expect(message).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
it "is invalid without channel permission for non-open channels" do
|
||||
chat_channel.update(channel_type: "invite_only")
|
||||
message = build(:message, chat_channel_id: chat_channel.id, user_id: user2.id)
|
||||
expect(message).not_to be_valid
|
||||
context "when callbacks are triggered before validation" do
|
||||
let_it_be(:article) { create(:article) }
|
||||
|
||||
describe "#message_html" do
|
||||
it "creates rich link with proper link" do
|
||||
message.message_markdown = "hello http://#{ApplicationConfig['APP_DOMAIN']}#{article.path}"
|
||||
message.validate!
|
||||
|
||||
expect(message.message_html).to include(article.title)
|
||||
expect(message.message_html).to include("data-content")
|
||||
end
|
||||
|
||||
it "creates rich link with non-rich link" do
|
||||
message.message_markdown = "hello http://#{ApplicationConfig['APP_DOMAIN']}/report-abuse"
|
||||
message.validate!
|
||||
|
||||
expect(message.message_html).not_to include("data-content")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "is valid with channel permission" do
|
||||
chat_channel.add_users([user2])
|
||||
message = build(:message, chat_channel_id: chat_channel.id, user_id: user2.id)
|
||||
expect(message).to be_valid
|
||||
end
|
||||
context "when callbacks are triggered after create" do
|
||||
before do
|
||||
chat_channel.add_users([user, user2])
|
||||
end
|
||||
|
||||
it "is invalid if over 1024 chars" do
|
||||
message = build(:message, chat_channel_id: chat_channel.id, user_id: user.id,
|
||||
message_markdown: long_text)
|
||||
expect(message).not_to be_valid
|
||||
end
|
||||
it "sends email if user not recently active on /connect" do
|
||||
chat_channel.update_column(:channel_type, "direct")
|
||||
user2.update_column(:updated_at, 1.day.ago)
|
||||
user2.chat_channel_memberships.last.update_column(:last_opened_at, 2.days.ago)
|
||||
|
||||
it "is valid if under 1024 chars" do
|
||||
message = build(:message, chat_channel_id: chat_channel.id, user_id: user.id,
|
||||
message_markdown: "hello")
|
||||
expect(message).to be_valid
|
||||
end
|
||||
create(:message, chat_channel: chat_channel, user: user)
|
||||
|
||||
it "creates rich link in connect with proper link" do
|
||||
article = create(:article)
|
||||
message = create(:message, chat_channel_id: chat_channel.id, user_id: user.id,
|
||||
message_markdown: "hello http://#{ApplicationConfig['APP_DOMAIN']}#{article.path}")
|
||||
expect(message.message_html).to include(article.title)
|
||||
expect(message.message_html).to include("data-content")
|
||||
end
|
||||
expect(EmailMessage.last.subject).to start_with("#{user.name} just messaged you")
|
||||
end
|
||||
|
||||
it "creates rich link in connect with non-rich link" do
|
||||
message = create(:message, chat_channel_id: chat_channel.id, user_id: user.id,
|
||||
message_markdown: "hello http://#{ApplicationConfig['APP_DOMAIN']}/report-abuse")
|
||||
expect(message.message_html).not_to include("data-content")
|
||||
end
|
||||
it "does not send email if user has been recently active" do
|
||||
expect do
|
||||
create(:message, chat_channel: chat_channel, user: user)
|
||||
end.to change(EmailMessage, :count).by(0)
|
||||
end
|
||||
|
||||
it "sends email if user not recently active on /connect" do
|
||||
chat_channel.add_users([user, user2])
|
||||
chat_channel.update_column(:channel_type, "direct")
|
||||
user2.update_column(:updated_at, 1.day.ago)
|
||||
user2.chat_channel_memberships.last.update_column(:last_opened_at, 2.days.ago)
|
||||
create(:message, chat_channel_id: chat_channel.id, user_id: user.id,
|
||||
message_markdown: "hello http://#{ApplicationConfig['APP_DOMAIN']}/report-abuse")
|
||||
expect(EmailMessage.last.subject).to start_with("#{user.name} just messaged you")
|
||||
end
|
||||
it "does not send email if user has email_messages turned off" do
|
||||
chat_channel.update_column(:channel_type, "direct")
|
||||
user2.update_columns(updated_at: 1.day.ago, email_connect_messages: false)
|
||||
user2.chat_channel_memberships.last.update_column(:last_opened_at, 2.days.ago)
|
||||
|
||||
it "does not send email if user has been recently active" do
|
||||
chat_channel.add_users([user, user2])
|
||||
create(:message, chat_channel_id: chat_channel.id, user_id: user.id,
|
||||
message_markdown: "hello http://#{ApplicationConfig['APP_DOMAIN']}/report-abuse")
|
||||
expect(EmailMessage.all.size).to eq(0)
|
||||
end
|
||||
|
||||
it "does not send email if user has email_messages turned off" do
|
||||
chat_channel.add_users([user, user2])
|
||||
chat_channel.update_column(:channel_type, "direct")
|
||||
user2.update_column(:updated_at, 1.day.ago)
|
||||
user2.update_column(:email_connect_messages, false)
|
||||
user2.chat_channel_memberships.last.update_column(:last_opened_at, 2.days.ago)
|
||||
create(:message, chat_channel_id: chat_channel.id, user_id: user.id,
|
||||
message_markdown: "hello http://#{ApplicationConfig['APP_DOMAIN']}/report-abuse")
|
||||
expect(EmailMessage.all.size).to eq(0)
|
||||
expect do
|
||||
create(:message, chat_channel: chat_channel, user: user)
|
||||
end.to change(EmailMessage, :count).by(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Notification, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:user3) { create(:user) }
|
||||
let(:organization) { create(:organization) }
|
||||
let(:article) { create(:article, :with_notification_subscription, user_id: user.id, page_views_count: 4000, positive_reactions_count: 70) }
|
||||
let(:follow_instance) { user.follow(user2) }
|
||||
let(:badge_achievement) { create(:badge_achievement) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:user2) { create(:user) }
|
||||
let_it_be(:user3) { create(:user) }
|
||||
let_it_be(:organization) { create(:organization) }
|
||||
let_it_be(:article) do
|
||||
create(:article, :with_notification_subscription, user: user, page_views_count: 4000, positive_reactions_count: 70)
|
||||
end
|
||||
let_it_be(:user_follows_user2) { user.follow(user2) }
|
||||
let_it_be(:comment) { create(:comment, user: user2, commentable: article) }
|
||||
let_it_be(:badge_achievement) { create(:badge_achievement) }
|
||||
|
||||
it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(%i[organization_id notifiable_id notifiable_type action]) }
|
||||
it do
|
||||
scopes = %i[organization_id notifiable_id notifiable_type action]
|
||||
# rubocop:disable RSpec/NamedSubject
|
||||
expect(subject).to validate_uniqueness_of(:user_id).scoped_to(scopes)
|
||||
# rubocop:enable RSpec/NamedSubject
|
||||
end
|
||||
|
||||
describe "when trying to create duplicate notifications" do
|
||||
# Duplicate notifications are not allowed even when validations are skipped
|
||||
|
|
@ -27,8 +35,12 @@ RSpec.describe Notification, type: :model do
|
|||
end
|
||||
|
||||
it "updates when trying to create a duplicate notification via import" do
|
||||
notification = create(:notification, user: user, notifiable: article, action: "Reaction", json_data: { "user_id" => 1 })
|
||||
duplicate_notification = build(:notification, user: user, notifiable: article, action: "Reaction", json_data: { "user_id" => 2 })
|
||||
notification = create(
|
||||
:notification, user: user, notifiable: article, action: "Reaction", json_data: { "user_id" => 1 }
|
||||
)
|
||||
duplicate_notification = build(
|
||||
:notification, user: user, notifiable: article, action: "Reaction", json_data: { "user_id" => 2 }
|
||||
)
|
||||
described_class.import([duplicate_notification],
|
||||
on_duplicate_key_update: {
|
||||
conflict_target: %i[notifiable_id notifiable_type user_id action],
|
||||
|
|
@ -85,170 +97,198 @@ RSpec.describe Notification, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe "when trying to #send_new_follower_notification after following a tag" do
|
||||
let(:tag) { create(:tag) }
|
||||
let(:tag_follow) { user.follow(tag) }
|
||||
|
||||
it "runs fine" do
|
||||
perform_enqueued_jobs do
|
||||
described_class.send_new_follower_notification(tag_follow)
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't create a notification" do
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_new_follower_notification(tag_follow)
|
||||
end.not_to change(described_class, :count)
|
||||
end
|
||||
context "when callbacks are triggered after create" do
|
||||
it "sets the notified_at column" do
|
||||
notification = create(:notification, notifiable: article, user: user2)
|
||||
expect(notification.notified_at).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_new_follower_notification" do
|
||||
before do
|
||||
perform_enqueued_jobs do
|
||||
described_class.send_new_follower_notification(follow_instance)
|
||||
context "when trying to a send notification after following a tag" do
|
||||
it "does not enqueue a notification job" do
|
||||
assert_no_enqueued_jobs(only: Notifications::NewFollowerJob) do
|
||||
tag_follow = user.follow(create(:tag))
|
||||
described_class.send_new_follower_notification(tag_follow)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "sets the notifiable_at column upon creation" do
|
||||
expect(described_class.last.notified_at).not_to eq nil
|
||||
end
|
||||
|
||||
context "when a user follows another user" do
|
||||
context "when trying to send a notification after following a user" do
|
||||
it "creates a notification belonging to the person being followed" do
|
||||
expect(described_class.first.user_id).to eq user2.id
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_new_follower_notification(user_follows_user2)
|
||||
end.to change(user2.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
it "creates a notification from the follow instance" do
|
||||
notifiable_data = { notifiable_id: described_class.first.notifiable_id, notifiable_type: described_class.first.notifiable_type }
|
||||
follow_data = { notifiable_id: follow_instance.id, notifiable_type: follow_instance.class.name }
|
||||
expect(notifiable_data).to eq follow_data
|
||||
perform_enqueued_jobs do
|
||||
described_class.send_new_follower_notification(user_follows_user2)
|
||||
end
|
||||
|
||||
notification = user2.notifications.last
|
||||
notifiable_data = { notifiable_id: notification.notifiable_id, notifiable_type: notification.notifiable_type }
|
||||
follow_data = { notifiable_id: user_follows_user2.id, notifiable_type: user_follows_user2.class.name }
|
||||
expect(notifiable_data).to eq(follow_data)
|
||||
end
|
||||
end
|
||||
|
||||
context "when a user follows an organization" do
|
||||
let(:follow_instance) { user.follow(organization) }
|
||||
let_it_be(:user_follows_organization) { user.follow(organization) }
|
||||
|
||||
it "creates a notification belonging to the organization" do
|
||||
expect(described_class.first.organization_id).to eq organization.id
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_new_follower_notification(user_follows_organization)
|
||||
end.to change(organization.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
it "does not create a notification belonging to a user" do
|
||||
expect(described_class.first.user_id).to eq nil
|
||||
perform_enqueued_jobs do
|
||||
described_class.send_new_follower_notification(user_follows_organization)
|
||||
expect(organization.notifications.last.user_id).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
it "creates a notification from the follow instance" do
|
||||
notifiable_data = { notifiable_id: described_class.first.notifiable_id, notifiable_type: described_class.first.notifiable_type }
|
||||
follow_data = { notifiable_id: follow_instance.id, notifiable_type: follow_instance.class.name }
|
||||
expect(notifiable_data).to eq follow_data
|
||||
perform_enqueued_jobs do
|
||||
described_class.send_new_follower_notification(user_follows_organization)
|
||||
end
|
||||
|
||||
notification = organization.notifications.last
|
||||
notifiable_data = { notifiable_id: notification.notifiable_id, notifiable_type: notification.notifiable_type }
|
||||
follow_data = {
|
||||
notifiable_id: user_follows_organization.id,
|
||||
notifiable_type: user_follows_organization.class.name
|
||||
}
|
||||
expect(notifiable_data).to eq(follow_data)
|
||||
end
|
||||
end
|
||||
|
||||
context "when a user unfollows another user" do
|
||||
it "destroys the follow notification" do
|
||||
follow_instance = user.stop_following(user2)
|
||||
# first we follow the user
|
||||
perform_enqueued_jobs do
|
||||
described_class.send_new_follower_notification(follow_instance)
|
||||
described_class.send_new_follower_notification(user_follows_user2)
|
||||
end
|
||||
|
||||
# then we stop following them
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
user_stops_following_user2 = user.stop_following(user2)
|
||||
described_class.send_new_follower_notification(user_stops_following_user2)
|
||||
end.to change(user2.notifications, :count).by(-1)
|
||||
end
|
||||
expect(described_class.count).to eq 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_new_comment_notifications" do
|
||||
let_it_be(:comment) { create(:comment, user: user2, commentable: article) }
|
||||
let_it_be(:child_comment) { create(:comment, user: user3, commentable: article, parent: comment) }
|
||||
|
||||
context "when all commenters are subscribed" do
|
||||
it "sends a notification to the author of the article" do
|
||||
comment = create(:comment, user: user2, commentable: article)
|
||||
described_class.send_new_comment_notifications_without_delay(comment)
|
||||
expect(user.notifications.count).to eq 1
|
||||
expect do
|
||||
described_class.send_new_comment_notifications_without_delay(comment)
|
||||
end.to change(user.notifications, :count).by(1)
|
||||
end
|
||||
|
||||
it "does not send a notification to the author of the article if the commenter is the author" do
|
||||
comment = create(:comment, user: user, commentable: article)
|
||||
described_class.send_new_comment_notifications_without_delay(comment)
|
||||
expect(user.notifications.count).to eq 0
|
||||
expect do
|
||||
described_class.send_new_comment_notifications_without_delay(comment)
|
||||
end.to change(user.notifications, :count).by(0)
|
||||
end
|
||||
|
||||
it "does not send a notification to the author of the comment" do
|
||||
comment = create(:comment, user: user2, commentable: article)
|
||||
described_class.send_new_comment_notifications_without_delay(comment)
|
||||
expect(user2.notifications.count).to eq 0
|
||||
expect do
|
||||
described_class.send_new_comment_notifications_without_delay(comment)
|
||||
end.to change(user2.notifications, :count).by(0)
|
||||
end
|
||||
|
||||
it "sends a notification to the author of the article about the child comment" do
|
||||
parent_comment = create(:comment, user: user2, commentable: article)
|
||||
child_comment = create(:comment, user: user3, commentable: article, ancestry: parent_comment.id.to_s)
|
||||
described_class.send_new_comment_notifications_without_delay(child_comment)
|
||||
expect(user.notifications.count).to eq(1)
|
||||
expect do
|
||||
described_class.send_new_comment_notifications_without_delay(child_comment)
|
||||
end.to change(user.notifications, :count).by(1)
|
||||
end
|
||||
|
||||
it "sends a notification to the organization" do
|
||||
org = create(:organization)
|
||||
create(:organization_membership, user: user, organization: org)
|
||||
article.update(organization: org)
|
||||
create(:organization_membership, user: user, organization: organization)
|
||||
article.update(organization: organization)
|
||||
comment = create(:comment, user: user2, commentable: article)
|
||||
described_class.send_new_comment_notifications_without_delay(comment)
|
||||
expect(org.notifications.count).to eq 1
|
||||
|
||||
expect do
|
||||
described_class.send_new_comment_notifications_without_delay(comment)
|
||||
end.to change(organization.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the author of the article is not subscribed" do
|
||||
let!(:comment) { create(:comment, user: user2, commentable: article) }
|
||||
|
||||
before do
|
||||
article.update(receive_notifications: false)
|
||||
article.notification_subscriptions.delete_all
|
||||
end
|
||||
|
||||
it "does not send a notification to the author of the article" do
|
||||
described_class.send_new_comment_notifications_without_delay(comment)
|
||||
expect(user.notifications.count).to eq 0
|
||||
expect do
|
||||
described_class.send_new_comment_notifications_without_delay(comment)
|
||||
end.to change(user.notifications, :count).by(0)
|
||||
end
|
||||
|
||||
it "doesn't send a notification to the author of the article about the child comment" do
|
||||
child_comment = create(:comment, user: user3, commentable: article, ancestry: comment.id.to_s)
|
||||
described_class.send_new_comment_notifications_without_delay(child_comment)
|
||||
expect(user.notifications.count).to eq 0
|
||||
expect do
|
||||
described_class.send_new_comment_notifications_without_delay(child_comment)
|
||||
end.to change(user.notifications, :count).by(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the author of a comment is not subscribed" do
|
||||
let(:parent_comment) { create(:comment, user: user2, commentable: article) }
|
||||
let!(:child_comment) { create(:comment, user: user3, commentable: article, ancestry: parent_comment.id.to_s) }
|
||||
|
||||
before do
|
||||
parent_comment.update(receive_notifications: false)
|
||||
comment.update(receive_notifications: false)
|
||||
end
|
||||
|
||||
it "does not send a notification to the author of the comment" do
|
||||
described_class.send_new_comment_notifications_without_delay(child_comment)
|
||||
expect(user2.notifications.count).to eq 0
|
||||
expect do
|
||||
described_class.send_new_comment_notifications_without_delay(child_comment)
|
||||
end.to change(child_comment.user.notifications, :count).by(0)
|
||||
end
|
||||
|
||||
it "sends a notification to the author of the article" do
|
||||
described_class.send_new_comment_notifications_without_delay(child_comment)
|
||||
expect(user.notifications.count).to eq(1)
|
||||
expect do
|
||||
described_class.send_new_comment_notifications_without_delay(child_comment)
|
||||
end.to change(user.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_reaction_notification" do
|
||||
before do
|
||||
article.update_columns(receive_notifications: true)
|
||||
comment.update_columns(receive_notifications: true)
|
||||
end
|
||||
|
||||
context "when reactable is receiving notifications" do
|
||||
it "sends a notification to the author of a comment" do
|
||||
comment = create(:comment, user: user2, commentable: article)
|
||||
reaction = create(:reaction, reactable: comment, user: user)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
expect(user2.notifications.count).to eq 1
|
||||
expect do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
end.to change(comment.user.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
it "sends a notification to the author of an article" do
|
||||
reaction = create(:reaction, reactable: article, user: user2)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
expect(user.notifications.count).to eq 1
|
||||
expect do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
end.to change(article.user.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -256,17 +296,18 @@ RSpec.describe Notification, type: :model do
|
|||
context "when a reaction is destroyed" do
|
||||
let(:comment) { create(:comment, user: user2, commentable: article) }
|
||||
let!(:notification) { create(:notification, user: user, notifiable: comment, action: "Reaction") }
|
||||
let(:sibling_reaction) { create(:reaction, reactable: comment, user: user3) }
|
||||
|
||||
it "destroys the notification if it exists" do
|
||||
reaction = create(:reaction, reactable: comment, user: user)
|
||||
reaction.destroy
|
||||
described_class.send_reaction_notification_without_delay(reaction, article.user)
|
||||
expect(described_class.where(id: notification.id)).not_to be_any
|
||||
expect(described_class.exists?(id: notification.id)).to be(false)
|
||||
end
|
||||
|
||||
it "keeps the notification if siblings exist" do
|
||||
reaction = create(:reaction, reactable: comment, user: user)
|
||||
create(:reaction, reactable: comment, user: user3)
|
||||
sibling_reaction # to create it
|
||||
reaction.destroy
|
||||
described_class.send_reaction_notification_without_delay(reaction, article.user)
|
||||
notification.reload
|
||||
|
|
@ -275,7 +316,7 @@ RSpec.describe Notification, type: :model do
|
|||
|
||||
it "doesn't keep data of the destroyed reaction in the notification" do
|
||||
reaction = create(:reaction, reactable: comment, user: user)
|
||||
create(:reaction, reactable: comment, user: user3)
|
||||
sibling_reaction # to create it
|
||||
reaction.destroy
|
||||
described_class.send_reaction_notification_without_delay(reaction, article.user)
|
||||
notification.reload
|
||||
|
|
@ -283,6 +324,22 @@ RSpec.describe Notification, type: :model do
|
|||
# not the user of the destroyed reaction!
|
||||
expect(notification.json_data["user"]["id"]).to eq(user3.id)
|
||||
end
|
||||
|
||||
it "creates and destroys the notification properly" do
|
||||
reaction = create(:reaction, user: user2, reactable: article, category: "like")
|
||||
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
end.to change(user.notifications, :count).by(1)
|
||||
|
||||
reaction.destroy!
|
||||
|
||||
expect do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
end.to change(user.notifications, :count).by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when reactable is not receiving notifications" do
|
||||
|
|
@ -290,96 +347,94 @@ RSpec.describe Notification, type: :model do
|
|||
comment = create(:comment, user: user2, commentable: article)
|
||||
comment.update(receive_notifications: false)
|
||||
reaction = create(:reaction, reactable: comment, user: user)
|
||||
described_class.send_reaction_notification_without_delay(reaction, reaction.reactable.user)
|
||||
expect(user2.notifications.count).to eq 0
|
||||
|
||||
expect do
|
||||
described_class.send_reaction_notification_without_delay(reaction, reaction.reactable.user)
|
||||
end.to change(user.notifications, :count).by(0)
|
||||
end
|
||||
|
||||
it "does not send a notification to the author of an article" do
|
||||
article.update(receive_notifications: false)
|
||||
reaction = create(:reaction, reactable: article, user: user2)
|
||||
described_class.send_reaction_notification_without_delay(reaction, reaction.reactable.user)
|
||||
expect(user.notifications.count).to eq 0
|
||||
|
||||
expect do
|
||||
described_class.send_reaction_notification_without_delay(reaction, reaction.reactable.user)
|
||||
end.to change(user.notifications, :count).by(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the reactable is an organization's article" do
|
||||
let(:org) { create(:organization) }
|
||||
|
||||
before do
|
||||
create(:organization_membership, user: user, organization: org, type_of_user: "admin")
|
||||
article.update(organization: org)
|
||||
create(:organization_membership, user: user, organization: organization, type_of_user: "admin")
|
||||
article.update(organization: organization)
|
||||
end
|
||||
|
||||
it "creates a notification with the organization's ID" do
|
||||
reaction = create(:reaction, reactable: article, user: user2)
|
||||
described_class.send_reaction_notification_without_delay(reaction, reaction.reactable.organization)
|
||||
expect(org.notifications.count).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
it "creates positive reaction notification" do
|
||||
reaction = article.reactions.create!(
|
||||
user_id: user2.id,
|
||||
category: "like",
|
||||
)
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
end.to change(described_class, :count).by(1)
|
||||
described_class.send_reaction_notification_without_delay(reaction, reaction.reactable.organization)
|
||||
end.to change(organization.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
it "does not create negative notification" do
|
||||
user2.add_role(:trusted)
|
||||
reaction = article.reactions.create!(
|
||||
user_id: user2.id,
|
||||
category: "vomit",
|
||||
)
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
end.not_to change(described_class, :count)
|
||||
end
|
||||
end
|
||||
context "when dealing with positive and negative reactions" do
|
||||
it "creates a notification for a positive reaction" do
|
||||
reaction = create(:reaction, reactable: article, user: user2, category: "like")
|
||||
|
||||
it "destroys the notification properly" do
|
||||
reaction = create(:reaction, user: user2, reactable: article, category: "like")
|
||||
perform_enqueued_jobs do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
reaction.destroy!
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
expect(described_class.count).to eq 0
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
end.to change(article.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
it "does not create a notification for a negative reaction" do
|
||||
user2.add_role(:trusted)
|
||||
reaction = create(:reaction, reactable: article, user: user2, category: "vomit")
|
||||
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_reaction_notification(reaction, reaction.reactable.user)
|
||||
end.to change(article.notifications, :count).by(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_to_followers" do
|
||||
context "when the notifiable is an article from a user" do
|
||||
before do
|
||||
user2.follow(user)
|
||||
perform_enqueued_jobs { described_class.send_to_followers(article, "Published") }
|
||||
end
|
||||
|
||||
it "sends a notification to the author's followers" do
|
||||
expect(described_class.first.user_id).to eq user2.id
|
||||
user2.follow(user)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_to_followers(article, "Published")
|
||||
end.to change(user2.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the notifiable is an article from an organization" do
|
||||
let(:article) { create(:article, organization_id: organization.id, user_id: user.id) }
|
||||
|
||||
before do
|
||||
user2.follow(user)
|
||||
user3.follow(organization)
|
||||
perform_enqueued_jobs { described_class.send_to_followers(article, "Published") }
|
||||
end
|
||||
let_it_be(:org_article) { create(:article, organization: organization, user: user) }
|
||||
|
||||
it "sends a notification to author's followers" do
|
||||
expect(user2.notifications.count).to eq 1
|
||||
user2.follow(user)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_to_followers(org_article, "Published")
|
||||
end.to change(user2.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
|
||||
it "sends a notification to the organization's followers" do
|
||||
expect(user3.notifications.count).to eq 1
|
||||
user3.follow(organization)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.send_to_followers(org_article, "Published")
|
||||
end.to change(user3.notifications, :count).by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -395,39 +450,42 @@ RSpec.describe Notification, type: :model do
|
|||
new_title = "hehehe hohoho!"
|
||||
article.update_column(:title, new_title)
|
||||
article.reload
|
||||
|
||||
perform_enqueued_jobs do
|
||||
described_class.update_notifications(article, "Published")
|
||||
first_notification_article_title = described_class.first.json_data["article"]["title"]
|
||||
expect(first_notification_article_title).to eq new_title
|
||||
expected_notification_article_title = user2.notifications.last.json_data["article"]["title"]
|
||||
expect(expected_notification_article_title).to eq(new_title)
|
||||
end
|
||||
end
|
||||
|
||||
it "adds organization data when the article now belongs to an org" do
|
||||
article.update_column(:organization_id, organization.id)
|
||||
article.reload
|
||||
|
||||
perform_enqueued_jobs do
|
||||
described_class.update_notifications(article.reload, "Published")
|
||||
first_notification_organization_id = described_class.first.json_data["organization"]["id"]
|
||||
expect(first_notification_organization_id).to eq organization.id
|
||||
described_class.update_notifications(article, "Published")
|
||||
expected_notification_organization_id = described_class.last.json_data["organization"]["id"]
|
||||
expect(expected_notification_organization_id).to eq(organization.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#aggregated?" do
|
||||
let_it_be(:notification) { build(:notification) }
|
||||
it "returns true if a notification's action is 'Reaction'" do
|
||||
notification = build(:notification, action: "Reaction")
|
||||
expect(notification.aggregated?).to eq true
|
||||
notification.action = "Reaction"
|
||||
expect(notification.aggregated?).to be(true)
|
||||
end
|
||||
|
||||
it "returns true if a notification's action is 'Follow'" do
|
||||
notification = build(:notification, action: "Follow")
|
||||
expect(notification.aggregated?).to eq true
|
||||
notification.action = "Follow"
|
||||
expect(notification.aggregated?).to be(true)
|
||||
end
|
||||
|
||||
it "returns false if a notification's action is not 'Reaction' or 'Follow'" do
|
||||
notification = build(:notification, action: "Published")
|
||||
expect(notification.aggregated?).to eq false
|
||||
notification.action = "Published"
|
||||
expect(notification.aggregated?).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -448,19 +506,14 @@ RSpec.describe Notification, type: :model do
|
|||
end
|
||||
|
||||
describe "#remove_all" do
|
||||
let(:mention) { create(:mention, user_id: user.id, mentionable_id: comment.id, mentionable_type: "Comment") }
|
||||
let(:comment) { create(:comment, user_id: user.id, commentable_id: article.id) }
|
||||
let(:notifiable_collection) { [mention] }
|
||||
|
||||
before do
|
||||
create(:notification, user_id: mention.user_id, notifiable_id: mention.id, notifiable_type: "Mention")
|
||||
end
|
||||
|
||||
it "removes all mention related notifications" do
|
||||
mention = create(:mention, user: user, mentionable: comment)
|
||||
create(:notification, user: mention.user, notifiable: mention)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
expect do
|
||||
described_class.remove_all(notifiable_ids: mention.id, notifiable_type: "Mention")
|
||||
end.to change(described_class, :count).by(-1)
|
||||
end.to change(user.notifications, :count).by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe NotificationSubscription, type: :model do
|
||||
subject { create(:notification_subscription, user: user, notifiable: article) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:article) { create(:article, user: user) }
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:article) { create(:article, user: user) }
|
||||
subject { create(:notification_subscription, user: user, notifiable: article) }
|
||||
|
||||
it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(%i[notifiable_type notifiable_id]) }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Organization, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:organization) { create(:organization) }
|
||||
|
||||
it { is_expected.to have_many(:sponsorships) }
|
||||
|
|
@ -9,22 +8,24 @@ RSpec.describe Organization, type: :model do
|
|||
|
||||
describe "#name" do
|
||||
it "rejects names with over 50 characters" do
|
||||
organization.name = Faker::Lorem.characters(number: 51)
|
||||
organization.name = "x" * 51
|
||||
expect(organization).not_to be_valid
|
||||
end
|
||||
|
||||
it "accepts names with 50 or less characters" do
|
||||
organization.name = "x" * 50
|
||||
expect(organization).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe "#summary" do
|
||||
it "rejects summaries with over 1000 characters" do
|
||||
organization.summary = Faker::Lorem.characters(number: 1001)
|
||||
it "rejects summaries with over 250 characters" do
|
||||
organization.summary = "x" * 251
|
||||
expect(organization).not_to be_valid
|
||||
end
|
||||
|
||||
it "accepts summaries with 1000 or less characters" do
|
||||
it "accepts summaries with 250 or less characters" do
|
||||
organization.summary = "x" * 250
|
||||
expect(organization).to be_valid
|
||||
end
|
||||
end
|
||||
|
|
@ -46,7 +47,7 @@ RSpec.describe Organization, type: :model do
|
|||
end
|
||||
|
||||
it "rejects wrong color format" do
|
||||
organization.text_color_hex = "##{Faker::Lorem.words(number: 4)}"
|
||||
organization.text_color_hex = "#FOOBAR"
|
||||
expect(organization).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
|
@ -128,44 +129,46 @@ RSpec.describe Organization, type: :model do
|
|||
end
|
||||
|
||||
describe "#check_for_slug_change" do
|
||||
def create_article_for_organization
|
||||
user.update(organization_id: organization.id, org_admin: true)
|
||||
create(:article, organization_id: organization.id, user_id: user.id)
|
||||
end
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it "properly updates the slug/username" do
|
||||
random_new_slug = "slug_#{rand(10_000)}"
|
||||
organization.update(slug: random_new_slug)
|
||||
expect(organization.slug).to eq random_new_slug
|
||||
expect(organization.slug).to eq(random_new_slug)
|
||||
end
|
||||
|
||||
it "updates old_slug to original slug if slug was changed" do
|
||||
original_slug = organization.slug
|
||||
organization.update(slug: "slug_#{rand(10_000)}")
|
||||
expect(organization.old_slug).to eq original_slug
|
||||
expect(organization.old_slug).to eq(original_slug)
|
||||
end
|
||||
|
||||
it "updates old_old_slug properly if slug was changed and there was an old_slug" do
|
||||
original_slug = organization.slug
|
||||
organization.update(slug: "something_else")
|
||||
organization.update(slug: "another_slug")
|
||||
expect(organization.old_old_slug).to eq original_slug
|
||||
expect(organization.old_old_slug).to eq(original_slug)
|
||||
end
|
||||
|
||||
it "updates the paths of the organization's articles" do
|
||||
create_article_for_organization
|
||||
new_slug = "slug_#{rand(10_000)}"
|
||||
organization.update(slug: new_slug)
|
||||
article = Article.find_by(organization_id: organization.id)
|
||||
expect(article.path).to include new_slug
|
||||
end
|
||||
context "when dealing with organization articles" do
|
||||
before do
|
||||
create(:organization_membership, user: user, organization: organization, type_of_user: "admin")
|
||||
create(:article, organization: organization, user: user)
|
||||
end
|
||||
|
||||
it "updates article cached_organizations" do
|
||||
create_article_for_organization
|
||||
new_slug = "slug_#{rand(10_000)}"
|
||||
organization.update(slug: new_slug)
|
||||
article = Article.find_by(organization_id: organization.id)
|
||||
expect(article.cached_organization.slug).to eq new_slug
|
||||
it "updates the paths of the organization's articles" do
|
||||
new_slug = "slug_#{rand(10_000)}"
|
||||
organization.update(slug: new_slug)
|
||||
article = Article.find_by(organization_id: organization.id)
|
||||
expect(article.path).to include(new_slug)
|
||||
end
|
||||
|
||||
it "updates article cached_organizations" do
|
||||
new_slug = "slug_#{rand(10_000)}"
|
||||
organization.update(slug: new_slug)
|
||||
article = Article.find_by(organization_id: organization.id)
|
||||
expect(article.cached_organization.slug).to eq(new_slug)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -187,7 +190,7 @@ RSpec.describe Organization, type: :model do
|
|||
|
||||
describe "#pro?" do
|
||||
it "always returns false" do
|
||||
expect(organization.pro?).to be(false)
|
||||
expect(build(:organization).pro?).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,34 +1,14 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Page, type: :model do
|
||||
let(:page) { create(:page) }
|
||||
|
||||
describe "#processed_html" do
|
||||
it "accepts body markdown and turns it into html" do
|
||||
page.body_markdown = "Hello `heyhey`"
|
||||
page.save
|
||||
expect(page.processed_html).to include("<code>")
|
||||
end
|
||||
|
||||
it "accepts body html" do
|
||||
page.body_html = "Hello `heyhey`"
|
||||
page.body_markdown = nil
|
||||
page.save
|
||||
expect(page.processed_html).to eq(page.body_html)
|
||||
end
|
||||
|
||||
describe "#validations" do
|
||||
it "requires either body_markdown or body_html" do
|
||||
page = build(:page)
|
||||
page.body_html = nil
|
||||
page.body_markdown = nil
|
||||
expect(page).not_to be_valid
|
||||
end
|
||||
|
||||
it "triggers cache busting on save" do
|
||||
expect { build(:page).save }.to have_enqueued_job.on_queue("pages_bust_cache")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#validations" do
|
||||
it "takes organization slug into account" do
|
||||
create(:organization, slug: "benandfriends")
|
||||
page = build(:page, slug: "benandfriends")
|
||||
|
|
@ -50,4 +30,27 @@ RSpec.describe Page, type: :model do
|
|||
expect(page.errors[:slug].to_s.include?("taken")).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context "when callbacks are triggered before save" do
|
||||
let(:page) { create(:page) }
|
||||
|
||||
describe "#processed_html" do
|
||||
it "accepts body markdown and turns it into html" do
|
||||
page.update(body_markdown: "Hello `heyhey`")
|
||||
expect(page.processed_html).to include("<code>")
|
||||
end
|
||||
|
||||
it "accepts body html without changing it" do
|
||||
html = "Hello `heyhey`"
|
||||
page.update(body_html: html, body_markdown: "")
|
||||
expect(page.processed_html).to eq(html)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when callbacks are triggered after save" do
|
||||
it "triggers cache busting on save" do
|
||||
expect { build(:page).save }.to have_enqueued_job.on_queue("pages_bust_cache")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe PageView, type: :model do
|
||||
let(:article) { create(:article) }
|
||||
let(:page_view) { create(:page_view, referrer: "http://example.com/page") }
|
||||
|
||||
it { is_expected.to belong_to(:user).optional }
|
||||
it { is_expected.to belong_to(:article) }
|
||||
|
||||
describe "#domain" do
|
||||
it "is automatically set when a new page view is created" do
|
||||
pv = create(:page_view, referrer: "http://example.com/page")
|
||||
expect(pv.reload.domain).to eq("example.com")
|
||||
context "when callbacks are triggered before create" do
|
||||
describe "#domain" do
|
||||
it "is automatically set when a new page view is created" do
|
||||
expect(page_view.domain).to eq("example.com")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#path" do
|
||||
it "is automatically set when a new page view is created" do
|
||||
pv = create(:page_view, referrer: "http://example.com/page")
|
||||
expect(pv.reload.path).to eq("/page")
|
||||
describe "#path" do
|
||||
it "is automatically set when a new page view is created" do
|
||||
expect(page_view.path).to eq("/page")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,55 +27,71 @@ RSpec.describe PodcastEpisode, type: :model do
|
|||
expect(ep2).not_to be_valid
|
||||
expect(ep2.errors[:media_url]).to be_present
|
||||
end
|
||||
|
||||
it "accepts valid podcast episode" do
|
||||
expect(podcast_episode).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe "#available" do
|
||||
let(:podcast) { create(:podcast) }
|
||||
let(:unpodcast) { create(:podcast, published: false) }
|
||||
let!(:episode) { create(:podcast_episode, podcast: podcast) }
|
||||
|
||||
before do
|
||||
create(:podcast_episode, podcast: unpodcast)
|
||||
create(:podcast_episode, podcast: podcast, reachable: false)
|
||||
end
|
||||
|
||||
it "is available when reachable and published" do
|
||||
available_ids = described_class.available.pluck(:id)
|
||||
expect(available_ids).to eq([episode.id])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#description" do
|
||||
it "strips tags from the body" do
|
||||
podcast_episode.body = "<h1>Body with HTML tags</h1>"
|
||||
expect(podcast_episode.description).to eq("Body with HTML tags")
|
||||
ep2 = build(:podcast_episode, guid: podcast_episode.guid)
|
||||
|
||||
ep2.body = "<h1>Body with HTML tags</h1>"
|
||||
expect(ep2.description).to eq("Body with HTML tags")
|
||||
end
|
||||
end
|
||||
|
||||
describe "image cleanup during validation" do
|
||||
it "removes empty paragraphs" do
|
||||
podcast_episode.body = "<p>\r\n<p> </p>\r\n</p>"
|
||||
podcast_episode.validate!
|
||||
expect(podcast_episode.processed_html).to eq("<p></p>")
|
||||
describe "#index_id" do
|
||||
it "is equal to articles-ID" do
|
||||
# NOTE: we shouldn't test private things but cheating a bit for Algolia here
|
||||
expect(podcast_episode.send(:index_id)).to eq("podcast_episodes-#{podcast_episode.id}")
|
||||
end
|
||||
end
|
||||
|
||||
describe ".available" do
|
||||
let_it_be(:podcast) { create(:podcast) }
|
||||
|
||||
it "is available when reachable and published" do
|
||||
expect do
|
||||
create(:podcast_episode, podcast: podcast)
|
||||
end.to change(described_class.available, :count).by(1)
|
||||
end
|
||||
|
||||
it "adds a wrapping paragraph" do
|
||||
podcast_episode.body = "the body"
|
||||
podcast_episode.validate!
|
||||
expect(podcast_episode.processed_html).to eq("<p>the body</p>")
|
||||
it "is not available when unreachable" do
|
||||
expect do
|
||||
create(:podcast_episode, podcast: podcast, reachable: false)
|
||||
end.to change(described_class.available, :count).by(0)
|
||||
end
|
||||
|
||||
it "does not add a wrapping paragraph if already present" do
|
||||
podcast_episode.body = "<p>the body</p>"
|
||||
podcast_episode.validate!
|
||||
expect(podcast_episode.processed_html).to eq("<p>the body</p>")
|
||||
it "is not available when podcast is unpublished" do
|
||||
expect do
|
||||
podcast = create(:podcast, published: false)
|
||||
create(:podcast_episode, podcast: podcast)
|
||||
end.to change(described_class.available, :count).by(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "when callbacks are triggered before validation" do
|
||||
let_it_be(:podcast_episode) { build(:podcast_episode) }
|
||||
|
||||
describe "paragraphs cleanup" do
|
||||
it "removes empty paragraphs" do
|
||||
podcast_episode.body = "<p>\r\n<p> </p>\r\n</p>"
|
||||
podcast_episode.validate!
|
||||
expect(podcast_episode.processed_html).to eq("<p></p>")
|
||||
end
|
||||
|
||||
it "adds a wrapping paragraph" do
|
||||
podcast_episode.body = "the body"
|
||||
podcast_episode.validate!
|
||||
expect(podcast_episode.processed_html).to eq("<p>the body</p>")
|
||||
end
|
||||
|
||||
it "does not add a wrapping paragraph if already present" do
|
||||
podcast_episode.body = "<p>the body</p>"
|
||||
podcast_episode.validate!
|
||||
expect(podcast_episode.processed_html).to eq("<p>the body</p>")
|
||||
end
|
||||
end
|
||||
|
||||
describe "Cloudinary configuration" do
|
||||
describe "Cloudinary configuration and processing" do
|
||||
it "prefixes an image URL with a path" do
|
||||
image_url = "https://dummyimage.com/10x10"
|
||||
podcast_episode.body = "<img src=\"#{image_url}\">"
|
||||
|
|
@ -99,7 +115,9 @@ RSpec.describe PodcastEpisode, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
it "triggers cache busting on save" do
|
||||
expect { build(:podcast_episode).save }.to have_enqueued_job.on_queue("podcast_episodes_bust_cache")
|
||||
context "when callbacks are triggered after save" do
|
||||
it "triggers cache busting on save" do
|
||||
expect { build(:podcast_episode).save }.to have_enqueued_job.on_queue("podcast_episodes_bust_cache")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,23 +1,21 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Podcast, type: :model do
|
||||
let(:podcast) { create(:podcast) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:image) }
|
||||
it { is_expected.to validate_presence_of(:slug) }
|
||||
it { is_expected.to validate_presence_of(:title) }
|
||||
it { is_expected.to validate_presence_of(:main_color_hex) }
|
||||
it { is_expected.to validate_presence_of(:feed_url) }
|
||||
|
||||
describe "validations" do
|
||||
let(:podcast) { create(:podcast) }
|
||||
|
||||
it "is valid" do
|
||||
expect(podcast).to be_valid
|
||||
end
|
||||
|
||||
context "when callbacks are triggered after save" do
|
||||
it "triggers cache busting on save" do
|
||||
expect { podcast.save }.to have_enqueued_job.on_queue("podcasts_bust_cache").twice
|
||||
expect { build(:podcast).save }.to have_enqueued_job.on_queue("podcasts_bust_cache").once
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
# Couldn't use shoulda uniqueness matchers for these tests because:
|
||||
# Shoulda uses `save(validate: false)` which skips validations
|
||||
# So an invalid record is trying to be saved but fails because of the db constraints
|
||||
|
|
@ -65,36 +63,52 @@ RSpec.describe Podcast, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#reachable and #available" do
|
||||
let(:podcast) { create(:podcast, reachable: false, published: true) }
|
||||
let!(:unpodcast) { create(:podcast, reachable: false, published: true) }
|
||||
let!(:unpodcast2) { create(:podcast, reachable: false, published: true) }
|
||||
let!(:cool_podcast) { create(:podcast, reachable: true, published: false) }
|
||||
let!(:reachable_podcast) { create(:podcast, reachable: true, published: true) }
|
||||
|
||||
before do
|
||||
create(:podcast_episode, reachable: true, podcast: podcast)
|
||||
create(:podcast_episode, reachable: false, podcast: unpodcast2)
|
||||
create(:podcast_episode, reachable: true, podcast: cool_podcast)
|
||||
create(:podcast_episode, reachable: true, podcast: reachable_podcast)
|
||||
describe ".reachable" do
|
||||
it "is reachable when it has a reachable episode" do
|
||||
expect do
|
||||
create(:podcast_episode, reachable: true)
|
||||
create(:podcast, published: true)
|
||||
end.to change(described_class.reachable, :count).by(1)
|
||||
end
|
||||
|
||||
it "is reachable when the feed is unreachable but the podcast has reachable podcasts" do
|
||||
reachable_ids = described_class.reachable.pluck(:id)
|
||||
expect(reachable_ids).to include(podcast.id)
|
||||
expect(reachable_ids).to include(cool_podcast.id)
|
||||
expect(reachable_ids).not_to include(unpodcast.id)
|
||||
expect(reachable_ids).not_to include(unpodcast2.id)
|
||||
it "is reachable when it has a reachable episode even if it is unpublished" do
|
||||
expect do
|
||||
create(:podcast_episode, reachable: true)
|
||||
create(:podcast, published: false)
|
||||
end.to change(described_class.reachable, :count).by(1)
|
||||
end
|
||||
|
||||
it "is available only when reachable and published" do
|
||||
available_ids = described_class.available.pluck(:id)
|
||||
expect(available_ids.sort).to eq([podcast.id, reachable_podcast.id].sort)
|
||||
it "is not reachable when it has an unreachable episode" do
|
||||
expect do
|
||||
create(:podcast_episode, reachable: false)
|
||||
create(:podcast, published: true)
|
||||
end.to change(described_class.reachable, :count).by(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".available" do
|
||||
it "is available when it has a reachable episode and it is published" do
|
||||
expect do
|
||||
create(:podcast_episode, reachable: true)
|
||||
create(:podcast, published: true)
|
||||
end.to change(described_class.available, :count).by(1)
|
||||
end
|
||||
|
||||
it "is not available when it has an unreachable episode" do
|
||||
expect do
|
||||
create(:podcast_episode, reachable: false)
|
||||
create(:podcast, published: true)
|
||||
end.to change(described_class.available, :count).by(0)
|
||||
end
|
||||
|
||||
it "is not available when it is not published" do
|
||||
expect do
|
||||
create(:podcast, published: false)
|
||||
end.to change(described_class.available, :count).by(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#existing_episode" do
|
||||
let(:podcast) { create(:podcast) }
|
||||
let(:guid) { "<guid isPermaLink=\"false\">http://podcast.example/file.mp3</guid>" }
|
||||
|
||||
let(:item) do
|
||||
|
|
@ -130,35 +144,24 @@ RSpec.describe Podcast, type: :model do
|
|||
end
|
||||
|
||||
it "doesn't determine existing episode by non-unique website_url" do
|
||||
podcast.update_columns(unique_website_url?: false)
|
||||
podcast.update_attribute(:unique_website_url?, false)
|
||||
create(:podcast_episode, podcast: podcast, website_url: "https://litealloy.ru")
|
||||
expect(podcast.existing_episode(item)).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#admins" do
|
||||
let(:podcast) { create(:podcast) }
|
||||
let(:podcast2) { create(:podcast) }
|
||||
let(:podcast3) { create(:podcast) }
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:user3) { create(:user) }
|
||||
|
||||
before do
|
||||
it "returns podcast admins" do
|
||||
user.add_role(:podcast_admin, podcast)
|
||||
user2.add_role(:podcast_admin, podcast)
|
||||
user.add_role(:podcast_admin, podcast3)
|
||||
user3.add_role(:podcast_admin, podcast2)
|
||||
user3.add_role(:podcast_admin, podcast2)
|
||||
[user, user2, user3].each(&:save)
|
||||
expect(podcast.admins).to include(user)
|
||||
end
|
||||
|
||||
it "returns proper admins" do
|
||||
expect(podcast.admins.sort).to eq([user, user2].sort)
|
||||
end
|
||||
|
||||
it "returns proper admins for podcast3" do
|
||||
expect(podcast3.admins).to eq([user])
|
||||
it "does not return admins for other podcasts" do
|
||||
other_podcast = create(:podcast)
|
||||
user.add_role(:podcast_admin, other_podcast)
|
||||
expect(podcast.admins).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe PollOption, type: :model do
|
||||
let(:article) { create(:article, featured: true) }
|
||||
let(:poll) { create(:poll, article_id: article.id) }
|
||||
let_it_be(:article) { build(:article, featured: true) }
|
||||
let_it_be(:poll) { build(:poll, article: article) }
|
||||
let_it_be(:poll_option) { build(:poll_option, poll: poll) }
|
||||
|
||||
it "allows up to 128 markdown characters" do
|
||||
poll_option = described_class.create(markdown: "0" * 30, poll_id: poll.id)
|
||||
expect(poll_option).to be_valid
|
||||
end
|
||||
describe "validations" do
|
||||
it "allows up to 128 markdown characters" do
|
||||
poll_option.markdown = "0" * 128
|
||||
expect(poll_option).to be_valid
|
||||
end
|
||||
|
||||
it "disallows over 128 markdown characters" do
|
||||
poll_option = described_class.create(markdown: "0" * 200, poll_id: poll.id)
|
||||
expect(poll_option).not_to be_valid
|
||||
it "disallows over 128 markdown characters" do
|
||||
poll_option.markdown = "0" * 129
|
||||
expect(poll_option).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,35 +1,29 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe PollSkip, type: :model do
|
||||
let(:article) { create(:article, featured: true) }
|
||||
let(:user) { create(:user) }
|
||||
let(:poll) { create(:poll, article_id: article.id) }
|
||||
let_it_be(:article) { create(:article, featured: true) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:poll) { create(:poll, article: article) }
|
||||
|
||||
it "is unique across poll and user" do
|
||||
described_class.create(user_id: user.id, poll_id: poll.id)
|
||||
described_class.create(user_id: user.id, poll_id: poll.id)
|
||||
described_class.create(user_id: user.id, poll_id: poll.id)
|
||||
expect(described_class.all.size).to eq(1)
|
||||
second_poll = create(:poll, article_id: article.id)
|
||||
described_class.create(user_id: user.id, poll_id: second_poll.id)
|
||||
expect(described_class.all.size).to eq(2)
|
||||
end
|
||||
describe "validations" do
|
||||
context "when checking against poll" do
|
||||
before do
|
||||
create(:poll_skip, user: user, poll: poll)
|
||||
end
|
||||
|
||||
it "is unique across user and poll votes for the poll" do
|
||||
PollVote.create(user_id: user.id, poll_id: poll.id, poll_option_id: poll.poll_options.last.id)
|
||||
described_class.create(user_id: user.id, poll_id: poll.id)
|
||||
described_class.create(user_id: user.id, poll_id: poll.id)
|
||||
expect(described_class.all.size).to eq(0)
|
||||
second_poll = create(:poll, article_id: article.id)
|
||||
described_class.create(user_id: user.id, poll_id: second_poll.id)
|
||||
expect(described_class.all.size).to eq(1)
|
||||
end
|
||||
it "is unique across poll and user" do
|
||||
expect(build(:poll_skip, user: user, poll: poll)).not_to be_valid
|
||||
end
|
||||
|
||||
it "is prevents a poll vote from being cast" do
|
||||
described_class.create(user_id: user.id, poll_id: poll.id)
|
||||
described_class.create(user_id: user.id, poll_id: poll.id)
|
||||
PollVote.create(user_id: user.id, poll_id: poll.id, poll_option_id: poll.poll_options.last.id)
|
||||
expect(described_class.all.size).to eq(1)
|
||||
expect(PollVote.all.size).to eq(0)
|
||||
it "is valid if it belongs to the same user but to a different poll" do
|
||||
second_poll = create(:poll, article: article)
|
||||
expect(build(:poll_skip, user: user, poll: second_poll)).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
it "is unique across user and poll votes for the poll" do
|
||||
create(:poll_vote, user: user, poll: poll, poll_option: poll.poll_options.last)
|
||||
expect(build(:poll_skip, user: user, poll: poll)).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,17 +1,30 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Poll, type: :model do
|
||||
let(:article) { create(:article, featured: true) }
|
||||
let(:poll) { create(:poll, article_id: article.id) }
|
||||
let_it_be(:article) { create(:article, featured: true) }
|
||||
|
||||
it "limits length of prompt" do
|
||||
long_string = "0" * 200
|
||||
poll.prompt_markdown = long_string
|
||||
expect(poll).not_to be_valid
|
||||
describe "validations" do
|
||||
let_it_be(:poll) { build(:poll, article: article) }
|
||||
|
||||
describe "#prompt_markdown" do
|
||||
it "is valid up to 128 chars" do
|
||||
poll.prompt_markdown = "x" * 128
|
||||
expect(poll).to be_valid
|
||||
end
|
||||
|
||||
it "is not valid with more than 128 chars" do
|
||||
poll.prompt_markdown = "x" * 129
|
||||
expect(poll).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "creates options from input" do
|
||||
poll = create(:poll, article_id: article.id, poll_options_input_array: %w[hello goodbye heyheyhey])
|
||||
expect(poll.poll_options.size).to eq(3)
|
||||
context "when callbacks are triggered after create" do
|
||||
it "creates options from input" do
|
||||
options = %w[hello goodbye heyheyhey]
|
||||
expect do
|
||||
create(:poll, article: article, poll_options_input_array: options)
|
||||
end.to change(PollOption, :count).by(options.size)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,45 +1,44 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe ProfilePin, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:second_user) { create(:user) }
|
||||
let(:article) { create(:article, user_id: user.id) }
|
||||
let(:second_article) { create(:article, user_id: user.id) }
|
||||
let(:third_article) { create(:article, user_id: user.id) }
|
||||
let(:fourth_article) { create(:article, user_id: user.id) }
|
||||
let(:fifth_article) { create(:article, user_id: user.id) }
|
||||
let(:sixth_article) { create(:article, user_id: user.id) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
describe "validations" do
|
||||
it "allows up to five pins per user" do
|
||||
create(:profile_pin, pinnable_id: article.id, profile_id: user.id)
|
||||
create(:profile_pin, pinnable_id: second_article.id, profile_id: user.id)
|
||||
create(:profile_pin, pinnable_id: third_article.id, profile_id: user.id)
|
||||
create(:profile_pin, pinnable_id: fourth_article.id, profile_id: user.id)
|
||||
last_pin = create(:profile_pin, pinnable_id: fifth_article.id, profile_id: user.id)
|
||||
expect(last_pin).to be_valid
|
||||
describe "number of pins" do
|
||||
let_it_be(:articles) { create_list(:article, 4, user: user) }
|
||||
let_it_be(:pins) do
|
||||
articles.each { |article| create(:profile_pin, pinnable_id: article.id, profile_id: user.id) }
|
||||
end
|
||||
|
||||
let(:fifth_article) { create(:article, user: user) }
|
||||
let(:sixth_article) { create(:article, user: user) }
|
||||
|
||||
it "allows up to five pins per user" do
|
||||
pin = build(:profile_pin, pinnable_id: fifth_article.id, profile_id: user.id)
|
||||
expect(pin).to be_valid
|
||||
end
|
||||
|
||||
it "disallows the sixth pin" do
|
||||
create(:profile_pin, pinnable_id: fifth_article.id, profile_id: user.id)
|
||||
expect do
|
||||
create(:profile_pin, pinnable_id: sixth_article.id, profile_id: user.id)
|
||||
end.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
end
|
||||
|
||||
it "disallows the sixth pin" do
|
||||
create(:profile_pin, pinnable_id: article.id, profile_id: user.id)
|
||||
create(:profile_pin, pinnable_id: second_article.id, profile_id: user.id)
|
||||
create(:profile_pin, pinnable_id: third_article.id, profile_id: user.id)
|
||||
create(:profile_pin, pinnable_id: fourth_article.id, profile_id: user.id)
|
||||
create(:profile_pin, pinnable_id: fifth_article.id, profile_id: user.id)
|
||||
expect do
|
||||
create(:profile_pin, pinnable_id: sixth_article.id, profile_id: user.id)
|
||||
end.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
describe "#profile" do
|
||||
let_it_be(:article) { create(:article, user: user) }
|
||||
|
||||
it "ensures pinnable belongs to profile" do
|
||||
pin = build(:profile_pin, pinnable_id: article.id, profile_id: second_user.id)
|
||||
expect(pin).not_to be_valid
|
||||
end
|
||||
it "ensures pinnable belongs to the same profile" do
|
||||
pin = build(:profile_pin, pinnable_id: article.id, profile_id: create(:user).id)
|
||||
expect(pin).not_to be_valid
|
||||
end
|
||||
|
||||
it "ensures one pin per pinnable per profile" do
|
||||
create(:profile_pin, pinnable_id: article.id, profile_id: user.id)
|
||||
last_pin = build(:profile_pin, pinnable_id: article.id, profile_id: user.id)
|
||||
expect(last_pin).not_to be_valid
|
||||
it "ensures one pin per pinnable per profile" do
|
||||
create(:profile_pin, pinnable_id: article.id, profile_id: user.id)
|
||||
other_pin = build(:profile_pin, pinnable_id: article.id, profile_id: user.id)
|
||||
expect(other_pin).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ require "rails_helper"
|
|||
|
||||
RSpec.describe RatingVote, type: :model do
|
||||
let(:user) { create(:user, :trusted) }
|
||||
let(:user2) { create(:user, :trusted) }
|
||||
let(:user3) { create(:user, :trusted) }
|
||||
let(:article) { create(:article, user_id: user.id) }
|
||||
let(:user2) { create(:user, :trusted) }
|
||||
let(:article) { create(:article, user: user) }
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to validate_numericality_of(:rating).is_greater_than(0.0).is_less_than_or_equal_to(10.0) }
|
||||
|
|
@ -28,36 +27,42 @@ RSpec.describe RatingVote, type: :model do
|
|||
it "assigns article rating" do
|
||||
rating = create(:rating_vote, article_id: article.id, user_id: user.id, rating: 2.0)
|
||||
create(:rating_vote, article_id: article.id, user_id: user2.id, rating: 3.0)
|
||||
|
||||
rating.assign_article_rating
|
||||
expect(article.reload.experience_level_rating).to eq(2.5)
|
||||
expect(article.reload.experience_level_rating_distribution).to eq(1.0)
|
||||
article.reload
|
||||
|
||||
expect(article.experience_level_rating).to eq(2.5)
|
||||
expect(article.experience_level_rating_distribution).to eq(1.0)
|
||||
end
|
||||
|
||||
it "assigns article rating with larger distribution" do
|
||||
rating = create(:rating_vote, article_id: article.id, user_id: user.id, rating: 1.0)
|
||||
create(:rating_vote, article_id: article.id, user_id: user2.id, rating: 7.0)
|
||||
|
||||
rating.assign_article_rating
|
||||
expect(article.reload.experience_level_rating).to eq(4.0)
|
||||
expect(article.reload.experience_level_rating_distribution).to eq(6.0)
|
||||
article.reload
|
||||
|
||||
expect(article.experience_level_rating).to eq(4.0)
|
||||
expect(article.experience_level_rating_distribution).to eq(6.0)
|
||||
end
|
||||
end
|
||||
|
||||
describe "permissions" do
|
||||
let_it_be(:untrusted_user) { create(:user) }
|
||||
|
||||
it "allows trusted users to make rating" do
|
||||
rating = build(:rating_vote, article_id: article.id, user_id: user.id)
|
||||
expect(rating).to be_valid
|
||||
end
|
||||
|
||||
it "does not allow non-trusted users to make rating" do
|
||||
nontrusted_user = create(:user)
|
||||
rating = build(:rating_vote, article_id: article.id, user_id: nontrusted_user.id)
|
||||
rating = build(:rating_vote, article_id: article.id, user_id: untrusted_user.id)
|
||||
expect(rating).not_to be_valid
|
||||
end
|
||||
|
||||
it "does allows author to make rating on own post" do
|
||||
nontrusted_user = create(:user)
|
||||
article = create(:article, user_id: nontrusted_user.id)
|
||||
rating = build(:rating_vote, article_id: article.id, user_id: nontrusted_user.id)
|
||||
article = create(:article, user: untrusted_user)
|
||||
rating = build(:rating_vote, article_id: article.id, user_id: untrusted_user.id)
|
||||
expect(rating).to be_valid
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Reaction, type: :model do
|
||||
let(:article) { create(:article, featured: true) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context "when creating and enqueueing" do
|
||||
it "enqueues the Users::TouchJob" do
|
||||
expect do
|
||||
create(:reaction, reactable: article, user: user)
|
||||
end.to have_enqueued_job(Users::TouchJob).exactly(:once).with(user.id)
|
||||
end
|
||||
|
||||
it "enqueues the Reactions::UpdateReactableJob" do
|
||||
expect do
|
||||
create(:reaction, reactable: article, user: user)
|
||||
end.to have_enqueued_job(Reactions::UpdateReactableJob).exactly(:once)
|
||||
end
|
||||
|
||||
it "enqueues the Reactions::BustReactableCacheJob" do
|
||||
expect do
|
||||
create(:reaction, reactable: article, user: user)
|
||||
end.to have_enqueued_job(Reactions::BustReactableCacheJob).exactly(:once)
|
||||
end
|
||||
|
||||
it "enqueues the Reactions::BustHomepageCacheJob" do
|
||||
expect do
|
||||
create(:reaction, reactable: article, user: user)
|
||||
end.to have_enqueued_job(Reactions::BustHomepageCacheJob).exactly(:once)
|
||||
end
|
||||
end
|
||||
|
||||
context "when creating and performing jobs" do
|
||||
it "updates the reactable Comment" do
|
||||
perform_enqueued_jobs do
|
||||
updated_at = 1.day.ago
|
||||
comment = create(:comment, commentable: article, updated_at: updated_at)
|
||||
create(:reaction, reactable: comment, user: user)
|
||||
expect(comment.reload.updated_at).to be > updated_at
|
||||
end
|
||||
end
|
||||
|
||||
it "touches the user" do
|
||||
perform_enqueued_jobs do
|
||||
updated_at = 1.day.ago
|
||||
user.update_columns(updated_at: updated_at)
|
||||
create(:reaction, reactable: article, user: user)
|
||||
expect(user.reload.updated_at).to be > updated_at
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Reaction, type: :model do
|
||||
let(:article) { create(:article, featured: true) }
|
||||
let!(:reaction) { create(:reaction, reactable: article) }
|
||||
|
||||
it "creates a ScoreCalcJob on article reaction destroy" do
|
||||
expect { reaction.destroy }.to have_enqueued_job(Articles::ScoreCalcJob).exactly(:once)
|
||||
end
|
||||
end
|
||||
|
|
@ -2,21 +2,15 @@ require "rails_helper"
|
|||
|
||||
RSpec.describe Reaction, type: :model do
|
||||
let(:user) { create(:user) }
|
||||
let(:article) { create(:article, featured: true) }
|
||||
let(:comment) { create(:comment, user: user, commentable: article) }
|
||||
let(:reaction) { build(:reaction, reactable: comment) }
|
||||
let(:article) { create(:article, user: user) }
|
||||
let(:reaction) { build(:reaction, reactable: article) }
|
||||
|
||||
describe "actual validation" do
|
||||
subject { described_class.new(reactable: article, reactable_type: "Article", user: user) }
|
||||
|
||||
before { user.add_role(:trusted) }
|
||||
describe "builtin validations" do
|
||||
subject { build(:reaction, reactable: article, user: user) }
|
||||
|
||||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to validate_inclusion_of(:category).in_array(Reaction::CATEGORIES) }
|
||||
it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(%i[reactable_id reactable_type category]) }
|
||||
|
||||
# Thumbsdown and Vomits test needed
|
||||
# it { is_expected.to validate_inclusion_of(:reactable_type).in_array(%w(Comment Article)) }
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
|
|
@ -74,37 +68,87 @@ RSpec.describe Reaction, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe "async callbacks" do
|
||||
it "runs async jobs effectively" do
|
||||
u2 = create(:user)
|
||||
c2 = create(:comment, commentable_id: article.id)
|
||||
create(:reaction, user: u2, reactable: c2)
|
||||
create(:reaction, user: u2, reactable: article)
|
||||
expect(reaction).to be_valid
|
||||
describe "#skip_notification_for?" do
|
||||
let_it_be(:receiver) { build(:user) }
|
||||
let_it_be(:reaction) { build(:reaction, reactable: build(:article), user: nil) }
|
||||
|
||||
context "when false" do
|
||||
it "is false when points are positive" do
|
||||
reaction.points = 1
|
||||
expect(reaction.skip_notification_for?(receiver)).to be(false)
|
||||
end
|
||||
|
||||
it "is false when the person who reacted is not the same as the reactable owner" do
|
||||
user_id = User.maximum(:id).to_i + 1
|
||||
reaction.user_id = user_id
|
||||
reaction.reactable.user_id = user_id + 1
|
||||
expect(reaction.skip_notification_for?(user)).to be(false)
|
||||
end
|
||||
|
||||
it "is false when receive_notifications is true" do
|
||||
reaction.reactable.receive_notifications = true
|
||||
expect(reaction.skip_notification_for?(receiver)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "when true" do
|
||||
it "is true when points are negative" do
|
||||
reaction.points = -2
|
||||
expect(reaction.skip_notification_for?(receiver)).to be(true)
|
||||
end
|
||||
|
||||
it "is true when the person who reacted is the same as the reactable owner" do
|
||||
user_id = User.maximum(:id).to_i + 1
|
||||
reaction.user_id = user_id
|
||||
reaction.reactable.user_id = user_id
|
||||
expect(reaction.skip_notification_for?(user)).to be(true)
|
||||
end
|
||||
|
||||
it "is true when the receive_notifications is false" do
|
||||
reaction.reactable.receive_notifications = false
|
||||
expect(reaction.skip_notification_for?(receiver)).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#skip_notification_for?" do
|
||||
let(:receiver) { build(:user) }
|
||||
let(:reaction2) { build(:reaction, reactable: comment, user_id: user.id + 1) }
|
||||
context "when callbacks are called after save" do
|
||||
let!(:reaction) { build(:reaction, category: "like", reactable: article, user: user) }
|
||||
|
||||
it "is false by default" do
|
||||
expect(reaction2.skip_notification_for?(receiver)).to be(false)
|
||||
it "enqueues the correct jobs" do
|
||||
expect do
|
||||
reaction.save
|
||||
end.to(
|
||||
have_enqueued_job(Users::TouchJob).with(user.id).exactly(:once).
|
||||
and(have_enqueued_job(Reactions::UpdateReactableJob).exactly(:once)).
|
||||
and(have_enqueued_job(Reactions::BustReactableCacheJob).exactly(:once)).
|
||||
and(have_enqueued_job(Reactions::BustHomepageCacheJob).exactly(:once)),
|
||||
)
|
||||
end
|
||||
|
||||
it "is true when points are negative" do
|
||||
reaction2.points = -2
|
||||
expect(reaction2.skip_notification_for?(receiver)).to be(true)
|
||||
it "updates updated_at if the reactable is a comment" do
|
||||
perform_enqueued_jobs do
|
||||
updated_at = 1.day.ago
|
||||
comment = create(:comment, commentable: article, updated_at: updated_at)
|
||||
reaction.update(reactable: comment)
|
||||
expect(comment.reload.updated_at).to be > updated_at
|
||||
end
|
||||
end
|
||||
|
||||
it "is true when the receiver is the same user as the one who reacted" do
|
||||
reaction.user = user
|
||||
expect(reaction.skip_notification_for?(user)).to be(true)
|
||||
it "updates updated_at for the user" do
|
||||
perform_enqueued_jobs do
|
||||
updated_at = user.updated_at
|
||||
Timecop.travel(1.day.from_now) do
|
||||
reaction.save
|
||||
expect(user.reload.updated_at).to be > updated_at
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "is true when the receive_notifications is false" do
|
||||
comment.receive_notifications = false
|
||||
expect(reaction.skip_notification_for?(receiver)).to be(true)
|
||||
context "when callbacks are called before destroy" do
|
||||
it "enqueues a ScoreCalcJob on article reaction destroy" do
|
||||
reaction = create(:reaction, reactable: article, user: user)
|
||||
expect { reaction.destroy }.to have_enqueued_job(Articles::ScoreCalcJob).exactly(:once)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,5 +3,16 @@ require "rails_helper"
|
|||
RSpec.describe Role, type: :model do
|
||||
it { is_expected.to belong_to(:resource).optional }
|
||||
it { is_expected.to validate_inclusion_of(:resource_type).in_array(Rolify.resource_types) }
|
||||
it { is_expected.to validate_inclusion_of(:name).in_array(%w[super_admin admin single_resource_admin tech_admin tag_moderator trusted banned warned workshop_pass chatroom_beta_tester comment_banned pro podcast_admin]) }
|
||||
it { is_expected.to validate_inclusion_of(:name).in_array(described_class::ROLES) }
|
||||
|
||||
describe "::ROLES" do
|
||||
it "contains the correct values" do
|
||||
expected_roles = %w[
|
||||
admin banned chatroom_beta_tester comment_banned
|
||||
podcast_admin pro single_resource_admin super_admin
|
||||
tag_moderator tech_admin trusted warned workshop_pass
|
||||
]
|
||||
expect(described_class::ROLES).to eq(expected_roles)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe SearchKeyword, type: :model do
|
||||
let(:search_keyword) { create(:search_keyword) }
|
||||
let(:search_keyword) { build(:search_keyword) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:keyword) }
|
||||
it { is_expected.to validate_presence_of(:google_result_path) }
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ RSpec.describe Sponsorship, type: :model do
|
|||
end
|
||||
|
||||
describe "validations" do
|
||||
let(:org) { create(:organization) }
|
||||
let(:org) { build(:organization) }
|
||||
|
||||
it "forbids an org to have multiple 'expiring' sponsorships" do
|
||||
create(:sponsorship, level: :gold, organization: org)
|
||||
|
|
|
|||
|
|
@ -1,75 +1,57 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe TagAdjustment, type: :model do
|
||||
before do
|
||||
mod_user.add_role(:tag_moderator, tag)
|
||||
admin_user.add_role(:admin)
|
||||
end
|
||||
|
||||
let(:article) { create(:article) }
|
||||
let(:tag) { create(:tag) }
|
||||
let(:admin_user) { create(:user) }
|
||||
let(:mod_user) { create(:user) }
|
||||
let(:regular_user) { create(:user) }
|
||||
let_it_be(:article) { create(:article) }
|
||||
let_it_be(:admin_user) { create(:user, :admin) }
|
||||
let_it_be(:regular_user) { create(:user) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:user_id) }
|
||||
it { is_expected.to validate_presence_of(:article_id) }
|
||||
it { is_expected.to validate_presence_of(:tag_id) }
|
||||
it { is_expected.to validate_presence_of(:tag_name) }
|
||||
it { is_expected.to validate_presence_of(:adjustment_type) }
|
||||
it { is_expected.to validate_inclusion_of(:adjustment_type).in_array(%w[removal addition]) }
|
||||
it { is_expected.to validate_presence_of(:status) }
|
||||
it { is_expected.to have_many(:notifications).dependent(:delete_all) }
|
||||
|
||||
describe "privileges" do
|
||||
it "allows tag mods to create for their tags" do
|
||||
tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id)
|
||||
expect(tag_adjustment).to be_valid
|
||||
end
|
||||
|
||||
it "does not allow tag mods to create for other tags" do
|
||||
another_tag = create(:tag)
|
||||
tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: another_tag.id)
|
||||
expect(tag_adjustment).to be_invalid
|
||||
end
|
||||
|
||||
it "allows admins to create for any tags" do
|
||||
tag_adjustment = build(:tag_adjustment, user_id: admin_user.id, article_id: article.id, tag_id: tag.id)
|
||||
expect(tag_adjustment).to be_valid
|
||||
end
|
||||
|
||||
it "does not allow normal users to create for any tags" do
|
||||
tag_adjustment = build(:tag_adjustment, user_id: regular_user.id, article_id: article.id, tag_id: tag.id)
|
||||
expect(tag_adjustment).to be_invalid
|
||||
end
|
||||
it do
|
||||
# rubocop:disable RSpec/NamedSubject
|
||||
expect(subject).to validate_inclusion_of(:status).in_array(
|
||||
%w[committed pending committed_and_resolvable resolved],
|
||||
)
|
||||
# rubocop:enable RSpec/NamedSubject
|
||||
end
|
||||
|
||||
describe "allowed attribute states" do
|
||||
it "allows proper adjustment_types" do
|
||||
tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id, adjustment_type: "removal")
|
||||
expect(tag_adjustment).to be_valid
|
||||
end
|
||||
it { is_expected.to have_many(:notifications).dependent(:delete_all) }
|
||||
|
||||
it "disallows improper adjustment_types" do
|
||||
tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id, adjustment_type: "slushie")
|
||||
expect(tag_adjustment).to be_invalid
|
||||
end
|
||||
describe "validations" do
|
||||
let(:tag) { create(:tag) }
|
||||
let(:mod_user) { create(:user) }
|
||||
|
||||
it "allows proper status" do
|
||||
tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id, status: "committed")
|
||||
expect(tag_adjustment).to be_valid
|
||||
end
|
||||
describe "privileges" do
|
||||
before do
|
||||
mod_user.add_role(:tag_moderator, tag)
|
||||
end
|
||||
|
||||
it "disallows improper status" do
|
||||
tag_adjustment = build(:tag_adjustment, user_id: mod_user.id, article_id: article.id, tag_id: tag.id, status: "slushiemonkey")
|
||||
expect(tag_adjustment).to be_invalid
|
||||
it "allows tag mods to create for their tags" do
|
||||
tag_adjustment = build(:tag_adjustment, user: mod_user, article: article, tag: tag)
|
||||
expect(tag_adjustment).to be_valid
|
||||
end
|
||||
|
||||
it "does not allow tag mods to create for other tags" do
|
||||
another_tag = create(:tag)
|
||||
tag_adjustment = build(:tag_adjustment, user: mod_user, article: article, tag: another_tag)
|
||||
expect(tag_adjustment).to be_invalid
|
||||
end
|
||||
|
||||
it "allows admins to create for any tags" do
|
||||
tag_adjustment = build(:tag_adjustment, user: admin_user, article: article, tag: tag)
|
||||
expect(tag_adjustment).to be_valid
|
||||
end
|
||||
|
||||
it "does not allow normal users to create for any tags" do
|
||||
tag_adjustment = build(:tag_adjustment, user: regular_user, article: article, tag: tag)
|
||||
expect(tag_adjustment).to be_invalid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# t.integer :user_id
|
||||
# t.integer :article_id
|
||||
# t.integer :tag_id
|
||||
# t.string :tag_name
|
||||
# t.string :adjustment_type
|
||||
# t.string :status
|
||||
# t.string :reason_for_adjustment
|
||||
|
|
|
|||
|
|
@ -1,16 +1,15 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe UserBlock, type: :model do
|
||||
let(:blocker) { create(:user) }
|
||||
let(:blocked) { create(:user) }
|
||||
let(:blocker) { build(:user) }
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to validate_inclusion_of(:config).in_array(%w[default]) }
|
||||
|
||||
it "prevents the blocker from blocking itself" do
|
||||
user_block = described_class.new(blocker_id: 1, blocked_id: 1, config: "default")
|
||||
expect(user_block.valid?).to eq false
|
||||
expect(user_block.errors.full_messages).to include "Blocker can't be the same as the blocked_id"
|
||||
user_block = build(:user_block, blocker: blocker, blocked: blocker, config: "default")
|
||||
expect(user_block).not_to be_valid
|
||||
expect(user_block.errors.full_messages).to include("Blocker can't be the same as the blocked_id")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,15 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Webhook::Event, type: :model do
|
||||
let(:article) { create(:article) }
|
||||
let!(:payload) { Webhook::PayloadAdapter.new(article).hash }
|
||||
let_it_be(:article) { create(:article) }
|
||||
let_it_be(:payload) { Webhook::PayloadAdapter.new(article).hash }
|
||||
|
||||
it "raises an exception with a unknown event type" do
|
||||
expect do
|
||||
described_class.new(event_type: "cool_event")
|
||||
end.to raise_error(Webhook::InvalidEvent)
|
||||
describe "validations" do
|
||||
it "raises an exception with a unknown event type" do
|
||||
expect do
|
||||
described_class.new(event_type: "cool_event")
|
||||
end.to raise_error(Webhook::InvalidEvent)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#as_json" do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue