* 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
329 lines
13 KiB
Ruby
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
|