require "rails_helper" RSpec.describe Article, type: :model do def build_and_validate_article(*args) article = build(:article, *args) article.validate! article end let(:user) { create(:user) } let!(:article) { create(:article, user: user) } include_examples "#sync_reactions_count", :article it_behaves_like "UserSubscriptionSourceable" describe "validations" do it { is_expected.to belong_to(:collection).optional } it { is_expected.to belong_to(:organization).optional } it { is_expected.to belong_to(:user) } it { is_expected.to have_one(:discussion_lock).dependent(:delete) } it { is_expected.to have_many(:comments).dependent(:nullify) } it { is_expected.to have_many(:mentions).dependent(:delete_all) } it { is_expected.to have_many(:html_variant_successes).dependent(:nullify) } it { is_expected.to have_many(:html_variant_trials).dependent(:nullify) } it { is_expected.to have_many(:notification_subscriptions).dependent(:delete_all) } it { is_expected.to have_many(:notifications).dependent(:delete_all) } it { is_expected.to have_many(:page_views).dependent(:delete_all) } it { is_expected.to have_many(:polls).dependent(:destroy) } it { is_expected.to have_many(:profile_pins).dependent(:delete_all) } it { is_expected.to have_many(:rating_votes).dependent(:destroy) } it { is_expected.to have_many(:sourced_subscribers) } it { is_expected.to have_many(:reactions).dependent(:destroy) } it { is_expected.to have_many(:tags) } it { is_expected.to have_many(:user_subscriptions).dependent(:nullify) } it { is_expected.to validate_length_of(:body_markdown).is_at_least(0) } it { is_expected.to validate_length_of(:cached_tag_list).is_at_most(126) } it { is_expected.to validate_length_of(:title).is_at_most(128) } it { is_expected.to validate_presence_of(:comments_count) } it { is_expected.to validate_presence_of(:positive_reactions_count) } it { is_expected.to validate_presence_of(:previous_public_reactions_count) } it { is_expected.to validate_presence_of(:public_reactions_count) } it { is_expected.to validate_presence_of(:rating_votes_count) } it { is_expected.to validate_presence_of(:reactions_count) } it { is_expected.to validate_presence_of(:user_subscriptions_count) } it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_presence_of(:user_id) } it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:user_id) } it { is_expected.not_to allow_value("foo").for(:main_image_background_hex_color) } describe "::admin_published_with" do it "includes mascot-published articles" do allow(Settings::General).to receive(:mascot_user_id).and_return(3) user = create(:user, id: 3) create(:article, user: user, tags: "challenge") expect(described_class.admin_published_with("challenge").count).to eq(1) end it "includes staff-user-published articles" do allow(Settings::Community).to receive(:staff_user_id).and_return(3) user = create(:user, id: 3) create(:article, user: user, tags: "challenge") expect(described_class.admin_published_with("challenge").count).to eq(1) end it "includes admin published articles" do user = create(:user, :admin) create(:article, user: user, tags: "challenge") expect(described_class.admin_published_with("challenge").count).to eq(1) end it "does not include regular user published articles" do user = create(:user) create(:article, user: user, tags: "challenge") expect(described_class.admin_published_with("challenge").count).to eq(0) end end describe "#body_markdown" do it "is unique scoped for user_id and title", :aggregate_failures do art2 = build(:article, body_markdown: article.body_markdown, user: article.user, title: article.title) expect(art2).not_to be_valid expect(art2.errors_as_sentence).to match("markdown has already been taken") end # using https://unicode-table.com/en/11A15/ multibyte char it "is valid if its bytesize is less than 800 kilobytes" do article.body_markdown = "𑨕" * 204_800 # 4 bytes x 204800 = 800 kilobytes expect(article).to be_valid end it "is not valid if its bytesize exceeds 800 kilobytes" do article.body_markdown = "𑨕" * 204_801 expect(article).not_to be_valid expect(article.errors_as_sentence).to match("too long") end end describe "#validate co_authors" do it "is invalid if the co_author is the same as the author" do article.co_author_ids = [user.id] expect(article).not_to be_valid end it "is invalid if there are duplicate co_authors for the same article" do co_author1 = create(:user) article.co_author_ids = [co_author1, co_author1] expect(article).not_to be_valid end it "is invalid if the co_author is entered as a text value rather than an integer" do article.co_author_ids = [user.id, "abc"] expect(article).not_to be_valid end it "is invalid if the co_author ID is not greater than 0" do article.co_author_ids = [user.id, 0] expect(article).not_to be_valid end it "is valid if co_author_ids is nil" do article.co_author_ids = nil expect(article).to be_valid end end context "when published" do before do # rubocop:disable RSpec/NamedSubject allow(subject).to receive(:published?).and_return(true) # rubocop:disable RSpec/SubjectStub # rubocop:enable RSpec/NamedSubject end it { is_expected.to validate_presence_of(:slug) } end describe "#search_id" do it "returns article_ID" do expect(article.search_id).to eq("article_#{article.id}") end end describe "#main_image_background_hex_color" do it "must have true hex for image background" do article.main_image_background_hex_color = "hello" expect(article.valid?).to eq(false) article.main_image_background_hex_color = "#fff000" expect(article.valid?).to eq(true) end end describe "#canonical_url_must_not_have_spaces" do let!(:article) { build :article, user: user } it "is valid without spaces" do valid_url = "https://www.positronx.io/angular-radio-buttons-example/" article.canonical_url = valid_url expect(article).to be_valid end it "is not valid with spaces" do invalid_url = "https://www.positronx.io/angular radio-buttons-example/" article.canonical_url = invalid_url message = "must not have spaces" expect(article).not_to be_valid expect(article.errors.messages[:canonical_url]).to include(message) end end describe "#main_image" do it "must have url for main image if present" do article.main_image = "hello" expect(article.valid?).to eq(false) article.main_image = "https://image.com/image.png" expect(article.valid?).to eq(true) end end describe "dates" do it "reject future dates" do expect(build(:article, with_date: true, date: Date.tomorrow).valid?).to be(false) end it "reject future dates even when it's published at" do article.published_at = Date.tomorrow expect(article.valid?).to be(false) end end describe "polls" do let!(:poll) { create(:poll, article: article) } it "does not allow the use of admin-only liquid tags for non-admins" do article.body_markdown = "hello hey hey hey {% poll #{poll.id} %}" expect(article.valid?).to eq(false) end it "allows admins" do article.user.add_role(:admin) article.body_markdown = "hello hey hey hey {% poll #{poll.id} %}" expect(article.valid?).to eq(true) end end describe "liquid tags" do xit "is not valid if it contains invalid liquid tags" do body = "{% github /thepracticaldev/dev.to %}" article = build(:article, body_markdown: body) expect(article).not_to be_valid expect(article.errors[:base].first).to match(/Invalid GitHub/) end it "is valid with valid liquid tags", :vcr do VCR.use_cassette("twitter_client_status_extended") do article = build_and_validate_article(with_tweet_tag: true) expect(article).to be_valid end end end describe "tag validation" do let(:article) { build(:article, user: user) } # See https://github.com/thepracticaldev/dev.to/pull/6302 # rubocop:disable RSpec/VerifiedDoubles it "does not modify the tag list if there are no adjustments" do allow(TagAdjustment).to receive(:where).and_return(TagAdjustment.none) allow(article).to receive(:tag_list).and_return(spy("tag_list")) article.save # We expect this to happen once in #evaluate_front_matter expect(article.tag_list).to have_received(:add).once expect(article.tag_list).not_to have_received(:remove) end # rubocop:enable RSpec/VerifiedDoubles end end context "when data is extracted from evaluation of the front matter during validation" do let!(:title) { "Talk About It, Justify It" } let!(:slug) { "talk-about-it-justify-it" } let!(:test_article) { build(:article, title: title) } before { test_article.validate } describe "#title" do it "produces a proper title" do expect(test_article.title).to eq(title) end end describe "#slug" do it "produces a proper slug similar to the title" do expect(test_article.slug).to start_with(slug) end end describe "#tag" do it "parses tags" do expect(test_article.tag_list.length.positive?).to be(true) end it "accepts an empty tag list and returns empty array" do expect(build_and_validate_article(with_tags: false).tag_list).to be_empty end it "rejects more than 4 tags" do five_tags = "one, two, three, four, five" expect(build(:article, tags: five_tags).valid?).to be(false) end it "rejects tags with length > 30" do tags = "'testing tag length with more than 30 chars', tag" expect(build(:article, tags: tags).valid?).to be(false) end it "rejects tag with non-alphanumerics" do expect { build(:article, tags: "c++").validate! }.to raise_error(ActiveRecord::RecordInvalid) end it "always downcase tags" do tags = "UPPERCASE, CAPITALIZE" article = create(:article, tags: tags) expect(article.tag_list).to eq(tags.downcase.split(", ")) end it "parses tags when description is empty" do body_markdown = "---\ntitle: Title\npublished: false\ndescription:\ntags: one\n---\n\n" expect(build_and_validate_article(body_markdown: body_markdown).tag_list).to eq(["one"]) end end describe "#description" do it "creates proper description when description is present" do body_markdown = "---\ntitle: Title\npublished: false\ndescription: hey hey hoho\ntags: one\n---\n\n" expect(build_and_validate_article(body_markdown: body_markdown).description).to eq("hey hey hoho") end it "creates proper description when description is not present and body is present and short, with no tags" do body_markdown = "---\ntitle: Title\npublished: false\ndescription:\ntags:\n---\n\nThis is the body yo" expect(build_and_validate_article(body_markdown: body_markdown).description).to eq("This is the body yo") end it "creates proper description when description is not present and body is present and short" do body_markdown = "---\ntitle: Title\npublished: false\ndescription:\ntags: heytag\n---\n\nThis is the body yo" expect(build_and_validate_article(body_markdown: body_markdown).description).to eq("This is the body yo") end it "creates proper description when description is not present and body is present and long" do paragraphs = Faker::Hipster.paragraph(sentence_count: 40) body_markdown = "---\ntitle: Title\npublished: false\ndescription:\ntags:\n---\n\n#{paragraphs}" expect(build_and_validate_article(body_markdown: body_markdown).description).to end_with("...") end end describe "#canonical_url" do let!(:article_with_canon_url) { build(:article, with_canonical_url: true) } before do article_with_canon_url.validate end it "parses does not assign canonical_url" do expect(article.canonical_url).to eq(nil) end it "parses canonical_url if canonical_url is present" do expect(article_with_canon_url.canonical_url).not_to be_nil end it "parses does not remove canonical_url" do initial_link = article_with_canon_url.canonical_url article_with_canon_url.body_markdown = build(:article).body_markdown article_with_canon_url.validate expect(article_with_canon_url.canonical_url).to eq(initial_link) end end describe "#reading_time" do it "produces a correct reading time" do expect(test_article.reading_time).to eq(1) end end describe "#processed_html" do it "fixes the issue with --- hr tags" do article = build_and_validate_article(with_hr_issue: true) expect(article.processed_html.include?("