docbrown/spec/services/articles/updater_spec.rb
Joshua Wehner 375cbfe946
Refresh audience segment for 'active' user (#19470)
* Refresh an individual user's segmentation

* Refresh segment if creating a published article

* Quick refactor

* Refresh segment if update publishing

* Quick refactor

* Refresh segment if update user settings

* Refresh segments after user update + role change

* Using latest_article_updated_at

* Sidekiq uses JSON for arguments, needs basic types

* Fix test issues with last_updated_at and sidekiq params

* Tests update continues

* Consolidate dependencies

* Method names acknowledging manual exclusive

* Fix test name copypasta

* Rubocop

* Try to clarify Creator#series

* Try to avoid naming predicate

* Remove erroneously included helper
2023-05-19 14:16:07 +02:00

329 lines
13 KiB
Ruby

require "rails_helper"
RSpec.describe Articles::Updater, type: :service do
let(:user) { create(:user) }
let!(:article) { create(:article, user: user) }
let(:attributes) { { body_markdown: "sample" } }
let(:draft) { create(:article, user: user, published: false, published_at: nil) }
before do
allow(FeatureFlag).to receive(:enabled?).with(:consistent_rendering, any_args).and_return(true)
allow(SegmentedUserRefreshWorker).to receive(:perform_async)
end
it "updates an article" do
described_class.call(user, article, attributes)
article.reload
expect(article.body_markdown).to eq("sample")
end
it "sets a collection" do
attributes[:series] = "collection-slug"
described_class.call(user, article, attributes)
article.reload
expect(article.collection).to be_a(Collection)
end
it "creates a collection for the user, not admin when updated by admin" do
admin = create(:user, :super_admin)
attributes[:series] = "new-slug"
described_class.call(admin, article, attributes)
expect(article.reload.collection.user).to eq(article.user)
end
it "sets tags" do
attributes[:tags] = %w[ruby productivity]
described_class.call(user, article, attributes)
article.reload
expect(article.tags.pluck(:name).sort).to eq(%w[productivity ruby])
end
describe "result" do
it "returns success when saved" do
result = described_class.call(user, article, attributes)
expect(result.success).to be true
expect(result.article).to be_a(ArticleDecorator)
end
it "returns not success when not saved" do
invalid_attributes = { body_markdown: nil }
result = described_class.call(user, article, invalid_attributes)
expect(result.success).to be false
expect(result.article.errors.any?).to be true
end
end
describe "notifications" do
let(:past_time) { 1.year.ago }
let(:future_time) { 2.days.from_now }
context "when an article is updated and published the first time" do
before { attributes[:published] = true }
# actually, published_at is set in a model
it "sets current published_at when publishing from a draft" do
attributes[:published_at] = nil
described_class.call(user, draft, attributes)
draft.reload
expect(draft.published_at).to be_within(1.minute).of(Time.current)
end
it "sets the passed published_at when a future published_at is passed" do
attributes[:published_at] = 1.day.from_now
described_class.call(user, draft, attributes)
draft.reload
expect(draft.published_at).to be_within(1.second).of(attributes[:published_at])
end
it "allows setting past published_at for exported articles (frontmatter)" do
draft.update_columns(published_from_feed: true)
past = 10.days.ago
published_at = past.strftime("%d/%m/%Y %H:%M %z")
body_markdown = "---\ntitle: Title\npublished: true\npublished_at: #{published_at}\ndescription:\ntags: heytag
\n---\n\nHey this is the article"
frontmatter_attributes = { body_markdown: body_markdown }
described_class.call(user, draft, frontmatter_attributes)
expect(draft.published_at).to be_within(1.minute).of(past)
end
end
context "when an article is being updated (published => published)" do
it "doesn't update published_at" do
attributes[:published] = true
article.update_column(:published_at, past_time)
described_class.call(user, article, attributes)
article.reload
expect(article.published_at).to be_within(1.second).of(past_time)
end
it "delegates to the Mentions::CreateAll service to create mentions" do
allow(Mentions::CreateAll).to receive(:call)
described_class.call(user, article, attributes)
expect(Mentions::CreateAll).to have_received(:call).with(article)
end
end
context "when an article is being republished" do
it "doesn't update past published_at" do
draft.update_column(:published_at, past_time)
attributes[:published] = true
described_class.call(user, draft, attributes)
draft.reload
expect(draft.published).to be true
expect(draft.published_at).to be_within(1.second).of(past_time)
end
it "doesn't update future published_at" do
draft.update_column(:published_at, future_time)
attributes[:published] = true
described_class.call(user, draft, attributes)
draft.reload
expect(draft.published).to be true
expect(draft.published_at).to be_within(1.second).of(future_time)
end
it "updates future published_at if new published_at is passed" do
draft.update_column(:published_at, future_time)
attributes[:published] = true
new_published_at = 1.day.from_now
attributes[:published_at] = new_published_at
described_class.call(user, draft, attributes)
draft.reload
expect(draft.published).to be true
expect(draft.published_at).to be_within(1.minute).of(new_published_at)
end
it "doesn't update past published_at if new published_at is passed" do
draft.update_column(:published_at, past_time)
attributes[:published] = true
new_published_at = 1.day.from_now
attributes[:published_at] = new_published_at
described_class.call(user, draft, attributes)
draft.reload
expect(draft.published).to be true
expect(draft.published_at).to be_within(1.second).of(past_time)
end
it "doesn't update published at when it is passed from frontmatter" do
draft.update_columns(published_at: past_time)
published_at = 10.days.from_now.strftime("%d/%m/%Y %H:%M %z")
body_markdown = "---\ntitle: Title\npublished: false\npublished_at: #{published_at}\ndescription:\ntags: heytag
\n---\n\nHey this is the article"
attributes = { body_markdown: body_markdown }
described_class.call(user, draft, attributes)
draft.reload
expect(draft.published_at).to be_within(1.second).of(past_time)
end
it "doesn't update published_at when published_at is passed and an article was exported" do
draft.update_columns(published_at: past_time, published_from_feed: true)
new_time = 10.days.from_now
attributes[:published_at] = new_time
described_class.call(user, draft, attributes)
draft.reload
expect(draft.published_at).to be_within(1.second).of(past_time)
end
it "doesn't update published_at when published_at is passed (from frontmatter) and an article was exported" do
draft.update_columns(published_at: past_time, published_from_feed: true)
new_time = 10.days.from_now
new_pub_at = new_time.strftime("%d/%m/%Y %H:%M %z")
body_markdown = "---\ntitle: Title\npublished: true\npublished_at: #{new_pub_at}\ndescription:\ntags: heytag
\n---\n\nHey this is the article"
frontmatter_attributes = { body_markdown: body_markdown }
described_class.call(user, draft, frontmatter_attributes)
draft.reload
expect(draft.published_at).to be_within(1.second).of(past_time)
end
end
context "when an article is being unpublished from frontmatter" do
let(:published_at) { 1.hour.from_now }
let(:published_at_str) { published_at.strftime("%d/%m/%Y %H:%M %z") }
let(:f_article) do
markdown = "---\ntitle: Title\npublished: true\npublished_at: #{published_at_str}\n
description:\ntags: heytag\n---\n\nHey this is the article"
create(:article, user: user, body_markdown: markdown)
end
it "doesn't remove published_at when it's not passed" do
body_markdown = "---\ntitle: Title\npublished: false\ndescription:\ntags: heytag
\n---\n\nHey this is the article"
described_class.call(user, f_article, { body_markdown: body_markdown })
f_article.reload
expect(f_article.published_at).to be_within(1.minute).of(published_at)
end
it "doesn't remove published_at when it's passed" do
body_markdown = "---\ntitle: Title\npublished: false\n
published_at: #{published_at_str}\ndescription:\ntags: heytag\n---\n\nHey this is the article"
described_class.call(user, f_article, { body_markdown: body_markdown })
f_article.reload
expect(f_article.published_at).to be_within(1.minute).of(published_at)
end
end
context "when an article is being unpublished" do
before { attributes[:published] = false }
it "doesn't update published_at" do
published_at = 1.day.ago
article.update_column(:published_at, published_at)
described_class.call(user, article, attributes)
article.reload
expect(article.published_at).to be_within(1.second).of(published_at)
end
it "doesn't send any notifications" do
sidekiq_assert_not_enqueued_with(job: Notifications::NotifiableActionWorker) do
described_class.call(user, article, attributes)
end
end
it "doesn't delegate to the Mentions::CreateAll service to create mentions" do
allow(Mentions::CreateAll).to receive(:call)
described_class.call(user, article, attributes)
expect(Mentions::CreateAll).not_to have_received(:call).with(article)
end
it "destroys the preexisting notifications" do
allow(Notification).to receive(:remove_all_by_action_without_delay).and_call_original
described_class.call(user, article, attributes)
attrs = { notifiable_ids: article.id, notifiable_type: "Article", action: "Published" }
expect(Notification).to have_received(:remove_all_by_action_without_delay).with(attrs)
# expect(ContextNotification).to have_received(:delete_all)
end
it "destroys the preexisting context notifications" do
create(:context_notification, context: article, action: "Published")
expect do
described_class.call(user, article, attributes)
end.to change(ContextNotification, :count).by(-1)
end
end
context "when an article is updated and remains draft" do
it "doesn't update published_at" do
attributes[:published] = false
described_class.call(user, draft, attributes)
article.reload
expect(draft.published_at).to be_nil
end
end
context "when an article is being unpublished and contains comments" do
let!(:comment) { create(:comment, user_id: user.id, commentable: article) }
let(:notification) do
create(:notification, user: user, notifiable_id: comment.id, notifiable_type: "Comment")
end
before do
attributes[:published] = false
allow(Notification).to receive(:remove_all).and_call_original
end
it "removes any preexisting comment notifications but does not delete the comment" do
described_class.call(user, article, attributes)
expect(Notification).to have_received(:remove_all).with(
notifiable_ids: [comment.id], notifiable_type: "Comment",
)
expect(article.comments.length).to eq(1)
end
end
context "when an article is being unpublished and contains mentions" do
let!(:mention) { create(:mention, mentionable: article, user: user) }
let(:notification) do
create(:notification, user: user, notifiable_id: mention.id, notifiable_type: "Mention")
end
before do
attributes[:published] = false
allow(Notification).to receive(:remove_all).and_call_original
end
it "removes any preexisting mention notifications but does not delete the mention" do
described_class.call(user, article, attributes)
expect(Notification).to have_received(:remove_all).with(
notifiable_ids: [mention.id], notifiable_type: "Mention",
)
expect(article.mentions.length).to eq(1)
end
end
end
context "when publishing a previously unpublished article" do
let(:attributes) { { published: true } }
let(:markdown) { "body text here, no frontmatter" }
let(:unpublished) { create(:unpublished_article, user: user) }
it "refreshes user segments" do
described_class.call(user, unpublished, attributes)
expect(SegmentedUserRefreshWorker).to have_received(:perform_async).with(user.id)
end
end
context "when unpublished a previously published article" do
let(:attributes) { { published: false } }
let(:markdown) { "body text here, no frontmatter" }
let(:published) { create(:published_article, :past, user: user) }
it "does not refresh user segments" do
described_class.call(user, published, attributes)
expect(SegmentedUserRefreshWorker).not_to have_received(:perform_async)
end
end
context "when setting published_at for a previously unpublished article" do
let(:attributes) { { published_at: 5.days.from_now } }
let(:markdown) { "body text here, no frontmatter" }
let(:unpublished) { create(:unpublished_article, user: user) }
it "does not refresh user segments" do
described_class.call(user, unpublished, attributes)
expect(SegmentedUserRefreshWorker).not_to have_received(:perform_async)
end
end
end