diff --git a/app/assets/stylesheets/admin/views/_host-content-update-event.scss b/app/assets/stylesheets/admin/views/_host-content-update-event.scss new file mode 100644 index 00000000000..15e8e392043 --- /dev/null +++ b/app/assets/stylesheets/admin/views/_host-content-update-event.scss @@ -0,0 +1,20 @@ +.app-view-editions-host-content-update-event-entry { + &__list-item { + margin-bottom: govuk-spacing(4); + } + + &__detail { + margin-top: govuk-spacing(0); + margin-bottom: govuk-spacing(0); + } + + &__heading { + margin-bottom: govuk-spacing(1); + } + + &__datetime { + margin-top: govuk-spacing(0); + margin-bottom: govuk-spacing(0); + color: $govuk-secondary-text-colour; + } +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 15a3d953592..f69dcfc37b6 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -34,6 +34,7 @@ $govuk-page-width: 1140px; @import "./admin/views/filter"; @import "./admin/views/govspeak-help"; @import "./admin/views/groups-index"; +@import "./admin/views/host-content-update-event"; @import "./admin/views/historical-accounts-index"; @import "./admin/views/organisations-index"; @import "./admin/views/organisations-edit"; diff --git a/app/components/admin/editions/audit_trail_entry_component.html.erb b/app/components/admin/editions/audit_trail_entry_component.html.erb index 7eb9ad3f83d..2896f5dec83 100644 --- a/app/components/admin/editions/audit_trail_entry_component.html.erb +++ b/app/components/admin/editions/audit_trail_entry_component.html.erb @@ -3,7 +3,7 @@ <% if compare_with_previous_version? %>

- <%= link_to "[Compare with previous version]", diff_admin_edition_path(@edition, audit_trail_entry_id: entry.version.item_id), class: "govuk-link" %> + <%= link_to "[Compare with previous version]", diff_admin_edition_path(@edition, audit_trail_entry_id: entry.item_id), class: "govuk-link" %>

<% end %> diff --git a/app/components/admin/editions/audit_trail_entry_component.rb b/app/components/admin/editions/audit_trail_entry_component.rb index 46cbaa60138..d55ef6cb6fc 100644 --- a/app/components/admin/editions/audit_trail_entry_component.rb +++ b/app/components/admin/editions/audit_trail_entry_component.rb @@ -25,6 +25,6 @@ def time end def compare_with_previous_version? - entry.action == "published" && edition.id != entry.version.item_id + entry.action == "published" && edition.id != entry.item_id end end diff --git a/app/components/admin/editions/document_history_tab_component.rb b/app/components/admin/editions/document_history_tab_component.rb index 033887c495c..283715292ed 100644 --- a/app/components/admin/editions/document_history_tab_component.rb +++ b/app/components/admin/editions/document_history_tab_component.rb @@ -26,6 +26,8 @@ def entries_on_previous_editions def render_entry(entry) if entry.is_a?(EditorialRemark) render(Admin::Editions::EditorialRemarkComponent.new(editorial_remark: entry)) + elsif entry.is_a?(HostContentUpdateEvent) + render(Admin::Editions::HostContentUpdateEventComponent.new(entry)) else render(Admin::Editions::AuditTrailEntryComponent.new(entry:, edition:)) end diff --git a/app/components/admin/editions/host_content_update_event_component.html.erb b/app/components/admin/editions/host_content_update_event_component.html.erb new file mode 100644 index 00000000000..3f85397a9ac --- /dev/null +++ b/app/components/admin/editions/host_content_update_event_component.html.erb @@ -0,0 +1,11 @@ +
+

Content Block Update

+ +

+ <%= activity %> +

+ +

+ <%= time %> by <%= actor %> +

+
diff --git a/app/components/admin/editions/host_content_update_event_component.rb b/app/components/admin/editions/host_content_update_event_component.rb new file mode 100644 index 00000000000..f224b858300 --- /dev/null +++ b/app/components/admin/editions/host_content_update_event_component.rb @@ -0,0 +1,23 @@ +class Admin::Editions::HostContentUpdateEventComponent < ViewComponent::Base + include ApplicationHelper + + def initialize(event) + @event = event + end + +private + + attr_reader :event + + def activity + "#{event.content_title.strip} updated" + end + + def time + absolute_time(event.created_at) + end + + def actor + event.author ? linked_author(event.author, class: "govuk-link") : "User (removed)" + end +end diff --git a/app/decorators/document/paginated_timeline/version_decorator.rb b/app/decorators/document/paginated_timeline/version_decorator.rb new file mode 100644 index 00000000000..966885d70cc --- /dev/null +++ b/app/decorators/document/paginated_timeline/version_decorator.rb @@ -0,0 +1,45 @@ +class Document::PaginatedTimeline::VersionDecorator < SimpleDelegator + def initialize(version, is_first_edition: false, previous_version: nil) + @is_first_edition = is_first_edition + @preloaded_previous_version = previous_version + super(version) + end + + def ==(other) + self.class == other.class && + id == other.id && + action == other.action + end + + def actor + user + end + + def action + case event + when "create" + @is_first_edition ? "created" : "editioned" + else + previous_version&.state != state ? state : "updated" + end + end + + def is_for_newer_edition?(edition) + item_id > edition.id + end + + def is_for_current_edition?(edition) + item_id == edition.id + end + + def is_for_older_edition?(edition) + item_id < edition.id + end + +private + + def previous_version + # we can avoid n+1 queries by using our preloaded_prev_version + @previous_version ||= @preloaded_previous_version || previous + end +end diff --git a/app/models/document.rb b/app/models/document.rb index 8630ef58764..bd1a3ac15a0 100644 --- a/app/models/document.rb +++ b/app/models/document.rb @@ -60,6 +60,31 @@ def self.at_slug(document_types, slug) find_by(document_type: document_types, slug:) end + def remarks_by_ids(remark_ids) + editorial_remarks.where(id: remark_ids).index_by(&:id) + end + + def active_edition_versions + edition_versions.where.not(state: "superseded") + end + + def decorated_edition_versions_by_ids(version_ids) + versions = active_edition_versions.where(id: version_ids) + + versions.map.with_index { |version, index| + version = Document::PaginatedTimeline::VersionDecorator.new( + version, + is_first_edition: version.item_id == first_edition_id, + previous_version: versions[index - 1], + ) + [version.id, version] + }.to_h + end + + def first_edition_id + @first_edition_id ||= editions.pick(:id) + end + def similar_slug_exists? scope = Document.where(document_type:) sequence_separator = friendly_id_config.sequence_separator diff --git a/app/models/document/paginated_timeline.rb b/app/models/document/paginated_timeline.rb index fa99ff53aa4..d4f1675f935 100644 --- a/app/models/document/paginated_timeline.rb +++ b/app/models/document/paginated_timeline.rb @@ -10,10 +10,10 @@ def initialize(document:, page:, only: nil) def entries @entries ||= begin raw_entries = query.raw_entries - remarks = query.remarks - versions = query.versions + remarks = document.remarks_by_ids(query.remark_ids) + versions = document.decorated_edition_versions_by_ids(query.version_ids) - raw_entries.map do |entry| + versions_and_remarks = raw_entries.map do |entry| case entry.model when "Version" versions.fetch(entry.id) @@ -21,6 +21,12 @@ def entries remarks.fetch(entry.id) end end + + if only.present? + versions_and_remarks + else + [*versions_and_remarks, *host_content_update_events].sort_by(&:created_at).reverse! + end end end @@ -29,33 +35,15 @@ def query end def entries_on_newer_editions(edition) - @entries_on_newer_editions ||= entries.select do |entry| - if entry.is_a?(EditorialRemark) - entry.edition_id > edition.id - else - entry.version.item_id > edition.id - end - end + entries.select { |e| e.is_for_newer_edition?(edition) } end def entries_on_current_edition(edition) - @entries_on_current_edition ||= entries.select do |entry| - if entry.is_a?(EditorialRemark) - entry.edition_id == edition.id - else - entry.version.item_id == edition.id - end - end + entries.select { |e| e.is_for_current_edition?(edition) } end def entries_on_previous_editions(edition) - @entries_on_previous_editions ||= entries.select do |entry| - if entry.is_a?(EditorialRemark) - entry.edition_id < edition.id - else - entry.version.item_id < edition.id - end - end + entries.select { |e| e.is_for_older_edition?(edition) } end def total_count @@ -89,4 +77,30 @@ def prev_page false end end + +private + + def host_content_update_events + return [] if query.raw_entries.empty? + + @host_content_update_events ||= HostContentUpdateEvent.all_for_date_window( + document:, + from: date_window.last, + to: date_window.first, + ) + end + + def date_window + @date_window ||= begin + start = page == 1 ? Time.zone.now : query.raw_entries.first.created_at + ends = next_page_entries ? next_page_entries.first.created_at : query.raw_entries.last.created_at + (start.to_time.round.utc..ends.to_time.round.utc) + end + end + + def next_page_entries + if next_page + Document::PaginatedTimelineQuery.new(document:, page: next_page, only:).raw_entries + end + end end diff --git a/app/models/edition.rb b/app/models/edition.rb index 6c61177de83..3e4325da6c3 100644 --- a/app/models/edition.rb +++ b/app/models/edition.rb @@ -347,6 +347,14 @@ def can_set_previously_published? true end + def superseded_at + versions.find { |v| v.state == "superseded" }&.created_at + end + + def published_at + versions.find { |v| v.state == "published" }&.created_at + end + def government if government_id.present? Government.find(government_id) diff --git a/app/models/editorial_remark.rb b/app/models/editorial_remark.rb index 7520d5bf28a..1f5b93f760c 100644 --- a/app/models/editorial_remark.rb +++ b/app/models/editorial_remark.rb @@ -3,4 +3,16 @@ class EditorialRemark < ApplicationRecord belongs_to :author, class_name: "User" validates :edition, :body, :author, presence: true + + def is_for_newer_edition?(edition) + edition_id > edition.id + end + + def is_for_current_edition?(edition) + edition_id == edition.id + end + + def is_for_older_edition?(edition) + edition_id < edition.id + end end diff --git a/app/models/host_content_update_event.rb b/app/models/host_content_update_event.rb new file mode 100644 index 00000000000..1265f5e15d6 --- /dev/null +++ b/app/models/host_content_update_event.rb @@ -0,0 +1,34 @@ +class HostContentUpdateEvent < Data.define(:author, :created_at, :content_id, :content_title) + def self.all_for_date_window(document:, from:, to:) + events = Services.publishing_api.get_events_for_content_id(document.content_id, { + action: "HostContentUpdateJob", + from:, + to:, + }) + + events.map do |event| + HostContentUpdateEvent.new( + author: get_user_for_uuid(event["payload"]["source_block"]["updated_by_user_uid"]), + created_at: Time.zone.parse(event["created_at"]), + content_id: event["payload"]["source_block"]["content_id"], + content_title: event["payload"]["source_block"]["title"], + ) + end + end + + def is_for_newer_edition?(edition) + edition.superseded? && created_at.after?(edition.superseded_at) + end + + def is_for_current_edition?(edition) + edition.published_at && created_at.after?(edition.published_at) && !is_for_newer_edition?(edition) + end + + def is_for_older_edition?(edition) + !is_for_newer_edition?(edition) && !is_for_current_edition?(edition) + end + + def self.get_user_for_uuid(uuid) + User.find_by(uid: uuid) + end +end diff --git a/app/presenters/queries/version_presenter.rb b/app/presenters/queries/version_presenter.rb deleted file mode 100644 index 9d92259b92b..00000000000 --- a/app/presenters/queries/version_presenter.rb +++ /dev/null @@ -1,45 +0,0 @@ -module Queries - class VersionPresenter - extend ActiveModel::Naming - - def self.model_name - ActiveModel::Name.new(Version, nil) - end - - attr_reader :version - - delegate :created_at, :to_key, to: :version - - def initialize(version, is_first_edition:, previous_version: nil) - @version = version - @is_first_edition = is_first_edition - @preloaded_previous_version = previous_version - end - - def ==(other) - self.class == other.class && - version == other.version && - action == other.action - end - - def actor - version.user - end - - def action - case version.event - when "create" - @is_first_edition ? "created" : "editioned" - else - previous_version&.state != version.state ? version.state : "updated" - end - end - - private - - def previous_version - # we can avoid n+1 queries by using our preloaded_prev_version - @previous_version ||= @preloaded_previous_version || version.previous - end - end -end diff --git a/app/queries/document/paginated_timeline_query.rb b/app/queries/document/paginated_timeline_query.rb index 83fa7fb3781..c4613b0ff5c 100644 --- a/app/queries/document/paginated_timeline_query.rb +++ b/app/queries/document/paginated_timeline_query.rb @@ -21,26 +21,6 @@ def total_count end end - def remarks - document_remarks.where(id: remark_ids).index_by(&:id) - end - - def versions - versions = document_versions.where(id: version_ids) - versions.map.with_index { |version, index| - presenter = Queries::VersionPresenter.new( - version, - is_first_edition: version.item_id == first_edition_id, - previous_version: versions[index - 1], - ) - [version.id, presenter] - }.to_h - end - - private - - attr_reader :document, :page, :only - def remark_ids raw_entries.select { |r| r.model == "EditorialRemark" }.map(&:id) end @@ -49,6 +29,10 @@ def version_ids raw_entries.select { |r| r.model == "Version" }.map(&:id) end + private + + attr_reader :document, :page, :only + def paginated_query sql = <<~SQL #{timeline_sql} @@ -67,14 +51,6 @@ def paginated_query ApplicationRecord.connection.exec_query(sql, "SQL", bind_params) end - def document_versions - @document.edition_versions.where.not(state: "superseded") - end - - def document_remarks - @document.editorial_remarks - end - def timeline_sql case only when "history" @@ -87,15 +63,15 @@ def timeline_sql end def versions_query - document_versions.select( - "'#{document_versions.class_name}' AS model_name", + document.active_edition_versions.select( + "'#{Version}' AS model_name", *common_fields, ).to_sql end def remarks_query - document_remarks.select( - "'#{document_remarks.class_name}' AS model_name", + document.editorial_remarks.select( + "'#{EditorialRemark}' AS model_name", *common_fields, ).to_sql end @@ -103,9 +79,5 @@ def remarks_query def common_fields %i[id created_at] end - - def first_edition_id - @first_edition_id ||= document.editions.pick(:id) - end end end diff --git a/features/admin-history-content-block-updates.feature b/features/admin-history-content-block-updates.feature new file mode 100644 index 00000000000..a3808cd478e --- /dev/null +++ b/features/admin-history-content-block-updates.feature @@ -0,0 +1,17 @@ +Feature: Showing content block updates in history + + Background: + Given I am an editor + And a published news article "Stubble to be Outlawed" exists + And the document has been updated by a change to the content block "Some email address" + + Scenario: Content block update exists for current edition + When I am on the edit page for news article "Stubble to be Outlawed" + And I click the "History" tab + Then I should see an entry for the content block "Some email address" on the current edition + + Scenario: Content block update exists for a previous edition + When I force publish a new edition of the news article "Stubble to be Outlawed" + And I am on the edit page for news article "Stubble to be Outlawed" + And I click the "History" tab + Then I should see an entry for the content block "Some email address" on the previous edition diff --git a/features/step_definitions/content_block_update_steps.rb b/features/step_definitions/content_block_update_steps.rb new file mode 100644 index 00000000000..662775d0146 --- /dev/null +++ b/features/step_definitions/content_block_update_steps.rb @@ -0,0 +1,13 @@ +And(/^the document has been updated by a change to the content block "([^"]*)"$/) do |content_block_title| + host_content_update_event = build(:host_content_update_event, content_title: content_block_title) + HostContentUpdateEvent.expects(:all_for_date_window).at_least_once.returns([host_content_update_event]) +end + +Then(/^I should see an entry for the content block "([^"]*)" on the (current|previous) edition$/) do |content_block, current_or_previous| + selector = current_or_previous == "current" ? ".app-view-editions__current-edition-entries" : ".app-view-editions__previous-edition-entries" + + within selector do + assert_text "Content Block Update" + assert_text "#{content_block} updated" + end +end diff --git a/features/step_definitions/document_steps.rb b/features/step_definitions/document_steps.rb index 46bbf4b6642..9e006a89b3e 100644 --- a/features/step_definitions/document_steps.rb +++ b/features/step_definitions/document_steps.rb @@ -92,6 +92,16 @@ publish end +When("I force publish a new edition of {edition}") do |edition| + stub_publishing_api_links_with_taxons(edition.content_id, %w[a-taxon-content-id]) + visit_edition_admin edition.title + click_button "Create new edition" + fill_in_change_note_if_required + apply_to_all_nations_if_required + click_button "Save and go to document summary" + publish(force: true) +end + When("I force publish {edition}") do |edition| stub_publishing_api_links_with_taxons(edition.content_id, %w[a-taxon-content-id]) visit_edition_admin edition.title, :draft @@ -139,7 +149,7 @@ When(/^I am on the edit page for (.*?) "(.*?)"$/) do |document_type, title| document_type = document_type.tr(" ", "_") - document = document_type.classify.constantize.find_by(title:) + document = document_type.classify.constantize.where(title:).last visit send("edit_admin_#{document_type}_path", document) end diff --git a/features/support/publishing_api.rb b/features/support/publishing_api.rb index cc68cb9ab98..c284d67e7da 100644 --- a/features/support/publishing_api.rb +++ b/features/support/publishing_api.rb @@ -14,6 +14,7 @@ GdsApi::PublishingApi.any_instance.stubs(:put_content) GdsApi::PublishingApi.any_instance.stubs(:patch_links) GdsApi::PublishingApi.any_instance.stubs(:unpublish) + GdsApi::PublishingApi.any_instance.stubs(:get_events_for_content_id).returns([]) need1 = { "content_id" => SecureRandom.uuid, diff --git a/test/components/admin/editions/audit_trail_entry_component_test.rb b/test/components/admin/editions/audit_trail_entry_component_test.rb index 35446e98827..d8881ea32a6 100644 --- a/test/components/admin/editions/audit_trail_entry_component_test.rb +++ b/test/components/admin/editions/audit_trail_entry_component_test.rb @@ -10,7 +10,7 @@ class Admin::Editions::AuditTrailEntryComponentTest < ViewComponent::TestCase edition = build_stubbed(:edition) version = edition.versions.new(event: "create", created_at: Time.zone.local(2020, 1, 1, 11, 11)) version.stubs(:user).returns(actor) - audit = Queries::VersionPresenter.new(version, is_first_edition: true) + audit = Document::PaginatedTimeline::VersionDecorator.new(version, is_first_edition: true) render_inline(Admin::Editions::AuditTrailEntryComponent.new(entry: audit, edition:)) @@ -23,7 +23,7 @@ class Admin::Editions::AuditTrailEntryComponentTest < ViewComponent::TestCase test "it constructs output based on the entry when an actor is absent" do edition = build_stubbed(:edition) version = edition.versions.new(event: "create", created_at: Time.zone.local(2020, 1, 1, 11, 11), whodunnit: nil) - audit = Queries::VersionPresenter.new(version, is_first_edition: true) + audit = Document::PaginatedTimeline::VersionDecorator.new(version, is_first_edition: true) render_inline(Admin::Editions::AuditTrailEntryComponent.new(entry: audit, edition:)) @@ -37,7 +37,7 @@ class Admin::Editions::AuditTrailEntryComponentTest < ViewComponent::TestCase edition.versions.new(event: "create", created_at: Time.zone.local(2020, 1, 1, 11, 11), whodunnit: actor.id) version = edition.versions.new(event: "published", created_at: Time.zone.local(2020, 1, 1, 11, 11), state: "published") version.stubs(:user).returns(actor) - audit = Queries::VersionPresenter.new(version, is_first_edition: true) + audit = Document::PaginatedTimeline::VersionDecorator.new(version, is_first_edition: true) newer_edition = build_stubbed(:edition, :draft) render_inline(Admin::Editions::AuditTrailEntryComponent.new(entry: audit, edition: newer_edition)) diff --git a/test/components/admin/editions/document_history_tab_component_test.rb b/test/components/admin/editions/document_history_tab_component_test.rb index 7cb766cc04d..7aa70aec28a 100644 --- a/test/components/admin/editions/document_history_tab_component_test.rb +++ b/test/components/admin/editions/document_history_tab_component_test.rb @@ -54,17 +54,22 @@ class Admin::Editions::DocumentHistoryTabComponentTest < ViewComponent::TestCase assert_selector ".app-view-editions__newer-edition-entries h3", text: "On newer editions" assert_selector ".app-view-editions__newer-edition-entries div.app-view-editions-audit-trail-entry__list-item", count: 2 assert_selector ".app-view-editions__newer-edition-entries div.app-view-editions-editorial-remark__list-item", count: 1 + assert_selector ".app-view-editions__newer-edition-entries div.app-view-editions-host-content-update-event-entry__list-item", count: 0 assert_selector ".app-view-editions__current-edition-entries h3", text: "On this edition" assert_selector ".app-view-editions__current-edition-entries div.app-view-editions-audit-trail-entry__list-item", count: 4 assert_selector ".app-view-editions__current-edition-entries div.app-view-editions-editorial-remark__list-item", count: 1 + assert_selector ".app-view-editions__current-edition-entries div.app-view-editions-host-content-update-event-entry__list-item", count: 1 assert_selector ".app-view-editions__previous-edition-entries h3", text: "On previous editions" assert_selector ".app-view-editions__previous-edition-entries div.app-view-editions-audit-trail-entry__list-item", count: 2 assert_selector ".app-view-editions__previous-edition-entries div.app-view-editions-editorial-remark__list-item", count: 0 + assert_selector ".app-view-editions__previous-edition-entries div.app-view-editions-host-content-update-event-entry__list-item", count: 2 end def seed_document_event_history + @events = [] + acting_as(@user) do @document = create(:document) @first_edition = create(:draft_edition, document: @document, major_change_published_at: Time.zone.now) @@ -96,6 +101,10 @@ def seed_document_event_history @first_edition.publish! end + some_time_passes + @events << build(:host_content_update_event, created_at: Time.zone.now) + some_time_passes + @events << build(:host_content_update_event, created_at: Time.zone.now) some_time_passes acting_as(@user) do @@ -108,6 +117,8 @@ def seed_document_event_history @second_edition.publish! end + some_time_passes + @events << build(:host_content_update_event, created_at: Time.zone.now) some_time_passes acting_as(@user2) do @@ -117,6 +128,8 @@ def seed_document_event_history some_time_passes create(:editorial_remark, edition: @third_edition, author: @user, body: "Drafted to include newer changes.") end + + HostContentUpdateEvent.stubs(:all_for_date_window).returns(@events) end def some_time_passes diff --git a/test/components/admin/editions/host_content_update_event_component_test.rb b/test/components/admin/editions/host_content_update_event_component_test.rb new file mode 100644 index 00000000000..7379fd9684d --- /dev/null +++ b/test/components/admin/editions/host_content_update_event_component_test.rb @@ -0,0 +1,32 @@ +require "test_helper" + +class Admin::Editions::HostContentUpdateEventComponentTest < ViewComponent::TestCase + extend Minitest::Spec::DSL + include Rails.application.routes.url_helpers + + let(:created_at) { Time.zone.local(2020, 1, 1, 11, 11) } + let(:content_title) { "Some content" } + let(:user) { build_stubbed(:user) } + + let(:host_content_update_event) do + build(:host_content_update_event, content_title:, created_at:, author: user) + end + + it "constructs output based on the entry when an actor is present" do + render_inline(Admin::Editions::HostContentUpdateEventComponent.new(host_content_update_event)) + + assert_equal page.find("h4").text, "Content Block Update" + assert_equal page.all("p")[0].text.strip, "#{content_title} updated" + assert_equal page.all("p")[1].text.strip, "1 January 2020 11:11am by #{user.name}" + end + + describe "when an actor is not present" do + let(:user) { nil } + + it "shows removed user when an actor is not present" do + render_inline(Admin::Editions::HostContentUpdateEventComponent.new(host_content_update_event)) + + assert_equal page.all("p")[1].text.strip, "1 January 2020 11:11am by User (removed)" + end + end +end diff --git a/test/factories/host_content_update_events.rb b/test/factories/host_content_update_events.rb new file mode 100644 index 00000000000..c59620bf1ad --- /dev/null +++ b/test/factories/host_content_update_events.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :host_content_update_event do + author { create(:user) } + created_at { Time.zone.now } + content_id { SecureRandom.uuid } + content_title { "Some title" } + + initialize_with do + new(author:, created_at:, content_id:, content_title:) + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 90f5bbf431c..f595527b719 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -69,6 +69,7 @@ class ActiveSupport::TestCase Sidekiq::Job.clear_all stub_any_publishing_api_call stub_publishing_api_publish_intent + Services.publishing_api.stubs(:get_events_for_content_id).returns([]) Services.stubs(:asset_manager).returns(stub_everything("asset-manager")) end diff --git a/test/unit/app/decorators/document/paginated_timeline/version_decorator_test.rb b/test/unit/app/decorators/document/paginated_timeline/version_decorator_test.rb new file mode 100644 index 00000000000..0a722cad62e --- /dev/null +++ b/test/unit/app/decorators/document/paginated_timeline/version_decorator_test.rb @@ -0,0 +1,118 @@ +require "test_helper" + +class Document::PaginatedTimeline::VersionDecoratorTest < ActiveSupport::TestCase + extend Minitest::Spec::DSL + + let(:user) { build(:user) } + let(:item_id) { 5 } + let(:edition) { build(:edition, id: item_id) } + let(:version) { build(:version, user:, item: edition) } + let(:is_first_edition) { false } + let(:previous_version) { build(:version, user:, item: edition) } + + let(:decorator) { Document::PaginatedTimeline::VersionDecorator.new(version, is_first_edition:, previous_version:) } + + describe "#==" do + it "is true if the class, ID and action are the same" do + other_decorator = Document::PaginatedTimeline::VersionDecorator.new(version, is_first_edition:, previous_version:) + + assert decorator == other_decorator + end + + it "is not true if the class ID and action are different" do + other_version = build(:version, user:, item: edition, id: 444) + other_decorator = Document::PaginatedTimeline::VersionDecorator.new(other_version, is_first_edition:, previous_version:) + + assert_not decorator == other_decorator + end + end + + describe "#actor" do + it "returns the version's user" do + assert_equal decorator.actor, version.user + end + end + + describe "#action" do + context "when the event is `create`" do + let(:version) { build(:version, user:, item: edition, event: "create") } + + it "returns `editioned`" do + assert_equal decorator.action, "editioned" + end + + context "and the edition is the first edition" do + let(:is_first_edition) { true } + + it "returns `created`" do + assert_equal decorator.action, "created" + end + end + end + + context "when the state has not changed" do + let(:version) { build(:version, user:, item: edition, state: "some_state") } + let(:previous_version) { build(:version, user:, item: edition, state: "some_state") } + + it "returns `updated`" do + assert_equal decorator.action, "updated" + end + end + + context "when the state has changed" do + let(:version) { build(:version, user:, item: edition, state: "state2") } + let(:previous_version) { build(:version, user:, item: edition, state: "state1") } + + it "returns the version's state" do + assert_equal decorator.action, version.state + end + end + + context "when previous_version is not preloaded" do + let(:previous_version) { nil } + + it "loads the previous version" do + loaded_version = build(:version, user:, item: edition) + version.expects(:previous).returns(loaded_version) + + decorator.action + end + end + end + + describe "#is_for_newer_edition?" do + it "returns true if the version's item_id is less than the edition's id" do + edition_to_compare = build(:edition, id: item_id - 1) + assert decorator.is_for_newer_edition?(edition_to_compare) + end + + it "returns false if the version's item_id is greater than the edition's id" do + edition_to_compare = build(:edition, id: item_id + 1) + assert_not decorator.is_for_newer_edition?(edition_to_compare) + end + end + + describe "#is_for_current_edition?" do + it "returns true if the version's item_id is equal to the edition's id" do + edition_to_compare = build(:edition, id: item_id) + assert decorator.is_for_current_edition?(edition_to_compare) + end + + it "returns true if the version's item_id is not equal to the edition's id" do + edition_to_compare = build(:edition, id: item_id + 1) + assert_not decorator.is_for_current_edition?(edition_to_compare) + end + end + + describe "#is_for_older_edition?" do + it "returns true if the version's item_id is greater than the edition's id" do + edition_to_compare = build(:edition, id: item_id + 1) + assert decorator.is_for_older_edition?(edition_to_compare) + end + + it "returns false if the version's item_id is less than the edition's id" do + edition_to_compare = build(:edition, id: item_id - 1) + assert_not decorator.is_for_older_edition?(edition_to_compare) + end + end +end diff --git a/test/unit/app/models/document/paginated_timeline_test.rb b/test/unit/app/models/document/paginated_timeline_test.rb index c32d245c72e..877a9f72042 100644 --- a/test/unit/app/models/document/paginated_timeline_test.rb +++ b/test/unit/app/models/document/paginated_timeline_test.rb @@ -1,94 +1,247 @@ require "test_helper" class PaginatedTimelineTest < ActiveSupport::TestCase + extend Minitest::Spec::DSL + setup do @user = create(:departmental_editor) @user2 = create(:departmental_editor) + HostContentUpdateEvent.stubs(:all_for_date_window).returns([]) seed_document_event_history end - test "#entries are ordered newest first (reverse chronological order)" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1) - entry_timestamps = timeline.entries.map(&:created_at) - assert_equal entry_timestamps.sort.reverse, entry_timestamps - end + describe "#entries" do + it "orders newest first (reverse chronological order)" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + entry_timestamps = timeline.entries.map(&:created_at) + assert_equal entry_timestamps.sort.reverse, entry_timestamps + end - test "#entries is a list of VersionPresenter and EditorialRemark objects when no 'only' argument is passed in" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1) - assert_equal [Queries::VersionPresenter, EditorialRemark].to_set, - timeline.entries.map(&:class).to_set - end + it "is a list of Document::PaginatedTimeline::VersionDecorator and EditorialRemark objects when 'only' argument is not present" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + assert_equal [Document::PaginatedTimeline::VersionDecorator, EditorialRemark].to_set, + timeline.entries.map(&:class).to_set + end - test "#total_count counts the list of VersionPresenter and EditorialRemark objects when no 'only' argument is passed in" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1) - assert_equal 11, timeline.total_count - end + it "paginates correctly" do + expect_total_count = 11 + results_per_page = 4 + expect_total_pages = 3 + + # Get paginated results + paginated_results = nil + mock_pagination(per_page: results_per_page) do + paginated_results = (1..expect_total_pages).map do |page| + Document::PaginatedTimeline.new(document: @document, page:).entries + end + end - test "#entries are paginated correctly" do - expect_total_count = 11 - results_per_page = 4 - expect_total_pages = 3 + # Get unpaginated results by setting per_page to the total count + unpaginated_results = mock_pagination(per_page: expect_total_count) do + Document::PaginatedTimeline.new(document: @document, page: 1).entries + end - # Get paginated results - paginated_results = nil - mock_pagination(per_page: results_per_page) do - paginated_results = (1..expect_total_pages).map do |page| - Document::PaginatedTimeline.new(document: @document, page:).entries + # Compare unpaginated results to paginated results + assert_equal unpaginated_results.each_slice(results_per_page).to_a, paginated_results + end + + it "correctly determines actions" do + mock_pagination(per_page: 30) do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + entries = timeline.entries.select { |e| e.instance_of?(Document::PaginatedTimeline::VersionDecorator) } + expected_actions = %w[updated + editioned + published + submitted + updated + rejected + submitted + created] + + assert_equal expected_actions, entries.map(&:action) end end - # Get unpaginated results by setting per_page to the total count - unpaginated_results = mock_pagination(per_page: expect_total_count) do - Document::PaginatedTimeline.new(document: @document, page: 1).entries + it "correctly determines actors" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + entries = timeline.entries.select { |e| e.instance_of?(Document::PaginatedTimeline::VersionDecorator) } + expected_actors = [@user, + @user, + @user2, + @user, + @user, + @user2, + @user] + + assert_equal expected_actors, entries.map(&:actor) end - # Compare unpaginated results to paginated results - assert_equal unpaginated_results.each_slice(results_per_page).to_a, paginated_results - end + describe "when there are HostContentUpdateEvents present" do + let(:host_content_update_events) { build_list(:host_content_update_event, 3) } - test "#entries is a list of EditorialRemark objects when 'internal_notes' is passed into the 'only' argument" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "internal_notes") - expected_entries = [@editorial_remark3, @editorial_remark2, @editorial_remark1] + before do + HostContentUpdateEvent.stubs(:all_for_date_window).returns(host_content_update_events) + end - assert_equal expected_entries, timeline.entries - end + it "orders newest first (reverse chronological order)" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + entry_timestamps = timeline.entries.flatten.map(&:created_at) + assert_equal entry_timestamps.sort.reverse, entry_timestamps + end - test "#total_count counts the total EditorialRemark objects when 'internal_notes' is passed into the 'only' argument" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "internal_notes") - assert_equal 3, timeline.total_count - end + it "is a list of Document::PaginatedTimeline::VersionDecorator and EditorialRemark and HostContentUpdateEvent objects when 'only' argument is not present" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + assert_equal [Document::PaginatedTimeline::VersionDecorator, EditorialRemark, HostContentUpdateEvent].to_set, + timeline.entries.map(&:class).to_set + end + + it "includes all of the HostContentUpdateEvent objects" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + + host_content_update_events.each do |event| + assert_includes timeline.entries, event + end + end + + describe "fetching HostContentUpdateEvents" do + before do + HostContentUpdateEvent.reset_mocha + end + + describe "when on the first page" do + it "requests a date window from the first created_at date of the next page to the current datetime" do + Timecop.freeze do + page2_entries = Document::PaginatedTimeline.new(document: @document, page: 2).query.raw_entries + + HostContentUpdateEvent.expects(:all_for_date_window).with( + document: @document, + from: page2_entries.first.created_at.to_time.utc, + to: Time.zone.now.to_time.round.utc, + ).at_least_once.returns(host_content_update_events) + + Document::PaginatedTimeline.new(document: @document, page: 1).entries + end + end + + describe "when there are no results" do + it "does not request any events" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + timeline.query.expects(:raw_entries).at_least_once.returns([]) + + HostContentUpdateEvent.expects(:all_for_date_window).never + + timeline.entries + end + end + end + + describe "when not on the first page" do + it "requests a window between the first created_at date of the next page to the first created_at date of the current page" do + mock_pagination(per_page: 3) do + page2_entries = Document::PaginatedTimeline.new(document: @document, page: 2).query.raw_entries + page3_entries = Document::PaginatedTimeline.new(document: @document, page: 3).query.raw_entries + + HostContentUpdateEvent.expects(:all_for_date_window).with( + document: @document, + from: page3_entries.first.created_at.to_time.utc, + to: page2_entries.first.created_at.to_time.utc, + ).at_least_once.returns(host_content_update_events) + + Document::PaginatedTimeline.new(document: @document, page: 2).entries + end + end + end + + describe "when on the last page" do + it "fetches with the created_at date for the last item" do + Timecop.freeze do + mock_pagination(per_page: 30) do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + timeline_entries = timeline.query.raw_entries + + HostContentUpdateEvent.expects(:all_for_date_window).with( + document: @document, + from: timeline_entries.last.created_at.to_time.utc, + to: timeline_entries.first.created_at.to_time.utc, + ).at_least_once.returns(host_content_update_events) + + timeline.entries + end + end + end + end + end + end + + describe "when only argument is set to history" do + it "is a list of Document::PaginatedTimeline::VersionDecorator objects" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "history") + expected_actions = %w[updated + editioned + published + submitted + updated + rejected + submitted + created] + + assert_equal expected_actions, timeline.entries.map(&:action) + end + + it "does not fetch any HostContentUpdateEvents" do + HostContentUpdateEvent.expects(:all_for_date_window).never + + Document::PaginatedTimeline.new(document: @document, page: 1, only: "history").entries + end + end + + describe "when only argument is set to internal_notes" do + it "is a list of EditorialRemark objects when 'internal_notes'" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "internal_notes") + expected_entries = [@editorial_remark3, @editorial_remark2, @editorial_remark1] + + assert_equal expected_entries, timeline.entries + end + + it "does not fetch any HostContentUpdateEvents" do + HostContentUpdateEvent.expects(:all_for_date_window).never - test "#entries is a list of VersionPresenter objects when 'history' is passed in as a 'only' argument" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "history") - expected_actions = %w[updated - editioned - published - submitted - updated - rejected - submitted - created] - - assert_equal expected_actions, timeline.entries.map(&:action) + Document::PaginatedTimeline.new(document: @document, page: 1, only: "internal_notes").entries + end + end end - test "#total_count counts the total VersionPresenter objects when 'history' is passed into the 'only' argument" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "history") - assert_equal 8, timeline.total_count + describe "#total_count" do + it "counts the list of Document::PaginatedTimeline::VersionDecorator and EditorialRemark objects when no 'only' argument is passed in" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + assert_equal 11, timeline.total_count + end + + it "counts the total EditorialRemark objects when 'internal_notes' is passed into the 'only' argument" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "internal_notes") + assert_equal 3, timeline.total_count + end + + it "counts the total Document::PaginatedTimeline::VersionDecorator objects when 'history' is passed into the 'only' argument" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "history") + assert_equal 8, timeline.total_count + end end - test "#only is the 'only' constructor argument" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1) - assert_nil timeline.only + describe "#only" do + it "is the 'only' constructor argument" do + timeline = Document::PaginatedTimeline.new(document: @document, page: 1) + assert_nil timeline.only - timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "history") - assert_equal "history", timeline.only + timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "history") + assert_equal "history", timeline.only - timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "internal_notes") - assert_equal "internal_notes", timeline.only + timeline = Document::PaginatedTimeline.new(document: @document, page: 1, only: "internal_notes") + assert_equal "internal_notes", timeline.only + end end - test "it implements methods required by Kaminari for pagination" do + it "implements methods required by Kaminari for pagination" do results_per_page = 4 expect_total_count = 11 expect_total_pages = 3 @@ -110,56 +263,76 @@ class PaginatedTimelineTest < ActiveSupport::TestCase end end - test "VersionPresenter correctly determines actions" do - mock_pagination(per_page: 30) do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1) - entries = timeline.entries.select { |e| e.instance_of?(Queries::VersionPresenter) } - expected_actions = %w[updated - editioned - published - submitted - updated - rejected - submitted - created] - - assert_equal expected_actions, entries.map(&:action) + describe "filtering entries" do + let(:entries_for_newer_editions) do + 2.times.map do + stub("entry", is_for_newer_edition?: true, is_for_current_edition?: false, is_for_older_edition?: false) + end end - end - test "VersionPresenter correctly determines actors" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1) - entries = timeline.entries.select { |e| e.instance_of?(Queries::VersionPresenter) } - expected_actors = [@user, - @user, - @user2, - @user, - @user, - @user2, - @user] - - assert_equal expected_actors, entries.map(&:actor) - end + let(:entries_for_current_edition) do + 4.times.map do + stub("entry", is_for_newer_edition?: false, is_for_current_edition?: true, is_for_older_edition?: false) + end + end - test "#entries_on_newer_editions returns entries on newer editions than the one passed in" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1) - expected_entries = timeline.entries.slice(1, 2) + let(:entries_for_older_editions) do + 3.times.map do + stub("entry", is_for_newer_edition?: false, is_for_current_edition?: false, is_for_older_edition?: true) + end + end - assert_equal expected_entries, timeline.entries_on_newer_editions(@first_edition) - end + let(:all_entries) do + [*entries_for_newer_editions, *entries_for_current_edition, *entries_for_older_editions] + end - test "#entries_on_current_edition returns entries for the edition passed in" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1) - expected_entries = timeline.entries - timeline.entries.slice(1, 2) + let(:timeline) { Document::PaginatedTimeline.new(document: @document, page: 1) } - assert_equal expected_entries, timeline.entries_on_current_edition(@first_edition) - end + before do + timeline.stubs(:entries).returns(all_entries) + end + + describe "#entries_on_newer_editions" do + it "returns entries on newer editions than the one passed in" do + assert_equal entries_for_newer_editions, timeline.entries_on_newer_editions(@first_edition) + end - test "#entries_on_previous_editions returns entries on previous editions" do - timeline = Document::PaginatedTimeline.new(document: @document, page: 1) - expected_entries = timeline.entries - timeline.entries.slice(1, 2) + it "calls the entries with the expected edition" do + all_entries.each do |entry| + entry.expects(:is_for_newer_edition?).with(@first_edition) + end + + timeline.entries_on_newer_editions(@first_edition) + end + end + + describe "#entries_on_current_edition" do + it "returns entries for the edition passed in" do + assert_equal entries_for_current_edition, timeline.entries_on_current_edition(@first_edition) + end - assert_equal expected_entries, timeline.entries_on_previous_editions(@newest_edition) + it "calls the entries with the expected edition" do + all_entries.each do |entry| + entry.expects(:is_for_current_edition?).with(@first_edition) + end + + timeline.entries_on_current_edition(@first_edition) + end + end + + describe "#entries_on_previous_editions" do + it "returns entries on previous editions" do + assert_equal entries_for_older_editions, timeline.entries_on_previous_editions(@newest_edition) + end + + it "calls the entries with the expected edition" do + all_entries.each do |entry| + entry.expects(:is_for_older_edition?).with(@first_edition) + end + + timeline.entries_on_previous_editions(@first_edition) + end + end end def seed_document_event_history diff --git a/test/unit/app/models/document_test.rb b/test/unit/app/models/document_test.rb index 445de311f04..5af59d62c3f 100644 --- a/test/unit/app/models/document_test.rb +++ b/test/unit/app/models/document_test.rb @@ -595,4 +595,114 @@ class DocumentTest < ActiveSupport::TestCase end end end + + describe "#remarks_by_ids" do + it "returns all the remarks keyed by ID" do + document = create(:document) + edition1 = create(:published_edition, document: document) + edition2 = create(:published_edition, document: document) + edition3 = create(:published_edition, document: document) + + editorial_remark1 = create(:editorial_remark, edition: edition1) + editorial_remark2 = create(:editorial_remark, edition: edition2) + editorial_remark3 = create(:editorial_remark, edition: edition3) + _other_remark = create(:editorial_remark, edition: edition3) + + expected_remarks = { + editorial_remark1.id => editorial_remark1, + editorial_remark2.id => editorial_remark2, + editorial_remark3.id => editorial_remark3, + } + + actual_remarks = document.remarks_by_ids([editorial_remark1.id, editorial_remark2.id, editorial_remark3.id]) + + assert_equal actual_remarks, expected_remarks + end + end + + describe "#decorated_edition_versions_by_ids" do + it "returns all the versions, presented as Document::PaginatedTimeline::VersionDecorator items" do + document = create(:document) + edition1 = create(:published_edition, document: document) + edition2 = create(:published_edition, document: document) + edition3 = create(:published_edition, document: document) + + version1 = create(:version, item: edition1, item_type: "Edition", state: "published") + version2 = create(:version, item: edition2, item_type: "Edition", state: "published") + version3 = create(:version, item: edition3, item_type: "Edition", state: "published") + _excluded_version = create(:version, item: edition3) + _superseded_version = create(:version, item: edition2, state: "superseded") + + versions = [version1, version2, version3] + + version1_stub = stub(id: version1.id) + version2_stub = stub(id: version2.id) + version3_stub = stub(id: version3.id) + + Document::PaginatedTimeline::VersionDecorator.stubs(:new).with { |v, **args| + v.id == version1.id && + args[:is_first_edition] == true && + args[:previous_version] == version3 + }.returns(version1_stub) + + Document::PaginatedTimeline::VersionDecorator.stubs(:new).with { |v, **args| + v.id == version2.id && + args[:is_first_edition] == false && + args[:previous_version] == version1 + }.returns(version2_stub) + + Document::PaginatedTimeline::VersionDecorator.stubs(:new).with { |v, **args| + v.id == version3.id && + args[:is_first_edition] == false && + args[:previous_version] == version2 + }.returns(version3_stub) + + all_versions = document.decorated_edition_versions_by_ids( + versions.map(&:id), + ) + + expected_versions = { + version1.id => version1_stub, + version2.id => version2_stub, + version3.id => version3_stub, + } + + assert_equal all_versions.count, versions.count + assert_equal all_versions.to_h, expected_versions + end + end + + describe "#first_edition_id" do + it "returns the ID of the first edition" do + document = create(:document) + edition1 = create(:published_edition, document: document) + _edition2 = create(:published_edition, document: document) + + assert_equal document.first_edition_id, edition1.id + end + end + + describe "#active_edition_versions" do + it "returns only active edition versions" do + document = create(:document) + edition1 = create(:published_edition, document: document) + edition2 = create(:published_edition, document: document) + + create(:version, item: edition1, item_type: "Edition", state: "published") + create(:version, item: edition1, item_type: "Edition", state: "published") + create(:version, item: edition2, item_type: "Edition", state: "published") + + expected_versions = [ + *edition1.versions, + *edition2.versions, + ] + + _superseded_version = create(:version, item: edition2, state: "superseded") + _document_version = create(:version, item: document) + + actual_versions = document.active_edition_versions + + assert_same_elements expected_versions, actual_versions + end + end end diff --git a/test/unit/app/models/edition_test.rb b/test/unit/app/models/edition_test.rb index b72b28ad28d..1191a6b1f43 100644 --- a/test/unit/app/models/edition_test.rb +++ b/test/unit/app/models/edition_test.rb @@ -997,6 +997,48 @@ class EditionTest < ActiveSupport::TestCase assert_equal false, edition.images_have_unique_filenames? end + test "#superseded_at returns the date an edition was superseded at when a superseded version exists" do + superseded_at = Time.zone.now - 3.days + + edition = build(:edition) + edition.versions = [ + build(:version, state: "published", item: edition), + build(:version, state: "superseded", created_at: superseded_at, item: edition), + ] + + assert_equal edition.superseded_at, superseded_at + end + + test "#superseded_at returns nil when a superseded version does not exist" do + edition = build(:edition) + edition.versions = [ + build(:version, state: "published", item: edition), + ] + + assert_nil edition.superseded_at + end + + test "#published_at returns the date an edition was published at when a published version exists" do + published_at = Time.zone.now - 3.days + + edition = build(:edition) + edition.versions = [ + build(:version, state: "published", created_at: published_at, item: edition), + build(:version, state: "superseded", item: edition), + ] + + assert_equal edition.published_at, published_at + end + + test "#published_at returns nil when a published version does not exist" do + edition = build(:edition) + edition.versions = [ + build(:version, state: "created", item: edition), + ] + + assert_nil edition.published_at + end + def decoded_token_payload(token) payload, _header = JWT.decode( token, diff --git a/test/unit/app/models/editorial_remark_test.rb b/test/unit/app/models/editorial_remark_test.rb index 2c31ad2daca..f6b28d7a646 100644 --- a/test/unit/app/models/editorial_remark_test.rb +++ b/test/unit/app/models/editorial_remark_test.rb @@ -1,18 +1,62 @@ require "test_helper" class EditorialRemarkTest < ActiveSupport::TestCase - test "should be invalid without a edition" do - editorial_remark = build(:editorial_remark, edition: nil) - assert_not editorial_remark.valid? + extend Minitest::Spec::DSL + + let(:edition_id) { 5 } + let(:edition) { build(:edition, id: edition_id) } + let(:editorial_remark) { build(:editorial_remark, edition:) } + + describe "#valid?" do + it "should be invalid without a edition" do + editorial_remark.edition = nil + assert_not editorial_remark.valid? + end + + it "should be invalid without a body" do + editorial_remark.body = nil + assert_not editorial_remark.valid? + end + + it "should be invalid without an author" do + editorial_remark.author = nil + assert_not editorial_remark.valid? + end end - test "should be invalid without a body" do - editorial_remark = build(:editorial_remark, body: nil) - assert_not editorial_remark.valid? + describe "#is_for_newer_edition?" do + it "returns true if the edition_id is less than the comparing edition's id" do + edition_to_compare = build(:edition, id: edition_id - 1) + assert editorial_remark.is_for_newer_edition?(edition_to_compare) + end + + it "returns false if the edition_id is greater than the comparing edition's id" do + edition_to_compare = build(:edition, id: edition_id + 1) + assert_not editorial_remark.is_for_newer_edition?(edition_to_compare) + end end - test "should be invalid without an author" do - editorial_remark = build(:editorial_remark, author: nil) - assert_not editorial_remark.valid? + describe "#is_for_current_edition?" do + it "returns true if the edition_id is equal to the comparing edition's id" do + edition_to_compare = build(:edition, id: edition_id) + assert editorial_remark.is_for_current_edition?(edition_to_compare) + end + + it "returns false if the edition_id is not equal to the comparing edition's id" do + edition_to_compare = build(:edition, id: edition_id + 1) + assert_not editorial_remark.is_for_current_edition?(edition_to_compare) + end + end + + describe "#is_for_older_edition?" do + it "returns true if the edition_id is greater than the comparing edition's id" do + edition_to_compare = build(:edition, id: edition_id + 1) + assert editorial_remark.is_for_older_edition?(edition_to_compare) + end + + it "returns true if the edition_id is less than the comparing edition's id" do + edition_to_compare = build(:edition, id: edition_id - 1) + assert_not editorial_remark.is_for_older_edition?(edition_to_compare) + end end end diff --git a/test/unit/app/models/host_content_update_event_test.rb b/test/unit/app/models/host_content_update_event_test.rb new file mode 100644 index 00000000000..dafa2b696ec --- /dev/null +++ b/test/unit/app/models/host_content_update_event_test.rb @@ -0,0 +1,162 @@ +require "test_helper" + +class HostContentUpdateEventTest < ActiveSupport::TestCase + extend Minitest::Spec::DSL + + let(:document) { create(:document) } + let(:user) { create(:user) } + + describe ".all_for_date_window" do + it "returns all HostContentUpdateJobs" do + from = Time.zone.now - 2.months + to = Time.zone.now - 1.month + + Services.publishing_api.expects(:get_events_for_content_id).with( + document.content_id, { + action: "HostContentUpdateJob", + from:, + to:, + } + ).returns( + [ + { + "id" => 1593, + "action" => "HostContentUpdateJob", + "created_at" => "2024-01-01T00:00:00.000Z", + "updated_at" => "2024-01-01T00:00:00.000Z", + "request_id" => SecureRandom.uuid, + "content_id" => document.content_id, + "payload" => { + "title" => "Host content updated by content block update", + "locale" => "en", + "content_id" => document.content_id, + "source_block" => { + "title" => "An exciting piece of content", + "content_id" => "ef224ae6-7a81-4c59-830b-e9884fe57ec8", + "updated_by_user_uid" => user.uid, + }, + }, + }, + { + "id" => 1593, + "action" => "HostContentUpdateJob", + "user_uid" => SecureRandom.uuid, + "created_at" => "2023-12-01T00:00:00.000Z", + "updated_at" => "2023-12-01T00:00:00.000Z", + "request_id" => SecureRandom.uuid, + "content_id" => document.content_id, + "payload" => { + "title" => "Host content updated by content block update", + "locale" => "en", + "content_id" => document.content_id, + "source_block" => { + "title" => "Another exciting piece of content", + "content_id" => "5c5520ce-6677-4a76-bd6e-4515f46a804e", + "updated_by_user_uid" => nil, + }, + }, + }, + ], + ) + + result = HostContentUpdateEvent.all_for_date_window(document:, from:, to:) + + assert_equal result.count, 2 + + assert_equal result.first.author, user + assert_equal result.first.created_at, Time.zone.parse("2024-01-01T00:00:00.000Z") + assert_equal result.first.content_id, "ef224ae6-7a81-4c59-830b-e9884fe57ec8" + assert_equal result.first.content_title, "An exciting piece of content" + + assert_nil result.second.author + assert_equal result.second.created_at, Time.zone.parse("2023-12-01T00:00:00.000Z") + assert_equal result.second.content_id, "5c5520ce-6677-4a76-bd6e-4515f46a804e" + assert_equal result.second.content_title, "Another exciting piece of content" + end + end + + describe "Timeline helpers" do + let(:created_at) { Time.zone.now - 1.month } + let(:host_content_update_event) { build(:host_content_update_event, created_at:) } + + describe "when the edition is published" do + let(:edition) { build(:edition) } + + before do + edition.stubs(:published_at).returns(created_at - 12.months) + end + + describe "when the event occurred after the edition was superseded" do + before do + edition.stubs(:superseded?).returns(true) + edition.stubs(:superseded_at).returns(created_at - 2.days) + end + + describe "#is_for_newer_edition?" do + it "returns true" do + assert host_content_update_event.is_for_newer_edition?(edition) + end + end + + describe "#is_for_current_edition?" do + it "returns false" do + assert_not host_content_update_event.is_for_current_edition?(edition) + end + end + + describe "#is_for_older_edition?" do + it "returns false" do + assert_not host_content_update_event.is_for_older_edition?(edition) + end + end + end + + describe "when the event occurred before the edition was superseded" do + before do + edition.stubs(:superseded?).returns(true) + edition.stubs(:superseded_at).returns(created_at + 2.days) + end + + describe "#is_for_newer_edition?" do + it "returns false" do + assert_not host_content_update_event.is_for_newer_edition?(edition) + end + end + + describe "#is_for_current_edition?" do + it "returns true" do + assert host_content_update_event.is_for_current_edition?(edition) + end + end + + describe "#is_for_older_edition?" do + it "returns false" do + assert_not host_content_update_event.is_for_older_edition?(edition) + end + end + end + end + + describe "when the edition is draft" do + let(:edition) { build(:edition, :draft) } + + describe "#is_for_newer_edition?" do + it "returns false" do + assert_not host_content_update_event.is_for_newer_edition?(edition) + end + end + + describe "#is_for_current_edition?" do + it "returns false" do + assert_not host_content_update_event.is_for_current_edition?(edition) + end + end + + describe "#is_for_older_edition?" do + it "returns true" do + assert host_content_update_event.is_for_older_edition?(edition) + end + end + end + end +end diff --git a/test/unit/app/presenters/queries/version_presenter_test.rb b/test/unit/app/presenters/queries/version_presenter_test.rb deleted file mode 100644 index c950f35d2c2..00000000000 --- a/test/unit/app/presenters/queries/version_presenter_test.rb +++ /dev/null @@ -1,72 +0,0 @@ -require "test_helper" - -class Queries::VersionPresenterTest < ActiveSupport::TestCase - extend Minitest::Spec::DSL - - let(:user) { build(:user) } - let(:edition) { build(:edition) } - let(:version) { build(:version, user:, item: edition) } - let(:is_first_edition) { false } - let(:previous_version) { build(:version, user:, item: edition) } - - let(:presenter) { Queries::VersionPresenter.new(version, is_first_edition:, previous_version:) } - - describe ".model_name" do - it "returns the model name" do - assert_equal Queries::VersionPresenter.model_name, ActiveModel::Name.new(Version, nil) - end - end - - describe "#actor" do - it "returns the version's user" do - assert_equal presenter.actor, version.user - end - end - - describe "#action" do - context "when the event is `create`" do - let(:version) { build(:version, user:, item: edition, event: "create") } - - it "returns `editioned`" do - assert_equal presenter.action, "editioned" - end - - context "and the edition is the first edition" do - let(:is_first_edition) { true } - - it "returns `created`" do - assert_equal presenter.action, "created" - end - end - end - - context "when the state has not changed" do - let(:version) { build(:version, user:, item: edition, state: "some_state") } - let(:previous_version) { build(:version, user:, item: edition, state: "some_state") } - - it "returns `updated`" do - assert_equal presenter.action, "updated" - end - end - - context "when the state has changed" do - let(:version) { build(:version, user:, item: edition, state: "state2") } - let(:previous_version) { build(:version, user:, item: edition, state: "state1") } - - it "returns the version's state" do - assert_equal presenter.action, version.state - end - end - - context "when previous_version is not preloaded" do - let(:previous_version) { nil } - - it "loads the previous version" do - loaded_version = build(:version, user:, item: edition) - version.expects(:previous).returns(loaded_version) - - presenter.action - end - end - end -end diff --git a/test/unit/app/queries/document/paginated_timeline_query_test.rb b/test/unit/app/queries/document/paginated_timeline_query_test.rb index 4cf48a56ea2..4ff2a0fd2f2 100644 --- a/test/unit/app/queries/document/paginated_timeline_query_test.rb +++ b/test/unit/app/queries/document/paginated_timeline_query_test.rb @@ -63,44 +63,6 @@ class Document::PaginatedTimelineQueryTest < ActiveSupport::TestCase end end - describe "#remarks" do - it "returns all the remarks keyed by ID" do - query = Document::PaginatedTimelineQuery.new(document: @document, page: 1) - expected_remarks = { - @editorial_remark1.id => @editorial_remark1, - @editorial_remark2.id => @editorial_remark2, - @editorial_remark3.id => @editorial_remark3, - } - - assert_equal query.remarks, expected_remarks - end - end - - describe "#versions" do - it "returns all the versions, presented as VersionPresenter items" do - page1_query = Document::PaginatedTimelineQuery.new(document: @document, page: 1) - page2_query = Document::PaginatedTimelineQuery.new(document: @document, page: 2) - - versions = @document.edition_versions.where.not(state: "superseded") - version_presenter_mocks = [] - - versions.each do |version| - version_presenter_mock = mock("Queries::VersionPresenter", id: version.id) - Queries::VersionPresenter.stubs(:new).with { |v, **_args| - v.id == version.id - }.returns(version_presenter_mock) - version_presenter_mocks.push(version_presenter_mock) - end - - all_versions = [*page1_query.versions, *page2_query.versions] - - expected_versions = version_presenter_mocks.index_by(&:id) - - assert_equal all_versions.count, versions.count - assert_equal all_versions.to_h, expected_versions - end - end - def seed_document_event_history acting_as(@user) do @document = create(:document)