diff --git a/app/models/chat_channel.rb b/app/models/chat_channel.rb index d52e56d71..9a2be086c 100644 --- a/app/models/chat_channel.rb +++ b/app/models/chat_channel.rb @@ -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 diff --git a/app/models/comment.rb b/app/models/comment.rb index 85f5573dc..917c94dc6 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -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 diff --git a/app/models/display_ad.rb b/app/models/display_ad.rb index 0e36bdf07..7ba594a2d 100644 --- a/app/models/display_ad.rb +++ b/app/models/display_ad.rb @@ -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 diff --git a/app/models/job_opportunity.rb b/app/models/job_opportunity.rb index 52bdfd4ab..4e9044475 100644 --- a/app/models/job_opportunity.rb +++ b/app/models/job_opportunity.rb @@ -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 diff --git a/app/models/mention.rb b/app/models/mention.rb index 0905deceb..013322c30 100644 --- a/app/models/mention.rb +++ b/app/models/mention.rb @@ -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 diff --git a/app/models/notification.rb b/app/models/notification.rb index 18e121b70..5c93072ea 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -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 diff --git a/app/models/page.rb b/app/models/page.rb index a51cb68e4..74ea373fe 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -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 diff --git a/app/models/podcast.rb b/app/models/podcast.rb index 1b0784311..1699209fa 100644 --- a/app/models/podcast.rb +++ b/app/models/podcast.rb @@ -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 diff --git a/app/models/podcast_episode.rb b/app/models/podcast_episode.rb index cf8e5d673..6050a4008 100644 --- a/app/models/podcast_episode.rb +++ b/app/models/podcast_episode.rb @@ -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. diff --git a/app/models/poll_skip.rb b/app/models/poll_skip.rb index ed2894cd1..6eefafe99 100644 --- a/app/models/poll_skip.rb +++ b/app/models/poll_skip.rb @@ -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 diff --git a/app/models/rating_vote.rb b/app/models/rating_vote.rb index d9d89e8d2..7469719cd 100644 --- a/app/models/rating_vote.rb +++ b/app/models/rating_vote.rb @@ -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 diff --git a/app/models/reaction.rb b/app/models/reaction.rb index 8d5bbb542..4e85f4c22 100644 --- a/app/models/reaction.rb +++ b/app/models/reaction.rb @@ -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 diff --git a/app/models/role.rb b/app/models/role.rb index 20d667f68..d187a5349 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -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 diff --git a/app/models/tag_adjustment.rb b/app/models/tag_adjustment.rb index f028563a2..bb99fea21 100644 --- a/app/models/tag_adjustment.rb +++ b/app/models/tag_adjustment.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 5d1e01306..09d4aa838 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/models/user_block.rb b/app/models/user_block.rb index 1ff2fc830..7c0ef09f9 100644 --- a/app/models/user_block.rb +++ b/app/models/user_block.rb @@ -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 diff --git a/spec/decorators/user_decorator_spec.rb b/spec/decorators/user_decorator_spec.rb new file mode 100644 index 000000000..0cea03c55 --- /dev/null +++ b/spec/decorators/user_decorator_spec.rb @@ -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 diff --git a/spec/factories/poll_skips.rb b/spec/factories/poll_skips.rb index 17b242431..48d359ba2 100644 --- a/spec/factories/poll_skips.rb +++ b/spec/factories/poll_skips.rb @@ -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 diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb index 7f82700f1..3c4dceea1 100644 --- a/spec/models/article_spec.rb +++ b/spec/models/article_spec.rb @@ -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 diff --git a/spec/models/audit_log_spec.rb b/spec/models/audit_log_spec.rb index 622ac3017..077642696 100644 --- a/spec/models/audit_log_spec.rb +++ b/spec/models/audit_log_spec.rb @@ -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 diff --git a/spec/models/badge_achievement_spec.rb b/spec/models/badge_achievement_spec.rb index c23d6520d..6e3c76db8 100644 --- a/spec/models/badge_achievement_spec.rb +++ b/spec/models/badge_achievement_spec.rb @@ -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("") end it "awards credits after create" do - achievement = create(:badge_achievement) expect(achievement.user.credits.size).to eq(5) end end diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb index be76bd2fc..dad1e6364 100644 --- a/spec/models/badge_spec.rb +++ b/spec/models/badge_spec.rb @@ -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) } diff --git a/spec/models/block_spec.rb b/spec/models/block_spec.rb index 04d2607fc..999b5a760 100644 --- a/spec/models/block_spec.rb +++ b/spec/models/block_spec.rb @@ -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 diff --git a/spec/models/buffer_update_spec.rb b/spec/models/buffer_update_spec.rb index 4dc017a02..763493322 100644 --- a/spec/models/buffer_update_spec.rb +++ b/spec/models/buffer_update_spec.rb @@ -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") diff --git a/spec/models/chat_channel_spec.rb b/spec/models/chat_channel_spec.rb index e9e9226e8..e836d74fb 100644 --- a/spec/models/chat_channel_spec.rb +++ b/spec/models/chat_channel_spec.rb @@ -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 diff --git a/spec/models/classified_listing_spec.rb b/spec/models/classified_listing_spec.rb index 51f3991e1..3a2ba9949 100644 --- a/spec/models/classified_listing_spec.rb +++ b/spec/models/classified_listing_spec.rb @@ -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) } diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index 934f2e998..b8cb33c56 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -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 diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index fd26e5e39..249076389 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -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?("


")
- 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("")
+ 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
diff --git a/spec/models/page_view_spec.rb b/spec/models/page_view_spec.rb
index 87a5e6507..28cebadc9 100644
--- a/spec/models/page_view_spec.rb
+++ b/spec/models/page_view_spec.rb
@@ -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
diff --git a/spec/models/podcast_episode_spec.rb b/spec/models/podcast_episode_spec.rb
index 0ec78c52d..1af8f024a 100644
--- a/spec/models/podcast_episode_spec.rb
+++ b/spec/models/podcast_episode_spec.rb
@@ -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 = "Body with HTML tags
"
- expect(podcast_episode.description).to eq("Body with HTML tags")
+ ep2 = build(:podcast_episode, guid: podcast_episode.guid)
+
+ ep2.body = "Body with HTML tags
"
+ 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 = "\r\n
\r\n"
- podcast_episode.validate!
- expect(podcast_episode.processed_html).to eq("")
+ 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("the body
")
+ 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 = "the body
"
- podcast_episode.validate!
- expect(podcast_episode.processed_html).to eq("the body
")
+ 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 = "\r\n
\r\n"
+ podcast_episode.validate!
+ expect(podcast_episode.processed_html).to eq("")
+ end
+
+ it "adds a wrapping paragraph" do
+ podcast_episode.body = "the body"
+ podcast_episode.validate!
+ expect(podcast_episode.processed_html).to eq("the body
")
+ end
+
+ it "does not add a wrapping paragraph if already present" do
+ podcast_episode.body = "the body
"
+ podcast_episode.validate!
+ expect(podcast_episode.processed_html).to eq("the body
")
+ 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 = "
"
@@ -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
diff --git a/spec/models/podcast_spec.rb b/spec/models/podcast_spec.rb
index c387996ae..b05961fa1 100644
--- a/spec/models/podcast_spec.rb
+++ b/spec/models/podcast_spec.rb
@@ -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) { "http://podcast.example/file.mp3 " }
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
diff --git a/spec/models/poll_option_spec.rb b/spec/models/poll_option_spec.rb
index 3dfb738ee..20ed5438f 100644
--- a/spec/models/poll_option_spec.rb
+++ b/spec/models/poll_option_spec.rb
@@ -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
diff --git a/spec/models/poll_skip_spec.rb b/spec/models/poll_skip_spec.rb
index 533a07c5c..8f55ac095 100644
--- a/spec/models/poll_skip_spec.rb
+++ b/spec/models/poll_skip_spec.rb
@@ -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
diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb
index 656d04d8c..e951a3aa0 100644
--- a/spec/models/poll_spec.rb
+++ b/spec/models/poll_spec.rb
@@ -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
diff --git a/spec/models/profile_pin_spec.rb b/spec/models/profile_pin_spec.rb
index 95a1f3663..c3504c87d 100644
--- a/spec/models/profile_pin_spec.rb
+++ b/spec/models/profile_pin_spec.rb
@@ -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
diff --git a/spec/models/rating_vote_spec.rb b/spec/models/rating_vote_spec.rb
index ec08b2cc8..b671ac075 100644
--- a/spec/models/rating_vote_spec.rb
+++ b/spec/models/rating_vote_spec.rb
@@ -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
diff --git a/spec/models/reaction_create_spec.rb b/spec/models/reaction_create_spec.rb
deleted file mode 100644
index 5440139a1..000000000
--- a/spec/models/reaction_create_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/models/reaction_destroy_spec.rb b/spec/models/reaction_destroy_spec.rb
deleted file mode 100644
index d31a30237..000000000
--- a/spec/models/reaction_destroy_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/models/reaction_spec.rb b/spec/models/reaction_spec.rb
index 71936d836..a27f0b5bf 100644
--- a/spec/models/reaction_spec.rb
+++ b/spec/models/reaction_spec.rb
@@ -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
diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb
index 09cc6a3d8..abbcb1efc 100644
--- a/spec/models/role_spec.rb
+++ b/spec/models/role_spec.rb
@@ -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
diff --git a/spec/models/search_keyword_spec.rb b/spec/models/search_keyword_spec.rb
index ee06297e6..951e91dc1 100644
--- a/spec/models/search_keyword_spec.rb
+++ b/spec/models/search_keyword_spec.rb
@@ -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) }
diff --git a/spec/models/sponsorship_spec.rb b/spec/models/sponsorship_spec.rb
index d53bee685..4f7e148de 100644
--- a/spec/models/sponsorship_spec.rb
+++ b/spec/models/sponsorship_spec.rb
@@ -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)
diff --git a/spec/models/tag_adjustment_spec.rb b/spec/models/tag_adjustment_spec.rb
index 14d59efd8..296534921 100644
--- a/spec/models/tag_adjustment_spec.rb
+++ b/spec/models/tag_adjustment_spec.rb
@@ -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
diff --git a/spec/models/user_block_spec.rb b/spec/models/user_block_spec.rb
index 08dd6f2ff..7fafb87ac 100644
--- a/spec/models/user_block_spec.rb
+++ b/spec/models/user_block_spec.rb
@@ -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
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 04d9b3e11..054c8d0db 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1,45 +1,66 @@
require "rails_helper"
+def user_from_authorization_service(service_name, signed_in_resource, cta_variant)
+ auth = OmniAuth.config.mock_auth[service_name]
+ service = AuthorizationService.new(auth, signed_in_resource, cta_variant)
+ service.get_user
+end
+
RSpec.describe User, type: :model do
- let!(:user) { create(:user) }
- let(:returning_user) { create(:user, signup_cta_variant: nil) }
- let(:second_user) { create(:user) }
- let(:article) { create(:article, user_id: user.id) }
- let(:tag) { create(:tag) }
- let(:org) { create(:organization) }
- let(:second_org) { create(:organization) }
+ let(:user) { create(:user) }
+ let(:other_user) { create(:user) }
+ let(:org) { create(:organization) }
before { mock_auth_hash }
describe "validations" do
- it { is_expected.to have_many(:api_secrets) }
- it { is_expected.to have_many(:articles) }
- it { is_expected.to have_many(:badge_achievements).dependent(:destroy) }
- it { is_expected.to have_many(:badges).through(:badge_achievements) }
- it { is_expected.to have_many(:collections).dependent(:destroy) }
- it { is_expected.to have_many(:comments) }
- it { is_expected.to have_many(:email_messages).class_name("Ahoy::Message") }
- it { is_expected.to have_many(:identities).dependent(:destroy) }
- it { is_expected.to have_many(:mentions).dependent(:destroy) }
- it { is_expected.to have_many(:notes) }
- it { is_expected.to have_many(:notifications).dependent(:destroy) }
- it { is_expected.to have_many(:reactions).dependent(:destroy) }
- it { is_expected.to have_many(:tweets).dependent(:destroy) }
- it { is_expected.to have_many(:github_repos).dependent(:destroy) }
- it { is_expected.to have_many(:chat_channel_memberships).dependent(:destroy) }
- it { is_expected.to have_many(:chat_channels).through(:chat_channel_memberships) }
- it { is_expected.to have_many(:push_notification_subscriptions).dependent(:destroy) }
- it { is_expected.to have_many(:notification_subscriptions).dependent(:destroy) }
- it { is_expected.to have_one(:pro_membership).dependent(:destroy) }
- it { is_expected.to validate_uniqueness_of(:username).case_insensitive }
- it { is_expected.to validate_uniqueness_of(:github_username).allow_nil }
- it { is_expected.to validate_uniqueness_of(:twitter_username).allow_nil }
- it { is_expected.to validate_presence_of(:username) }
- it { is_expected.to validate_length_of(:username).is_at_most(30).is_at_least(2) }
- it { is_expected.to validate_length_of(:name).is_at_most(100) }
- it { is_expected.to validate_inclusion_of(:inbox_type).in_array(%w[open private]) }
- it { is_expected.to have_many(:access_grants).class_name("Doorkeeper::AccessGrant").with_foreign_key("resource_owner_id").dependent(:delete_all) }
- it { is_expected.to have_many(:access_tokens).class_name("Doorkeeper::AccessToken").with_foreign_key("resource_owner_id").dependent(:delete_all) }
+ describe "builtin validations" do
+ it { is_expected.to have_many(:api_secrets) }
+ it { is_expected.to have_many(:articles) }
+ it { is_expected.to have_many(:badge_achievements).dependent(:destroy) }
+ it { is_expected.to have_many(:badges).through(:badge_achievements) }
+ it { is_expected.to have_many(:collections).dependent(:destroy) }
+ it { is_expected.to have_many(:comments) }
+ it { is_expected.to have_many(:email_messages).class_name("Ahoy::Message") }
+ it { is_expected.to have_many(:identities).dependent(:destroy) }
+ it { is_expected.to have_many(:mentions).dependent(:destroy) }
+ it { is_expected.to have_many(:notes) }
+ it { is_expected.to have_many(:notifications).dependent(:destroy) }
+ it { is_expected.to have_many(:reactions).dependent(:destroy) }
+ it { is_expected.to have_many(:tweets).dependent(:destroy) }
+ it { is_expected.to have_many(:github_repos).dependent(:destroy) }
+ it { is_expected.to have_many(:chat_channel_memberships).dependent(:destroy) }
+ it { is_expected.to have_many(:chat_channels).through(:chat_channel_memberships) }
+ it { is_expected.to have_many(:push_notification_subscriptions).dependent(:destroy) }
+ it { is_expected.to have_many(:notification_subscriptions).dependent(:destroy) }
+ it { is_expected.to have_one(:pro_membership).dependent(:destroy) }
+
+ # rubocop:disable RSpec/NamedSubject
+ it do
+ expect(subject).to have_many(:access_grants).
+ class_name("Doorkeeper::AccessGrant").
+ with_foreign_key("resource_owner_id").
+ dependent(:delete_all)
+ end
+
+ it do
+ expect(subject).to have_many(:access_tokens).
+ class_name("Doorkeeper::AccessToken").
+ with_foreign_key("resource_owner_id").
+ dependent(:delete_all)
+ end
+ # rubocop:enable RSpec/NamedSubject
+
+ it { is_expected.to have_many(:organization_memberships).dependent(:destroy) }
+
+ it { is_expected.to validate_uniqueness_of(:username).case_insensitive }
+ it { is_expected.to validate_uniqueness_of(:github_username).allow_nil }
+ it { is_expected.to validate_uniqueness_of(:twitter_username).allow_nil }
+ it { is_expected.to validate_presence_of(:username) }
+ it { is_expected.to validate_length_of(:username).is_at_most(30).is_at_least(2) }
+ it { is_expected.to validate_length_of(:name).is_at_most(100).is_at_least(1) }
+ it { is_expected.to validate_inclusion_of(:inbox_type).in_array(%w[open private]) }
+ end
it "validates username against reserved words" do
user = build(:user, username: "readinglist")
@@ -69,306 +90,445 @@ RSpec.describe User, type: :model do
end
end
- # the followings are failing
- # it { is_expected.to have_many(:keys) }
- # it { is_expected.to have_many(:job_applications) }
- # it { is_expected.to have_many(:answers) }
- # it { is_expected.to validate_uniqueness_of(:email).case_insensitive.allow_blank }
+ context "when callbacks are triggered before validation" do
+ let(:user) { build(:user) }
- def user_from_authorization_service(service_name, signed_in_resource, cta_variant)
- auth = OmniAuth.config.mock_auth[service_name]
- service = AuthorizationService.new(auth, signed_in_resource, cta_variant)
- service.get_user
- end
+ describe "#twitter_username" do
+ it "sets twitter username to nil if empty" do
+ user.twitter_username = ""
+ user.validate!
+ expect(user.twitter_username).to eq(nil)
+ end
- describe "makes sure usernames and email are not blank" do
- it "sets twitter username to nil" do
- user = create(:user, twitter_username: "")
- user.reload
- expect(user.twitter_username).to eq(nil)
+ it "does not change a valid name" do
+ user.twitter_username = "hello"
+ user.validate!
+ expect(user.twitter_username).to eq("hello")
+ end
end
- it "sets github username to nil" do
- user = create(:user, github_username: "")
- user.reload
- expect(user.github_username).to eq(nil)
+ describe "#github_username" do
+ it "sets github username to nil if empty" do
+ user.github_username = ""
+ user.validate!
+ expect(user.github_username).to eq(nil)
+ end
+
+ it "does not change a valid name" do
+ user.github_username = "hello"
+ user.validate!
+ expect(user.github_username).to eq("hello")
+ end
end
- it "sets correct usernames if they are not blank" do
- user = create(:user, github_username: "hello", twitter_username: "world")
- user.reload
- expect(user.github_username).to eq("hello")
- expect(user.twitter_username).to eq("world")
+ describe "#email" do
+ it "sets email to nil if empty" do
+ user.email = ""
+ user.validate!
+ expect(user.email).to eq(nil)
+ end
+
+ it "does not change a valid name" do
+ user.email = "anna@example.com"
+ user.validate!
+ expect(user.email).to eq("anna@example.com")
+ end
end
- it "sets email to nil" do
- user = create(:user, email: "")
- user.reload
- expect(user.email).to eq(nil)
+ describe "#username" do
+ it "receives a temporary username if none is given" do
+ user.username = ""
+ user.validate!
+ expect(user.username).not_to be_blank
+ end
+
+ it "does not allow to change to a username that is taken" do
+ user.username = other_user.username
+ expect(user).not_to be_valid
+ end
+
+ it "does not allow to change to a username that is taken by an organization" do
+ user.username = create(:organization).slug
+ expect(user).not_to be_valid
+ end
end
- it "sets correct email if it's not blank" do
- user = create(:user, email: "anna@example.com")
- user.reload
- expect(user.email).to eq("anna@example.com")
+ describe "#website_url" do
+ it "does not accept invalid website url" do
+ user.website_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+
+ it "accepts valid http website url" do
+ user.website_url = "http://ben.com"
+ expect(user).to be_valid
+ end
end
- it "sets onboarding_variant_version" do
- user = create(:user, email: "anna@example.com")
- user.reload
- expect(user.onboarding_variant_version).to be_in(%w[0 1 2 3 4 5 6 7 8 9])
+ describe "#mastodon_url" do
+ it "accepts valid https mastodon url" do
+ user.mastodon_url = "https://mastodon.social/@test"
+ expect(user).to be_valid
+ end
+
+ it "does not accept a denied mastodon instance" do
+ user.mastodon_url = "https://SpammyMcSpamface.com/"
+ expect(user).not_to be_valid
+ end
+
+ it "does not accept invalid mastodon url" do
+ user.mastodon_url = "mastodon.social/@test"
+ expect(user).not_to be_valid
+ end
+ end
+
+ describe "#facebook_url" do
+ it "accepts valid https facebook url", :aggregate_failures do
+ %w[thepracticaldev thepracticaldev/ the.practical.dev].each do |username|
+ user.facebook_url = "https://facebook.com/#{username}"
+ expect(user).to be_valid
+ end
+ end
+
+ it "does not accept invalid facebook url" do
+ user.facebook_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+ end
+
+ describe "#behance_url" do
+ it "accepts valid https behance url", :aggregate_failures do
+ %w[jess jess/ je-ss jes_ss].each do |username|
+ user.behance_url = "https://behance.net/#{username}"
+ expect(user).to be_valid
+ end
+ end
+
+ it "does not accept invalid behance url" do
+ user.behance_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+ end
+
+ describe "#twitch_url" do
+ it "does not accept invalid twitch url" do
+ user.twitch_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+
+ it "accepts valid https twitch url", :aggregate_failures do
+ %w[pandyzhao pandyzhao/ PandyZhao_ pandy_Zhao].each do |username|
+ user.twitch_url = "https://twitch.tv/#{username}"
+ expect(user).to be_valid
+ end
+ end
+ end
+
+ describe "#stackoverflow_url" do
+ it "accepts valid https stackoverflow url", :aggregate_failures do
+ %w[pandyzhao pandyzhao/ pandy-zhao].each do |username|
+ user.stackoverflow_url = "https://stackoverflow.com/users/7381391/#{username}"
+ expect(user).to be_valid
+ end
+ end
+
+ it "does not accept invalid stackoverflow url" do
+ user.stackoverflow_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+
+ it "accepts valid stackoverflow sub community url", :aggregate_failures do
+ %w[pt ru es ja].each do |subcommunity|
+ user.stackoverflow_url = "https://#{subcommunity}.stackoverflow.com/users/7381391/mazen"
+ expect(user).to be_valid
+ end
+ end
+
+ it "does not accept invalid stackoverflow sub community url" do
+ user.stackoverflow_url = "https://fr.stackoverflow.com/users/7381391/mazen"
+ expect(user).not_to be_valid
+ end
+ end
+
+ describe "#linkedin_url" do
+ it "accepts valid https linkedin url", :aggregate_failures do
+ %w[jessleenyc jessleenyc/ jess-lee-nyc].each do |username|
+ user.linkedin_url = "https://linkedin.com/in/#{username}"
+ expect(user).to be_valid
+ end
+ end
+
+ it "accepts valid country specific https linkedin url" do
+ user.linkedin_url = "https://mx.linkedin.com/in/jessleenyc"
+ expect(user).to be_valid
+ end
+
+ it "does not accept three letters country codes in http linkedin url" do
+ user.linkedin_url = "http://mex.linkedin.com/in/jessleenyc"
+ expect(user).not_to be_valid
+ end
+
+ it "does not accept three letters country codes in https linkedin url" do
+ user.linkedin_url = "https://mex.linkedin.com/in/jessleenyc"
+ expect(user).not_to be_valid
+ end
+
+ it "does not accept invalid linkedin url" do
+ user.linkedin_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+ end
+
+ describe "#dribbble_url", :aggregate_failures do
+ it "accepts valid https dribbble url" do
+ %w[jess jess/ je-ss je_ss].each do |username|
+ user.dribbble_url = "https://dribbble.com/#{username}"
+ expect(user).to be_valid
+ end
+ end
+
+ it "does not accept invalid dribbble url" do
+ user.dribbble_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+ end
+
+ describe "#medium_url" do
+ it "accepts valid https medium url", :aggregate_failures do
+ %w[jess jess/ je-ss je_ss].each do |username|
+ user.medium_url = "https://medium.com/#{username}"
+ expect(user).to be_valid
+ end
+ end
+
+ it "does not accept invalid medium url" do
+ user.medium_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+ end
+
+ describe "#instagram_url" do
+ it "does not accept invalid instagram url" do
+ user.instagram_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+
+ it "accepts valid instagram url", :aggregate_failures do
+ %w[jess je_ss je_ss.tt A.z.E.r.T.y].each do |username|
+ user.instagram_url = "https://instagram.com/#{username}"
+ expect(user).to be_valid
+ end
+ end
+ end
+
+ describe "#gitlab_url" do
+ it "accepts valid https gitlab url", :aggregate_failures do
+ %w[jess jess/ je-ss je_ss].each do |username|
+ user.gitlab_url = "https://gitlab.com/#{username}"
+ expect(user).to be_valid
+ end
+ end
+
+ it "does not accept invalid gitlab url" do
+ user.gitlab_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+ end
+
+ describe "#employer_url" do
+ it "does not accept invalid employer url" do
+ user.employer_url = "ben.com"
+ expect(user).not_to be_valid
+ end
+
+ it "does accept valid http employer url" do
+ user.employer_url = "http://ben.com"
+ expect(user).to be_valid
+ end
+
+ it "does accept valid https employer url" do
+ user.employer_url = "https://ben.com"
+ expect(user).to be_valid
+ end
+ end
+
+ describe "#config_theme" do
+ it "accepts valid theme" do
+ user.config_theme = "night theme"
+ expect(user).to be_valid
+ end
+
+ it "does not accept invalid theme" do
+ user.config_theme = "no night mode"
+ expect(user).not_to be_valid
+ end
+ end
+
+ describe "#config_font" do
+ it "accepts valid font" do
+ user.config_font = "sans serif"
+ expect(user).to be_valid
+ end
+
+ it "does not accept invalid font" do
+ user.config_font = "goobledigook"
+ expect(user).not_to be_valid
+ end
+ end
+
+ describe "#config_navbar" do
+ it "accepts valid navbar" do
+ user.config_navbar = "static"
+ expect(user).to be_valid
+ end
+
+ it "does not accept invalid navbar" do
+ user.config_navbar = "not valid navbar input"
+ expect(user).not_to be_valid
+ end
+ end
+
+ context "when the past value is relevant" do
+ let(:user) { create(:user) }
+
+ it "changes old_username and old_old_username properly if username changes" do
+ old_username = user.username
+ random_new_username = "username_#{rand(100_000_000)}"
+ user.update(username: random_new_username)
+ expect(user.username).to eq(random_new_username)
+ expect(user.old_username).to eq(old_username)
+
+ new_username = user.username
+ user.update(username: "username_#{rand(100_000_000)}")
+ expect(user.old_username).to eq(new_username)
+ expect(user.old_old_username).to eq(old_username)
+ end
+
+ it "enforces summary length validation if previous summary was valid" do
+ user.summary = "0" * 999
+ user.save(validate: false)
+ user.summary = "0" * 999
+ expect(user).to be_valid
+ end
+
+ it "does not enforce summary validation if previous summary was invalid" do
+ user = build(:user, summary: "0" * 999)
+ expect(user).not_to be_valid
+ end
end
end
- describe "validations" do
- it "gets a username after create" do
- expect(user.username).not_to eq(nil)
- end
+ context "when callbacks are triggered before and after create" do
+ let_it_be(:user) { create(:user, email: nil) }
- it "does not accept invalid website url" do
- user.website_url = "ben.com"
- expect(user).not_to be_valid
- end
+ describe "#language_settings" do
+ it "sets correct language_settings by default" do
+ expect(user.language_settings).to eq("preferred_languages" => %w[en])
+ end
- it "accepts valid https mastodon url" do
- user.mastodon_url = "https://mastodon.social/@test"
- expect(user).to be_valid
- end
-
- it "does not accept a denied mastodon instance" do
- user.mastodon_url = "https://SpammyMcSpamface.com/"
- expect(user).not_to be_valid
- end
-
- it "does not accept invalid mastodon url" do
- user.mastodon_url = "mastodon.social/@test"
- expect(user).not_to be_valid
- end
-
- it "accepts valid http website url" do
- user.website_url = "http://ben.com"
- expect(user).to be_valid
- end
-
- it "accepts valid https website url" do
- user.website_url = "https://ben.com"
- expect(user).to be_valid
- end
-
- it "accepts valid https facebook url" do
- %w[thepracticaldev thepracticaldev/ the.practical.dev].each do |username|
- user.facebook_url = "https://facebook.com/#{username}"
- expect(user).to be_valid
+ it "sets correct language_settings by default after the jobs are processed" do
+ perform_enqueued_jobs do
+ expect(user.language_settings).to eq("preferred_languages" => %w[en])
+ end
end
end
- it "does not accept invalid facebook url" do
- user.facebook_url = "ben.com"
- expect(user).not_to be_valid
- end
+ describe "#estimated_default_language" do
+ it "estimates default language to be nil" do
+ perform_enqueued_jobs do
+ expect(user.estimated_default_language).to be(nil)
+ end
+ end
- it "accepts valid https behance url" do
- %w[jess jess/ je-ss jes_ss].each do |username|
- user.behance_url = "https://behance.net/#{username}"
- expect(user).to be_valid
+ it "estimates default language to be japanese with .jp email" do
+ perform_enqueued_jobs do
+ user = create(:user, email: "ben@hello.jp")
+ expect(user.reload.estimated_default_language).to eq("ja")
+ end
+ end
+
+ it "estimates default language based on ID dump" do
+ perform_enqueued_jobs do
+ new_user = user_from_authorization_service(:twitter, nil, "navbar_basic")
+ expect(new_user.estimated_default_language).to eq(nil)
+ end
end
end
- it "does not accept invalid behance url" do
- user.behance_url = "ben.com"
- expect(user).not_to be_valid
- end
-
- it "does not accept invalid twitch url" do
- user.twitch_url = "ben.com"
- expect(user).not_to be_valid
- end
-
- it "accepts valid https twitch url" do
- %w[pandyzhao pandyzhao/ PandyZhao_ pandy_Zhao].each do |username|
- user.twitch_url = "https://twitch.tv/#{username}"
- expect(user).to be_valid
+ describe "#preferred_languages_array" do
+ it "returns proper preferred_languages_array" do
+ perform_enqueued_jobs do
+ user = create(:user, email: "ben@hello.jp")
+ expect(user.reload.preferred_languages_array).to eq(%w[en ja])
+ end
end
- end
- it "accepts valid https stackoverflow url" do
- %w[pandyzhao pandyzhao/ pandy-zhao].each do |username|
- user.stackoverflow_url = "https://stackoverflow.com/users/7381391/#{username}"
- expect(user).to be_valid
+ it "returns a correct array when language settings are in a new format" do
+ language_settings = { estimated_default_language: "en", preferred_languages: %w[en ru it] }
+ user = build(:user, language_settings: language_settings)
+ expect(user.preferred_languages_array).to eq(%w[en ru it])
end
- end
- it "does not accept invalid stackoverflow url" do
- user.stackoverflow_url = "ben.com"
- expect(user).not_to be_valid
- end
-
- it "accepts valid stackoverflow sub community url" do
- %w[pt ru es ja].each do |subcommunity|
- user.stackoverflow_url = "https://#{subcommunity}.stackoverflow.com/users/7381391/mazen"
- expect(user).to be_valid
+ it "returns a correct array when language settings are in the old format" do
+ language_settings = {
+ estimated_default_language: "en",
+ prefer_language_en: true,
+ prefer_language_ja: false,
+ prefer_language_es: true
+ }
+ user = build(:user, language_settings: language_settings)
+ expect(user.preferred_languages_array).to eq(%w[en es])
end
end
-
- it "does not accept invalid stackoverflow sub community url" do
- user.stackoverflow_url = "https://fr.stackoverflow.com/users/7381391/mazen"
- expect(user).not_to be_valid
- end
-
- it "accepts valid https linkedin url" do
- %w[jessleenyc jessleenyc/ jess-lee-nyc].each do |username|
- user.linkedin_url = "https://linkedin.com/in/#{username}"
- expect(user).to be_valid
- end
- end
-
- it "accepts valid country specific https linkedin url" do
- user.linkedin_url = "https://mx.linkedin.com/in/jessleenyc"
- expect(user).to be_valid
- end
-
- it "does not accept three letters country codes in http linkedin url" do
- user.linkedin_url = "http://mex.linkedin.com/in/jessleenyc"
- expect(user).not_to be_valid
- end
-
- it "does not accept three letters country codes in https linkedin url" do
- user.linkedin_url = "https://mex.linkedin.com/in/jessleenyc"
- expect(user).not_to be_valid
- end
-
- it "does not accept invalid linkedin url" do
- user.linkedin_url = "ben.com"
- expect(user).not_to be_valid
- end
-
- it "accepts valid https dribbble url" do
- %w[jess jess/ je-ss je_ss].each do |username|
- user.dribbble_url = "https://dribbble.com/#{username}"
- expect(user).to be_valid
- end
- end
-
- it "does not accept invalid dribbble url" do
- user.dribbble_url = "ben.com"
- expect(user).not_to be_valid
- end
-
- it "accepts valid https medium url" do
- %w[jess jess/ je-ss je_ss].each do |username|
- user.medium_url = "https://medium.com/#{username}"
- expect(user).to be_valid
- end
- end
-
- it "does not accept invalid medium url" do
- user.medium_url = "ben.com"
- expect(user).not_to be_valid
- end
-
- it "does not accept invalid instagram url" do
- user.instagram_url = "ben.com"
- expect(user).not_to be_valid
- end
-
- it "accepts valid instagram url" do
- %w[jess je_ss je_ss.tt A.z.E.r.T.y].each do |username|
- user.instagram_url = "https://instagram.com/#{username}"
- expect(user).to be_valid
- end
- end
-
- it "accepts valid https gitlab url" do
- %w[jess jess/ je-ss je_ss].each do |username|
- user.gitlab_url = "https://gitlab.com/#{username}"
- expect(user).to be_valid
- end
- end
-
- it "does not accept invalid gitlab url" do
- user.gitlab_url = "ben.com"
- expect(user).not_to be_valid
- end
-
- it "changes old_username and old_old_username properly if username changes" do
- old_username = user.username
- random_new_username = "username_#{rand(100_000_000)}"
- user.update(username: random_new_username)
- expect(user.username).to eq(random_new_username)
- expect(user.old_username).to eq(old_username)
- new_username = user.username
- user.update(username: "username_#{rand(100_000_000)}")
- expect(user.old_username).to eq(new_username)
- expect(user.old_old_username).to eq(old_username)
- end
-
- it "does not allow too short or too long name" do
- user.name = ""
- expect(user).not_to be_valid
- user.name = Faker::Lorem.paragraph_by_chars(number: 200)
- expect(user).not_to be_valid
- end
-
- it "does not accept invalid employer url" do
- user.employer_url = "ben.com"
- expect(user).not_to be_valid
- end
-
- it "does accept valid http employer url" do
- user.employer_url = "http://ben.com"
- expect(user).to be_valid
- end
-
- it "does accept valid https employer url" do
- user.employer_url = "https://ben.com"
- expect(user).to be_valid
- end
-
- it "enforces summary length validation if old summary was valid" do
- user.summary = "0" * 999
- user.save(validate: false)
- user.summary = "0" * 999
- expect(user).to be_valid
- end
-
- it "does not inforce summary validation if old summary was invalid" do
- user.summary = "0" * 999
- expect(user).not_to be_valid
- end
-
- it "accepts valid theme" do
- user.config_theme = "night theme"
- expect(user).to be_valid
- end
-
- it "does not accept invalid theme" do
- user.config_theme = "no night mode"
- expect(user).not_to be_valid
- end
-
- it "accepts valid font" do
- user.config_font = "sans serif"
- expect(user).to be_valid
- end
-
- it "does not accept invalid font" do
- user.config_theme = "goobledigook"
- expect(user).not_to be_valid
- end
-
- it "accepts valid navbar" do
- user.config_navbar = "static"
- expect(user).to be_valid
- end
-
- it "does not accept invalid navbar" do
- user.config_navbar = "not valid navbar input"
- expect(user).not_to be_valid
- end
end
- ## Registration
+ context "when callbacks are triggered after save" do
+ describe "subscribing to mailchip newsletter" do
+ it "enqueues SubscribeToMailchimpNewsletterJob" do
+ expect do
+ user.save
+ end.to have_enqueued_job(Users::SubscribeToMailchimpNewsletterJob).exactly(:once).with(user.id)
+ end
+
+ it "does not enqueue without an email" do
+ expect do
+ user.update(email: "")
+ end.not_to have_enqueued_job(Users::SubscribeToMailchimpNewsletterJob).exactly(:once).with(user.id)
+ end
+
+ it "does not enqueue with an invalid email" do
+ expect do
+ user.update(email: "foobar")
+ end.not_to have_enqueued_job(Users::SubscribeToMailchimpNewsletterJob).exactly(:once).with(user.id)
+ end
+
+ it "does not enqueue with an unconfirmed email" do
+ expect do
+ user.update(unconfirmed_email: "bob@bob.com", confirmation_sent_at: Time.current)
+ end.not_to have_enqueued_job(Users::SubscribeToMailchimpNewsletterJob).exactly(:once).with(user.id)
+ end
+ end
+ end
+
+ context "when indexing and deindexing" do
+ it "triggers background auto-indexing when user is saved" do
+ expect { user.save }.to have_enqueued_job.with(user, "index!").on_queue("algoliasearch")
+ end
+
+ it "doesn't enqueue a job on destroy" do
+ user = build(:user)
+
+ perform_enqueued_jobs do
+ user.save
+ end
+
+ expect { user.destroy }.not_to have_enqueued_job.on_queue("algoliasearch")
+ end
+ end
+
describe "user registration" do
+ let(:user) { create(:user) }
+
it "finds user by email and assigns identity to that if exists" do
OmniAuth.config.mock_auth[:twitter].info.email = user.email
@@ -385,7 +545,6 @@ RSpec.describe User, type: :model do
end
it "assigns random username if username is taken by organization on registration" do
- org = create(:organization)
OmniAuth.config.mock_auth[:twitter].info.nickname = org.slug
new_user = user_from_authorization_service(:twitter, nil, "navbar_basic")
@@ -399,6 +558,7 @@ RSpec.describe User, type: :model do
end
it "does not assign signup_cta_variant to non-new users" do
+ returning_user = build(:user, signup_cta_variant: nil)
new_user = user_from_authorization_service(:twitter, returning_user, "hey-hey-hey")
expect(new_user.signup_cta_variant).to eq(nil)
end
@@ -426,285 +586,110 @@ RSpec.describe User, type: :model do
expect(new_user.identities.size).to eq(2)
end
- context "when estimating the default language" do
- it "sets correct language_settings by default" do
- user2 = create(:user, email: nil)
- expect(user2.language_settings).to eq("preferred_languages" => %w[en])
- end
+ it "persists JSON dump of identity data" do
+ new_user = user_from_authorization_service(:twitter, nil, "navbar_basic")
+ identity = new_user.identities.first
+ expect(identity.auth_data_dump.provider).to eq(identity.provider)
+ end
- it "sets correct language_settings by default after the callbacks" do
- perform_enqueued_jobs do
- user2 = create(:user, email: nil)
- expect(user2.language_settings).to eq("preferred_languages" => %w[en])
- end
- end
+ it "persists extracts relevant identity data from new twitter user" do
+ new_user = user_from_authorization_service(:twitter, nil, "navbar_basic")
+ expect(new_user.twitter_followers_count).to eq(100)
+ expect(new_user.twitter_created_at).to be_kind_of(ActiveSupport::TimeWithZone)
+ end
- it "estimates default language to be nil" do
- perform_enqueued_jobs do
- user.estimate_default_language
- end
- expect(user.reload.estimated_default_language).to eq(nil)
- end
-
- it "estimates default language to be japan with jp email" do
- perform_enqueued_jobs do
- user.update_column(:email, "ben@hello.jp")
- user.estimate_default_language
- end
- expect(user.reload.estimated_default_language).to eq("ja")
- end
-
- it "estimates default language based on ID dump" do
- perform_enqueued_jobs do
- new_user = user_from_authorization_service(:twitter, nil, "navbar_basic")
- new_user.estimate_default_language
- expect(user.reload.estimated_default_language).to eq(nil)
- end
- end
-
- it "returns proper preferred_languages_array" do
- perform_enqueued_jobs do
- user.update_column(:email, "ben@hello.jp")
- user.estimate_default_language
- end
- expect(user.reload.preferred_languages_array).to include("ja")
- end
+ it "persists extracts relevant identity data from new github user" do
+ new_user = user_from_authorization_service(:github, nil, "navbar_basic")
+ expect(new_user.github_created_at).to be_kind_of(ActiveSupport::TimeWithZone)
end
end
- describe "#preferred_languages_array" do
- it "returns a correct array when language settings are in a new format" do
- user.update_columns(language_settings: { estimated_default_language: "en", preferred_languages: %w[en ru it] })
- expect(user.preferred_languages_array).to eq(%w[en ru it])
+ describe "#follow and #all_follows" do
+ it "follows users" do
+ expect do
+ user.follow(create(:user))
+ end.to change(user.all_follows, :size).by(1)
end
-
- it "returns a correct array when language settings are in the old format" do
- user.update_columns(language_settings: { estimated_default_language: "en", prefer_language_en: true, prefer_language_ja: false, prefer_language_es: true })
- expect(user.preferred_languages_array).to eq(%w[en es])
- end
- end
-
- it "follows users" do
- user2 = create(:user)
- user3 = create(:user)
- user.follow(user2)
- user.follow(user3)
- expect(user.all_follows.size).to eq(2)
end
describe "#moderator_for_tags" do
- let(:tag1) { create(:tag) }
- let(:tag2) { create(:tag) }
- let(:tag3) { create(:tag) }
+ let(:tags) { create_list(:tag, 2) }
it "lists tags user moderates" do
- user.add_role(:tag_moderator, tag1)
- user.add_role(:tag_moderator, tag2)
- expect(user.moderator_for_tags).to include(tag1.name)
- expect(user.moderator_for_tags).to include(tag2.name)
- expect(user.moderator_for_tags).not_to include(tag3.name)
+ user.add_role(:tag_moderator, tags.first)
+
+ expect(user.moderator_for_tags).to include(tags.first.name)
+ expect(user.moderator_for_tags).not_to include(tags.last.name)
end
it "returns empty array if no tags moderated" do
- expect(user.moderator_for_tags).to eq([])
+ expect(user.moderator_for_tags).to be_empty
end
end
describe "#followed_articles" do
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
+ let_it_be(:another_user) { create(:user) }
+ let_it_be(:articles) { create_list(:article, 2, user: another_user) }
before do
- create_list(:article, 3, user_id: user2.id)
- user.follow(user2)
+ user.follow(another_user)
end
it "returns all articles following" do
- expect(user.followed_articles.size).to eq(3)
+ expect(user.followed_articles.size).to eq(articles.size)
end
it "returns segment of articles if limit is passed" do
- expect(user.followed_articles.limit(2).size).to eq(2)
+ expect(user.followed_articles.limit(1).size).to eq(articles.size - 1)
end
end
- describe "#cached_followed_tags" do
- let(:tag1) { create(:tag) }
- let(:tag2) { create(:tag) }
- let(:tag3) { create(:tag) }
- let(:tag4) { create(:tag) }
+ describe "#calculate_score" do
+ it "calculates a score" do
+ create(:article, featured: true, user: user)
- it "returns empty if no tags followed" do
- expect(user.decorate.cached_followed_tags.size).to eq(0)
+ user.calculate_score
+ expect(user.score).to be_positive
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
- user.follow(tag1)
- Follow.last.update(points: 0.1)
- expect(user.decorate.cached_followed_tags.first.points).to eq(0.1)
- end
- end
-
- describe "theming properties" 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 "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
-
- 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 "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
-
- 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
- end
-
- it "does not allow to change to username that is taken" do
- user.username = second_user.username
- expect(user).not_to be_valid
- end
-
- it "does not allow to change to username that is taken by an organization" do
- user.username = create(:organization).slug
- expect(user).not_to be_valid
- end
-
- it "indexes into Algolia search" do
- user.index!
- end
-
- it "calculates score" do
- article.featured = true
- article.save
- user.calculate_score
- expect(user.score).to be > 0
- end
-
- it "persists JSON dump of identity data" do
- new_user = user_from_authorization_service(:twitter, nil, "navbar_basic")
- identity = new_user.identities.first
- expect(identity.auth_data_dump.provider).to eq(identity.provider)
- end
-
- it "persists extracts relevant identity data from logged in user" do
- new_user = user_from_authorization_service(:twitter, nil, "navbar_basic")
- expect(new_user.twitter_following_count).to be_an(Integer)
- expect(new_user.twitter_followers_count).to eq(100)
- expect(new_user.twitter_created_at).to be_kind_of(ActiveSupport::TimeWithZone)
- new_user = user_from_authorization_service(:github, nil, "navbar_basic")
- expect(new_user.github_created_at).to be_kind_of(ActiveSupport::TimeWithZone)
end
describe "cache counts" do
it "has an accurate tag follow count" do
+ tag = create(:tag)
user.follow(tag)
- expect(user.reload.following_tags_count).to eq 1
+ expect(user.reload.following_tags_count).to eq(1)
end
it "has an accurate user follow count" do
- user.follow(second_user)
- expect(user.reload.following_users_count).to eq 1
+ user.follow(other_user)
+ expect(user.reload.following_users_count).to eq(1)
end
it "has an accurate organization follow count" do
user.follow(org)
- expect(user.reload.following_orgs_count).to eq 1
+ expect(user.reload.following_orgs_count).to eq(1)
end
end
- describe "organization admin privileges" do
+ describe "#org_admin?" do
it "recognizes an org admin" do
create(:organization_membership, user: user, organization: org, type_of_user: "admin")
- expect(user.org_admin?(org)).to be true
+ expect(user.org_admin?(org)).to be(true)
end
it "forbids an incorrect org admin" do
+ other_org = create(:organization)
create(:organization_membership, user: user, organization: org, type_of_user: "admin")
- expect(user.org_admin?(second_org)).to be false
- expect(second_user.org_admin?(org)).to be false
+ expect(user.org_admin?(other_org)).to be(false)
+ expect(other_user.org_admin?(org)).to be(false)
end
- it "responds to nil" do
- expect(user.org_admin?(nil)).to be false
- expect(second_user.org_admin?(nil)).to be false
- end
- end
-
- describe "#destroy" do
- it "successfully destroys a user" do
- user.destroy
- expect(user.persisted?).to be false
- end
-
- it "destroys associated organization memberships" do
- organization_membership = create(:organization_membership, user_id: user.id, organization_id: org.id)
- user.destroy
- expect { organization_membership.reload }.to raise_error ActiveRecord::RecordNotFound
+ it "returns false if nil is given" do
+ expect(user.org_admin?(nil)).to be(false)
end
end
describe "#pro?" do
- let(:user) { create(:user) }
-
it "returns false if the user is not a pro" do
expect(user.pro?).to be(false)
end
@@ -715,7 +700,7 @@ RSpec.describe User, type: :model do
end
it "returns true if the user has an active pro membership" do
- create(:pro_membership, user: user)
+ user.pro_membership = build(:pro_membership, status: "active")
expect(user.pro?).to be(true)
end
@@ -728,16 +713,6 @@ RSpec.describe User, type: :model do
end
end
- describe "when agolia auto-indexing/removal is triggered" do
- it "process background auto-indexing when user is saved" do
- expect { user.save }.to have_enqueued_job.with(user, "index!").on_queue("algoliasearch")
- end
-
- it "doesn't schedule a job on destroy" do
- expect { user.destroy }.not_to have_enqueued_job.on_queue("algoliasearch")
- end
- end
-
describe "#has_enough_credits?" do
it "returns false if the user has less unspent credits than neeed" do
expect(user.has_enough_credits?(1)).to be(false)
@@ -753,14 +728,4 @@ RSpec.describe User, type: :model do
expect(user.has_enough_credits?(1)).to be(true)
end
end
-
- describe "#subscribe_to_mailchimp_newsletter" do
- let(:user2) { create :user }
-
- it "schedules the job" do
- expect do
- user2.subscribe_to_mailchimp_newsletter
- end.to have_enqueued_job(Users::SubscribeToMailchimpNewsletterJob).exactly(:once).with(user2.id)
- end
- end
end
diff --git a/spec/models/webhook/event_spec.rb b/spec/models/webhook/event_spec.rb
index 9e4d05244..08215f14c 100644
--- a/spec/models/webhook/event_spec.rb
+++ b/spec/models/webhook/event_spec.rb
@@ -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