docbrown/spec/models/organization_spec.rb
Jeremy Friesen d462abb5b5
Ensuring article's cache updates on org image update (#17052)
There are three major things occurring in this pull request:

1. Renaming `Article#update_cached_user` to `Article#set_cached_entities`.
2. Reducing an organization's direct knowledge of which of the org's
   attributes an article caches.
3. Removing duplicate calls to update the article associated with the
   organization.

For renaming to `Article#set_cached_entities`, the prior method implied
we were updating the persistence layer.  However, we were not making any
save nor update calls.  This rename should clarify intention.

For reducing knowledge, the comments for
`Article::ATTRIBUTES_CACHED_FOR_RELATED_ENTITY` should explain the details.

And last, removing the duplicate calls; we had three methods that were
attempting to build and update the `Article#cached_organization`'s
value.

Closes forem/forem#17041
2022-04-01 10:54:55 -04:00

339 lines
13 KiB
Ruby

require "rails_helper"
RSpec.describe Organization, type: :model do
let(:organization) { create(:organization) }
describe "validations" do
describe "builtin validations" do
subject { organization }
it { is_expected.to have_many(:articles).dependent(:nullify) }
it { is_expected.to have_many(:collections).dependent(:nullify) }
it { is_expected.to have_many(:credits).dependent(:restrict_with_error) }
it { is_expected.to have_many(:display_ads).dependent(:destroy) }
it { is_expected.to have_many(:listings).dependent(:destroy) }
it { is_expected.to have_many(:notifications).dependent(:delete_all) }
it { is_expected.to have_many(:organization_memberships).dependent(:delete_all) }
it { is_expected.to have_many(:profile_pins).dependent(:destroy) }
it { is_expected.to have_many(:sponsorships).dependent(:destroy) }
it { is_expected.to have_many(:unspent_credits).class_name("Credit") }
it { is_expected.to have_many(:users).through(:organization_memberships) }
it { is_expected.to validate_length_of(:company_size).is_at_most(7) }
it { is_expected.to validate_length_of(:cta_body_markdown).is_at_most(256) }
it { is_expected.to validate_length_of(:cta_button_text).is_at_most(20) }
it { is_expected.to validate_length_of(:email).is_at_most(64) }
it { is_expected.to validate_length_of(:github_username).is_at_most(50) }
it { is_expected.to validate_length_of(:location).is_at_most(64) }
it { is_expected.to validate_length_of(:name).is_at_most(50) }
it { is_expected.to validate_length_of(:proof).is_at_most(1500) }
it { is_expected.to validate_length_of(:secret).is_equal_to(100) }
it { is_expected.to validate_length_of(:slug).is_at_least(2).is_at_most(18) }
it { is_expected.to validate_length_of(:story).is_at_most(640) }
it { is_expected.to validate_length_of(:tag_line).is_at_most(60) }
it { is_expected.to validate_length_of(:tech_stack).is_at_most(640) }
it { is_expected.to validate_length_of(:twitter_username).is_at_most(15) }
it { is_expected.to validate_length_of(:url).is_at_most(200) }
it { is_expected.to validate_presence_of(:articles_count) }
it { is_expected.to validate_presence_of(:credits_count) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:profile_image) }
it { is_expected.to validate_presence_of(:slug) }
it { is_expected.to validate_presence_of(:spent_credits_count) }
it { is_expected.to validate_presence_of(:summary) }
it { is_expected.to validate_presence_of(:unspent_credits_count) }
it { is_expected.to validate_presence_of(:url) }
it { is_expected.to validate_uniqueness_of(:secret).allow_nil }
it { is_expected.to validate_uniqueness_of(:slug).case_insensitive }
it { is_expected.not_to allow_value("#xyz").for(:bg_color_hex) }
it { is_expected.not_to allow_value("#xyz").for(:text_color_hex) }
it { is_expected.to allow_value("#aabbcc").for(:bg_color_hex) }
it { is_expected.to allow_value("#aabbcc").for(:text_color_hex) }
it { is_expected.to allow_value("#abc").for(:bg_color_hex) }
it { is_expected.to allow_value("#abc").for(:text_color_hex) }
it { is_expected.not_to allow_value("3.0").for(:company_size) }
it { is_expected.to allow_value("3").for(:company_size) }
end
end
context "when callbacks are triggered before save" do
it "generates a secret if set to empty string" do
organization.secret = ""
organization.save
expect(organization.reload.secret).not_to eq("")
end
it "generates a secret if set to nil" do
organization.secret = nil
organization.save
expect(organization.reload.secret).not_to be_nil
end
end
describe "#name" do
it "rejects names with over 50 characters" do
organization.name = "x" * 51
expect(organization).not_to be_valid
end
it "accepts names with 50 or less characters" do
organization.name = "x" * 50
expect(organization).to be_valid
end
end
describe "#summary" do
it "rejects summaries with over 250 characters" do
organization.summary = "x" * 251
expect(organization).not_to be_valid
end
it "accepts summaries with 250 or less characters" do
organization.summary = "x" * 250
expect(organization).to be_valid
end
end
describe "#text_color_hex" do
it "accepts hex color codes" do
organization.text_color_hex = Faker::Color.hex_color
expect(organization).to be_valid
end
it "rejects color names" do
organization.text_color_hex = Faker::Color.color_name
expect(organization).not_to be_valid
end
it "rejects RGB colors" do
organization.text_color_hex = Faker::Color.rgb_color
expect(organization).not_to be_valid
end
it "rejects wrong color format" do
organization.text_color_hex = "#FOOBAR"
expect(organization).not_to be_valid
end
end
describe "#slug" do
it "accepts properly formatted slug" do
organization.slug = "heyho"
expect(organization).to be_valid
end
it "accepts properly formatted slug with numbers" do
organization.slug = "HeyHo2"
expect(organization).to be_valid
end
it "rejects slug with spaces" do
organization.slug = "hey ho"
expect(organization).not_to be_valid
end
it "rejects slug with unacceptable character" do
organization.slug = "Hey&Ho"
expect(organization).not_to be_valid
end
it "downcases slug" do
organization.slug = "HaHaHa"
organization.save
expect(organization.slug).to eq("hahaha")
end
it "rejects reserved slug" do
organization = build(:organization, slug: "settings")
expect(organization).not_to be_valid
expect(organization.errors[:slug].to_s.include?("reserved")).to be true
end
it "takes organization slug into account" do
create(:user, username: "lightalloy")
organization = build(:organization, slug: "lightalloy")
expect(organization).not_to be_valid
expect(organization.errors[:slug].to_s.include?("taken")).to be true
end
it "takes podcast slug into account" do
create(:podcast, slug: "devpodcast")
organization = build(:organization, slug: "devpodcast")
expect(organization).not_to be_valid
expect(organization.errors[:slug].to_s.include?("taken")).to be true
end
it "takes page slug into account" do
create(:page, slug: "needed_info_for_site")
organization = build(:organization, slug: "needed_info_for_site")
expect(organization).not_to be_valid
expect(organization.errors[:slug].to_s.include?("taken")).to be true
end
it "takes sitemap into account" do
organization = build(:organization, slug: "sitemap-yo")
expect(organization).not_to be_valid
expect(organization.errors[:slug].to_s.include?("taken")).to be true
end
context "when callbacks are triggered after save" do
let(:organization) { build(:organization) }
before do
allow(Organizations::BustCacheWorker).to receive(:perform_async)
end
it "triggers cache busting on save" do
organization.save
expect(Organizations::BustCacheWorker).to have_received(:perform_async).with(organization.id, organization.slug)
end
end
end
describe "#url" do
it "accepts valid http url" do
organization.url = "http://ben.com"
expect(organization).to be_valid
end
it "accepts valid secure http url" do
organization.url = "https://ben.com"
expect(organization).to be_valid
end
it "rejects invalid http url" do
organization.url = "ben.com"
expect(organization).not_to be_valid
end
end
describe "#check_for_slug_change" do
let(:user) { create(:user) }
it "properly updates the slug/username" do
random_new_slug = "slug_#{rand(10_000)}"
organization.update(slug: random_new_slug)
expect(organization.slug).to eq(random_new_slug)
end
it "updates old_slug to original slug if slug was changed" do
original_slug = organization.slug
organization.update(slug: "slug_#{rand(10_000)}")
expect(organization.old_slug).to eq(original_slug)
end
it "updates old_old_slug properly if slug was changed and there was an old_slug" do
original_slug = organization.slug
organization.update(slug: "something_else")
organization.update(slug: "another_slug")
expect(organization.old_old_slug).to eq(original_slug)
end
context "when dealing with organization articles" do
before do
create(:organization_membership, user: user, organization: organization, type_of_user: "admin")
article = create(:article, organization: organization, user: user)
organization.articles << article
organization.save
end
it "updates the paths of the organization's articles" do
new_slug = "slug_#{rand(10_000)}"
sidekiq_perform_enqueued_jobs(only: Organizations::UpdateOrganizationArticlesPathsWorker) do
organization.update(slug: new_slug)
end
article = Article.find_by(organization_id: organization.id)
expect(article.path).to include(new_slug)
end
it "updates article's cached_organization profile image", :aggregate_failures do
# What is going on with this spec? Follow the comments.
#
# tl;dr - verifying fix for https://github.com/forem/forem/issues/17041
# Create an organization and article, verify the article has cached the initial profile
# image of the organization.
original_org = create(:organization, profile_image: File.open(Rails.root.join("app/assets/images/1.png")))
article = create(:article, organization: original_org)
expect(article.cached_organization.profile_image_url).to eq(original_org.profile_image_url)
# For good measure let's have two of these articles
create(:article, organization: original_org)
# A foible of CarrierWave is that I can't re-upload for the same model in memory. I need to
# refind the record. #reload does not work either. (I lost sleep on this portion of the
# test)
organization = described_class.find(original_org.id)
# Verify that the organization's image changes. See the above CarrierWave foible
expect do
organization.profile_image = File.open(Rails.root.join("app/assets/images/2.png"))
organization.save!
end.to change { organization.reload.profile_image_url }
# I want to collect reloaded versions of the organization's articles so I can see their
# cached organization profile image
articles_profile_image_urls = organization.articles
.map { |art| art.reload.cached_organization.profile_image_url }.uniq
# Because of the change `{ organization.reload... }` the organization's profile_image_url is
# the most recent change.
expect(articles_profile_image_urls).to eq([organization.profile_image_url])
end
it "updates article cached_organizations" do
new_slug = "slug_#{rand(10_000)}"
sidekiq_perform_enqueued_jobs(only: Organizations::UpdateOrganizationArticlesPathsWorker) do
organization.update(slug: new_slug)
end
article = Article.find_by(organization_id: organization.id)
expect(article.cached_organization.slug).to eq(new_slug)
end
# these tests rely on `Organization.update_articles_cached_organization` callback,
# which will eventually invoke the trigger
# rubocop:disable RSpec/NestedGroups
context "when callbacks eventually invoke the trigger on Article.reading_list_document" do
it "updates the articles .reading_list_document when updating the name" do
article = Article.find_by(organization_id: organization.id)
old_reading_list_document = article.reading_list_document
organization.update(name: "ACME Org")
expect(article.reload.reading_list_document).not_to eq(old_reading_list_document)
end
it "does not update the articles .reading_list_document when updating the company_size" do
article = Article.find_by(organization_id: organization.id)
old_reading_list_document = article.reading_list_document
organization.update(company_size: "200")
expect(article.reload.reading_list_document).to eq(old_reading_list_document)
end
end
# rubocop:enable RSpec/NestedGroups
end
end
describe "#enough_credits?" do
it "returns false if the user has less unspent credits than neeed" do
expect(organization.enough_credits?(1)).to be(false)
end
it "returns true if the user has the exact amount of unspent credits" do
create(:credit, organization: organization, spent: false)
expect(organization.enough_credits?(1)).to be(true)
end
it "returns true if the user has more unspent credits than needed" do
create_list(:credit, 2, organization: organization, spent: false)
expect(organization.enough_credits?(1)).to be(true)
end
end
end