From a2d1d36baf9bb680cd573cb5a7cc4697e7876753 Mon Sep 17 00:00:00 2001
From: Jeremy Friesen <jeremy.n.friesen@gmail.com>
Date: Tue, 28 Nov 2023 15:17:22 -0500
Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=81=20=20=20Add=20conditional=20PDF=20?=
 =?UTF-8?q?split=20or=20PDF.js?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This is a preliminary commit to add conditional logic for either
rendering PDF.js or IIIF viewer for split pages.

There is a particular case where QA might appear to fail:

1. Set the Tenant to use UV
2. Create a work with a PDF (this will split the page)
3. Go to the work's page, you'll see the UV
4. Set the Tenant to use PDF.js
5. Refresh the work page, and you should see PDF.js but instead will see
   the UV.

To work around this (and realistically how most people will experience this):

1. Set the Tenant to use UV
2. Create a work with a PDF
3. Go to the Dashboard
4. Click the work's URL
5. You should see a UV for the PDF
6. Close the work's page
7. Change the tenant to use PDF.js
8. Go to the Dashboard
9. Click the work's URL

The conjecture is that there's some iframe and/or turbolinks caching.
Given that this is an edge case regarding toggling on and off a tenant
feature, we think the work around is adequate.

Related to:

- https://github.com/scientist-softserv/palni-palci/issues/666
- https://github.com/scientist-softserv/palni-palci/issues/675
- https://github.com/scientist-softserv/palni-palci/issues/704
- https://github.com/scientist-softserv/palni-palci/issues/705
- https://github.com/scientist-softserv/atla-hyku/issues/162

Co-authored-by: Shana Moore <shana@scientist.com>
Co-authored-by: Kirk Wang <kirk.wang@scientist.com>
---
 Gemfile.lock                                  |  22 +-
 app/helpers/iiif_print_helper.rb              |   5 +
 app/models/concerns/pdf_behavior.rb           |   5 +
 app/presenters/hyku/work_show_presenter.rb    |  25 +--
 app/services/iiif_print/tenant_config.rb      | 202 +++++++++++++++++
 config/application.rb                         |  16 ++
 ..._print_pending_relationships.iiif_print.rb |   8 +
 db/schema.rb                                  |   5 +-
 docker-compose.bundle.yml                     |   9 +
 .../displays_content_decorator_spec.rb        |  15 ++
 .../hyku/work_show_presenter_spec.rb          |  74 ++++++-
 .../services/iiif_print/tenant_config_spec.rb | 208 ++++++++++++++++++
 12 files changed, 566 insertions(+), 28 deletions(-)
 create mode 100644 app/helpers/iiif_print_helper.rb
 create mode 100644 app/services/iiif_print/tenant_config.rb
 create mode 100644 db/migrate/20231128191136_add_model_details_to_iiif_print_pending_relationships.iiif_print.rb
 create mode 100644 docker-compose.bundle.yml
 create mode 100644 spec/presenters/concerns/hyrax/iiif_av/displays_content_decorator_spec.rb
 create mode 100644 spec/services/iiif_print/tenant_config_spec.rb

diff --git a/Gemfile.lock b/Gemfile.lock
index f970628a..adda38e2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -43,17 +43,15 @@ GIT
 
 GIT
   remote: https://github.com/scientist-softserv/iiif_print.git
-  revision: 8fdf56e151b5adb7e6aab1cd29d39b74554ed48a
+  revision: 50b2659f4584ac85f468c7fcec56398f6221a412
   branch: main
   specs:
     iiif_print (1.0.0)
-      blacklight_iiif_search (~> 1.0)
-      dry-monads (~> 1.4.0)
-      hyrax (>= 2.5, < 4.0)
+      blacklight_iiif_search (>= 1.0, < 3.0)
+      derivative-rodeo (~> 0.5)
+      hyrax (>= 2.5, < 6)
       nokogiri (>= 1.13.2)
-      rails (~> 5.0)
       rdf-vocab (~> 3.0)
-      reform-rails (= 0.2.3)
 
 GIT
   remote: https://github.com/tawan/active-elastic-job.git
@@ -313,6 +311,15 @@ GEM
     declarative-option (0.1.0)
     deprecation (1.1.0)
       activesupport
+    derivative-rodeo (0.5.2)
+      activesupport (>= 5)
+      aws-sdk-s3
+      aws-sdk-sqs
+      httparty
+      marcel
+      mime-types
+      mini_magick
+      nokogiri
     devise (4.8.1)
       bcrypt (~> 3.0)
       orm_adapter (~> 0.1)
@@ -469,6 +476,9 @@ GEM
     hiredis (0.6.3)
     htmlentities (4.3.4)
     http_logger (0.7.0)
+    httparty (0.21.0)
+      mini_mime (>= 1.0.0)
+      multi_xml (>= 0.5.2)
     httpclient (2.8.3)
     hydra-access-controls (11.0.7)
       active-fedora (>= 10.0.0)
diff --git a/app/helpers/iiif_print_helper.rb b/app/helpers/iiif_print_helper.rb
new file mode 100644
index 00000000..0c1859cf
--- /dev/null
+++ b/app/helpers/iiif_print_helper.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+module IiifPrintHelper
+  include IiifPrint::IiifPrintHelperBehavior
+end
diff --git a/app/models/concerns/pdf_behavior.rb b/app/models/concerns/pdf_behavior.rb
index ca2ede58..bba7a8af 100644
--- a/app/models/concerns/pdf_behavior.rb
+++ b/app/models/concerns/pdf_behavior.rb
@@ -25,6 +25,11 @@ module PdfBehavior
              end
 
     after_initialize :set_default_show_pdf_viewer, :set_default_show_pdf_download_button
+
+    include IiifPrint.model_configuration(
+      pdf_split_child_model: GenericWork,
+      pdf_splitter_service: IiifPrint::TenantConfig::PdfSplitter
+    )
   end
 
   private
diff --git a/app/presenters/hyku/work_show_presenter.rb b/app/presenters/hyku/work_show_presenter.rb
index 3a3f3e49..21796933 100644
--- a/app/presenters/hyku/work_show_presenter.rb
+++ b/app/presenters/hyku/work_show_presenter.rb
@@ -11,6 +11,12 @@ class WorkShowPresenter < Hyrax::WorkShowPresenter
     include Hyrax::IiifAv::DisplaysIiifAv
     Hyrax::MemberPresenterFactory.file_presenter_class = Hyrax::IiifAv::IiifFileSetPresenter
 
+    ##
+    # NOTE: IIIF Print prepends a IiifPrint::WorkShowPresenterDecorator to Hyrax::WorkShowPresenter
+    # However, with the above `include Hyrax::IiifAv::DisplaysIiifAv` we obliterate that logic.  So
+    # we need to re-introduce that logic.
+    prepend IiifPrint::TenantConfig::WorkShowPresenterDecorator
+
     delegate :title_or_label, :extent, :show_pdf_viewer, :show_pdf_download_button, to: :solr_document
 
     # OVERRIDE Hyrax v2.9.0 here to make featured collections work
@@ -58,15 +64,6 @@ def user_can_feature_collection?
     end
     # End Featured Collections Methods
 
-    # @return [Boolean] render a IIIF viewer
-    def iiif_viewer?
-      Hyrax.config.iiif_image_server? &&
-        representative_id.present? &&
-        representative_presenter.present? &&
-        iiif_media? &&
-        members_include_viewable?
-    end
-
     def video_embed_viewer?
       extract_video_embed_presence
     end
@@ -96,16 +93,6 @@ def extract_video_embed_presence
         solr_document[:video_embed_tesim]&.all?(&:present?)
       end
 
-      def iiif_media?(presenter: representative_presenter)
-        presenter.image? || presenter.video? || presenter.audio?
-      end
-
-      def members_include_viewable?
-        file_set_presenters.any? do |presenter|
-          iiif_media?(presenter: presenter) && current_ability.can?(:read, presenter.id)
-        end
-      end
-
       def extract_from_identifier(rgx)
         if solr_document['identifier_tesim'].present?
           ref = solr_document['identifier_tesim'].map do |str|
diff --git a/app/services/iiif_print/tenant_config.rb b/app/services/iiif_print/tenant_config.rb
new file mode 100644
index 00000000..96bbaf95
--- /dev/null
+++ b/app/services/iiif_print/tenant_config.rb
@@ -0,0 +1,202 @@
+# frozen_string_literal: true
+
+# rubocop:disable Metrics/LineLength
+module IiifPrint
+  ##
+  # This module encapsulates the logic for whether or not we'll use the IIIF Print services for the
+  # current tenant/account.  The IIIF Print services does the following:
+  #
+  # - Skipping IIIF Print based derivative generation
+  # - Skipping PDF Splitting
+  # - Ignoring showing PDFs in the UV
+  #
+  # @note I am specifically isolating as much of this code into one module as possible, so that it
+  #       it is hopefully easier to understand the configuration requirements and scope to this
+  #       change.  At some point, this might make sense to bring into IIIF Print directly.
+  #
+  # @see https://github.com/scientist-softserv/palni-palci/issues/656 palni-palci#656
+  # @see https://github.com/scientist-softserv/palni-palci/issues/657 palni-palci#657
+  # @see https://github.com/scientist-softserv/palni-palci/issues/658 palni-palci#658
+  # @see https://github.com/scientist-softserv/palni-palci/issues/659 palni-palci#659
+  module TenantConfig
+    ##
+    # When we were not planning on calling the underlying IiifPrint service but did due to some kind
+    # of faulty programming logic.
+    #
+    # @note This is raised as a guard to say "Hey, you thought you weren't using IIIF Print but your
+    #       code's logic paths say otherwise."
+    class LeakyAbstractionError < StandardError
+      def initialize(klass:, method_name:)
+        super("Called #{klass}##{method_name} when we had said that #{klass} was not valid because we weren't using IIIF Print")
+      end
+    end
+
+    ##
+    # If the default PDF viewer (PDF.js) is enabled, this method returns false,
+    # meaning the application should not use IIIF Print. If the default viewer is
+    # disabled, this method returns true, meaning the application should use IIIF Print.
+    def self.use_iiif_print?
+      !::Flipflop.default_pdf_viewer?
+    end
+
+    ##
+    # This class implements the interface of the Hyrax::DerivativeService.  It is responsible for
+    # negotiating whether or not the DerivativeService is "on" for the current tenant.
+    #
+    # @see https://github.com/samvera/hyrax/blob/08ef6c9a4fac489972eea9be53403e173f4ffb29/app/services/hyrax/derivative_service.rb Hyrax::DerivativeService
+    class DerivativeService
+      ##
+      # This allows you to specify the IIIF derivative service to use when the tenant has chosen to
+      # use IIIF Print for processing PDFs.
+      #
+      # If you are using the DerivativeRodeo, you'd specify something else.
+      class_attribute :iiif_service_class, default: ::IiifPrint::PluggableDerivativeService
+
+      def initialize(file_set)
+        @file_set = file_set
+      end
+
+      delegate :use_iiif_print?, to: TenantConfig
+
+      def valid?
+        return false unless use_iiif_print?
+
+        iiif_print_service_instance.valid?
+      end
+
+      %i[create_derivatives cleanup_derivatives].each do |method_name|
+        define_method(method_name) do |*args|
+          raise LeakyAbstractionError.new(klass: self.class, method_name: method_name) unless use_iiif_print?
+
+          iiif_print_service_instance.public_send(method_name, *args)
+        end
+      end
+
+      ##
+      # @api private
+      #
+      # @note Public to ease testing.
+      def iiif_print_service_instance
+        @iiif_print_service_instance ||= iiif_service_class.new(@file_set)
+      end
+    end
+
+    ##
+    # This is the pdf_splitter_service that will be used.  If the tenant does not allow PDF splitting
+    # we will return an empty array.
+    #
+    # @example
+    #
+    #  class MyWork
+    #    include IiifPrint.model_configuration(
+    #      pdf_split_child_model: Attachment,
+    #      pdf_splitter_service: IiifPrint::TenantConfig::PdfSplitter,
+    #      derivative_service_plugins: [ IiifPrint::TextExtractionDerivativeService ])
+    #  end
+    #
+    # @see https://github.com/scientist-softserv/iiif_print/blob/9e7837ce4bd08bf8fff9126455d0e0e2602f6018/lib/iiif_print.rb#L86-L138 Documentation for configuring
+    # @see https://github.com/scientist-softserv/adventist-dl/blob/d7676bdac2c672f09b28086d7145b68306978950/app/models/image.rb#L14-L20 Example implementation
+    module PdfSplitter
+      mattr_accessor :iiif_print_splitter
+      self.iiif_print_splitter = ::IiifPrint::SplitPdfs::PagesToJpgsSplitter
+
+      ##
+      # @api public
+      def self.call(*args)
+        return [] unless TenantConfig.use_iiif_print?
+
+        iiif_print_splitter.call(*args)
+      end
+    end
+
+    ##
+    # @see https://github.com/scientist-softserv/iiif_print/blob/9e7837ce4bd08bf8fff9126455d0e0e2602f6018/lib/iiif_print/split_pdfs/child_work_creation_from_pdf_service.rb#L10-L46 Interface of FileSetActor#service
+    module SkipSplittingPdfService
+      ##
+      # @return [Symbol] Always :tenant_does_not_split_pdfs
+      def self.conditionally_enqueue(*_args)
+        :tenant_does_not_split_pdfs
+      end
+    end
+
+    ##
+    # This decorator should ensure that we don't call model configured :pdf_splitter_service as
+    # documented in {TenantConfig::PdfSplitter} and the IIIF Print gem.  It avoids the potentially
+    # expensive conditionally enqueue logic of the super class.
+    #
+    # Why not make an `app/actors/hyrax/actors/file_set_actor_decorator.rb`?  It would be lost in that
+    # it is decorating the decoration of the IIIF Print gem.  Beside, in bringing this here, we have
+    # a relatively singular place for all of the configurations.
+    module FileSetActorDecorator
+      ##
+      # @see https://github.com/scientist-softserv/iiif_print/blob/9e7837ce4bd08bf8fff9126455d0e0e2602f6018/app/actors/iiif_print/actors/file_set_actor_decorator.rb#L33-L35 Method we're overriding
+      def service
+        return TenantConfig::SkipSplittingPdfService unless TenantConfig.use_iiif_print?
+
+        super
+      end
+    end
+
+    ##
+    # OVERRIDE IiifPrint::WorkShowPresenterDecorator
+    # OVERRIDE Hyrax::WorkShowPresenter
+    #
+    # In IiifPrint we overrided #members_include_viewable_image? to query for both file sets and
+    # child works.  (Child works being the pages split off of a PDF)
+    #
+    # In Hyrax::WorkShowPresenter we're only looking at the underlying file_sets.  But IiifPrint
+    # needs to look at multiple places.
+    module WorkShowPresenterDecorator
+      ##
+      # @return [Array<Symbol>] predicate methods (e.g. ending in "?") that reflect the types
+      #         of files we want to consider for showing in the IIIF Viewer.
+      def iiif_media_predicates
+        if TenantConfig.use_iiif_print?
+          %i[image? audio? video? pdf?]
+        else
+          %i[image? audio? video?]
+        end
+      end
+
+      def iiif_media?(presenter: representative_presenter)
+        iiif_media_predicates.any? { |predicate| presenter.try(predicate) || presenter.try(:solr_document).try(predicate) }
+      end
+
+      ##
+      # @return [Boolean] render a IIIF viewer
+      #
+      # OVERRIDE Hyrax::WorkShowPresenter; this override introduces behavior to handle over-rides.
+      def iiif_viewer?
+        Hyrax.config.iiif_image_server? &&
+          representative_id.present? &&
+          representative_presenter.present? &&
+          iiif_media? &&
+          members_include_iiif_viewable?
+      end
+
+      def members_include_iiif_viewable?
+        iiif_presentable_member_presenters.any? do |presenter|
+          iiif_media?(presenter: presenter) && current_ability.can?(:read, presenter.id)
+        end
+      end
+
+      ##
+      # @return [Array<Object>] An array of presenter objects
+      #
+      # In a non-IIIF Print using scenario, we use the file_set_presenters value; that is for
+      # objects that are very specifically file_sets.
+      #
+      # In a IIIF Print using scenario, we use the ill-named 'file_set_ids_ssim', because a
+      # long-standing decision is that this field will have both file_set IDs and child work IDs.
+      def iiif_presentable_member_presenters
+        if TenantConfig.use_iiif_print?
+          presentable_member_ids = Array.wrap(solr_document.try(:file_set_ids) || solr_document.try(:[], 'file_set_ids_ssim'))
+          member_presenters_for(presentable_member_ids)
+        else
+          file_set_presenters
+        end
+      end
+    end
+  end
+end
+# rubocop:enable Metrics/LineLength
diff --git a/config/application.rb b/config/application.rb
index f858fd46..80c8c780 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -60,5 +60,21 @@ class Application < Rails::Application
 
       Object.include(AccountSwitch)
     end
+
+    config.after_initialize do
+      ##
+      # The first "#valid?" service is the one that we'll use for generating derivatives.
+      Hyrax::DerivativeService.services = [
+        IiifPrint::TenantConfig::DerivativeService,
+        Hyrax::FileSetDerivativesService
+      ]
+
+      ##
+      # This needs to be in the after initialize so that the IiifPrint gem can do it's decoration.
+      #
+      # @see https://github.com/scientist-softserv/iiif_print/blob/9e7837ce4bd08bf8fff9126455d0e0e2602f6018/lib/iiif_print/engine.rb#L54 Where we do the override.
+      Hyrax::Actors::FileSetActor.prepend(IiifPrint::TenantConfig::FileSetActorDecorator)
+      Hyrax::WorkShowPresenter.prepend(IiifPrint::TenantConfig::WorkShowPresenterDecorator)
+    end
   end
 end
diff --git a/db/migrate/20231128191136_add_model_details_to_iiif_print_pending_relationships.iiif_print.rb b/db/migrate/20231128191136_add_model_details_to_iiif_print_pending_relationships.iiif_print.rb
new file mode 100644
index 00000000..23437715
--- /dev/null
+++ b/db/migrate/20231128191136_add_model_details_to_iiif_print_pending_relationships.iiif_print.rb
@@ -0,0 +1,8 @@
+# This migration comes from iiif_print (originally 20231110163052)
+class AddModelDetailsToIiifPrintPendingRelationships < ActiveRecord::Migration[5.2]
+  def change
+    add_column :iiif_print_pending_relationships, :parent_model, :string
+    add_column :iiif_print_pending_relationships, :child_model, :string
+    add_column :iiif_print_pending_relationships, :file_id, :string
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index efdb5619..31243d54 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2023_06_08_153601) do
+ActiveRecord::Schema.define(version: 2023_11_28_191136) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -382,6 +382,9 @@
     t.string "child_order", null: false
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
+    t.string "parent_model"
+    t.string "child_model"
+    t.string "file_id"
     t.index ["parent_id"], name: "index_iiif_print_pending_relationships_on_parent_id"
   end
 
diff --git a/docker-compose.bundle.yml b/docker-compose.bundle.yml
new file mode 100644
index 00000000..d3f73cdf
--- /dev/null
+++ b/docker-compose.bundle.yml
@@ -0,0 +1,9 @@
+# Copy file to docker-compose.override.yml to override docker-compose.yml
+# Only use for local development
+version: '3.8'
+services:
+  web:
+    command: sh -l -c "bundle && bundle exec puma -v -b tcp://0.0.0.0:3000"
+  worker:
+    command: sh -l -c "bundle && bundle exec sidekiq"
+
diff --git a/spec/presenters/concerns/hyrax/iiif_av/displays_content_decorator_spec.rb b/spec/presenters/concerns/hyrax/iiif_av/displays_content_decorator_spec.rb
new file mode 100644
index 00000000..1ede624a
--- /dev/null
+++ b/spec/presenters/concerns/hyrax/iiif_av/displays_content_decorator_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Hyrax::IiifAv::DisplaysContentDecorator do
+  # We're prepending the DisplaysContentDecorator to the Hyrax::IiifAv::DisplaysContent
+  describe Hyrax::IiifAv::DisplaysContent do
+    describe '.public_instance_methods' do
+      subject { Hyrax::IiifAv::DisplaysContent.public_instance_methods }
+
+      it { is_expected.to include(:solr_document) }
+      it { is_expected.to include(:current_ability) }
+    end
+  end
+end
diff --git a/spec/presenters/hyku/work_show_presenter_spec.rb b/spec/presenters/hyku/work_show_presenter_spec.rb
index 75c948f6..5985071c 100644
--- a/spec/presenters/hyku/work_show_presenter_spec.rb
+++ b/spec/presenters/hyku/work_show_presenter_spec.rb
@@ -22,10 +22,36 @@
     before do
       allow(solr_document).to receive(:representative_id).and_return(solr_document.member_ids.first)
       allow(ability).to receive(:can?).and_return true
-      allow_any_instance_of(Hyrax::IiifAv::IiifFileSetPresenter).to receive(:image?).and_return true
+      allow_any_instance_of(Hyrax::IiifAv::IiifFileSetPresenter).to receive(:image?).and_return false
+      allow_any_instance_of(Hyrax::IiifAv::IiifFileSetPresenter).to receive(:pdf?).and_return false
+      allow_any_instance_of(Hyrax::IiifAv::IiifFileSetPresenter).to receive(:video?).and_return false
+      allow_any_instance_of(Hyrax::IiifAv::IiifFileSetPresenter).to receive(:audio?).and_return false
     end
 
-    it { is_expected.to be true }
+    context 'method owner' do
+      # I was noticing load logic issues, so I'm adding this spec for verification
+      subject { presenter.method(:iiif_viewer?).owner }
+
+      it { is_expected.to eq(IiifPrint::TenantConfig::WorkShowPresenterDecorator) }
+    end
+
+    context "for a PDF file" do
+      let!(:test_strategy) { Flipflop::FeatureSet.current.test! }
+
+      before { allow_any_instance_of(Hyrax::IiifAv::IiifFileSetPresenter).to receive(:pdf?).and_return true }
+
+      context 'when the tenant is not configured to use IIIF Print' do
+        before { test_strategy.switch!(:default_pdf_viewer, true) }
+
+        it { is_expected.to be false }
+      end
+
+      context 'when the tenant is configured to use IIIF Print' do
+        before { test_strategy.switch!(:default_pdf_viewer, false) }
+
+        it { is_expected.to be true }
+      end
+    end
 
     context "for an audio file" do
       before do
@@ -35,6 +61,14 @@
       it { is_expected.to be true }
     end
 
+    context "for an image file" do
+      before do
+        allow_any_instance_of(Hyrax::IiifAv::IiifFileSetPresenter).to receive(:image?).and_return true
+      end
+
+      it { is_expected.to be true }
+    end
+
     context "for a video file" do
       before do
         allow_any_instance_of(Hyrax::IiifAv::IiifFileSetPresenter).to receive(:video?).and_return true
@@ -120,5 +154,41 @@
         expect(presenter.isbns).to be_empty
       end
     end
+
+    describe "#parent_works" do
+      let(:public_doc) { double(SolrDocument, public?: true) }
+      let(:non_public_doc) { double(SolrDocument, public?: false) }
+      let(:parent_docs) { [public_doc, non_public_doc] }
+      let(:current_user) { double(User, ability: double) }
+
+      before do
+        allow(solr_document).to receive(:load_parent_docs).and_return(parent_docs)
+      end
+
+      it 'returns the parent works of the solr document' do
+        parent_docs.each do |doc|
+          allow(doc).to receive(:public?).and_return(true) # Assumes all parent docs are public
+        end
+
+        expect(presenter.parent_works).to eq(parent_docs)
+      end
+
+      context 'when a public doc is not public' do
+        it 'excludes non-public documents' do
+          allow(non_public_doc).to receive(:public?).and_return(false)
+
+          expect(presenter.parent_works).to eq([public_doc])
+        end
+      end
+
+      context 'with a current user and their ability' do
+        it 'filters based on user ability' do
+          allow(current_user.ability).to receive(:can?).with(:read, public_doc).and_return(false)
+          allow(current_user.ability).to receive(:can?).with(:read, non_public_doc).and_return(true)
+
+          expect(presenter.parent_works(current_user)).to eq([non_public_doc])
+        end
+      end
+    end
   end
 end
diff --git a/spec/services/iiif_print/tenant_config_spec.rb b/spec/services/iiif_print/tenant_config_spec.rb
new file mode 100644
index 00000000..148ec31d
--- /dev/null
+++ b/spec/services/iiif_print/tenant_config_spec.rb
@@ -0,0 +1,208 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+##
+# Yes good reader this is a lot of modules that have specs; however, they are a cohesive and atomic
+# unit.  That is to say they each build towards the feature of "whether or not we use IIIF Print".
+#
+# So instead of sprinkling these all around, I opted to compartmentalize them in the
+# IiifPrint::TenantConfig module.
+#
+# rubocop:disable RSpec/DescribeClass
+RSpec.describe 'Tenant Config for IIIF Print' do
+  let!(:test_strategy) { Flipflop::FeatureSet.current.test! }
+
+  describe IiifPrint::TenantConfig do
+    describe '.use_iiif_print?' do
+      subject { described_class.use_iiif_print? }
+
+      context 'by default' do
+        it { is_expected.to be_falsey }
+      end
+
+      context 'when the feature is flipped to false' do
+        before { test_strategy.switch!(:default_pdf_viewer, true) }
+
+        it { is_expected.to be_falsey }
+      end
+
+      context 'when the feature is flipped to true' do
+        before { test_strategy.switch!(:default_pdf_viewer, false) }
+
+        it { is_expected.to be_truthy }
+      end
+    end
+  end
+
+  describe IiifPrint::TenantConfig::DerivativeService do
+    let(:fake_service_class) do
+      # Creating a class that inherits from the configured service but doesn't do all the antics of
+      # that service.  For testing and implementation purposes we must assume the underlying thing
+      # works as intended, but are instead testing that we're properly calling things.
+      Class.new(described_class.iiif_service_class) do
+        def initialize(file_set); end
+      end
+    end
+
+    let(:file_set) { double(FileSet) }
+
+    let(:instance) { described_class.new(file_set).tap { |i| i.iiif_service_class = fake_service_class } }
+
+    describe '#iiif_service_class' do
+      subject { described_class.iiif_service_class }
+
+      it { is_expected.to eq(::IiifPrint::PluggableDerivativeService) }
+    end
+
+    describe '#valid?' do
+      subject { instance.valid? }
+
+      context 'when the feature is flipped to false' do
+        before { test_strategy.switch!(:default_pdf_viewer, true) }
+
+        it { is_expected.to be_falsey }
+      end
+
+      context 'when the feature is flipped to true' do
+        before { test_strategy.switch!(:default_pdf_viewer, false) }
+
+        it 'delegates to the configured iiif_service' do
+          expect(instance.iiif_print_service_instance).to receive(:valid?)
+          subject
+        end
+      end
+    end
+
+    describe '#create_derivatives' do
+      subject { instance.create_derivatives("filename") }
+
+      context 'when the feature is flipped to false' do
+        before { test_strategy.switch!(:default_pdf_viewer, true) }
+
+        it 'raises an error' do
+          expect { subject }.to raise_error(IiifPrint::TenantConfig::LeakyAbstractionError)
+        end
+      end
+
+      context 'when the feature is flipped to true' do
+        before { test_strategy.switch!(:default_pdf_viewer, false) }
+        it 'delegates to the configured iiif_service' do
+          expect(instance.iiif_print_service_instance).to receive(:create_derivatives)
+          subject
+        end
+      end
+    end
+
+    describe '#cleanup_derivatives' do
+      subject { instance.cleanup_derivatives }
+
+      context 'when the feature is flipped to false' do
+        before { test_strategy.switch!(:default_pdf_viewer, true) }
+
+        it 'raises an error' do
+          expect { subject }.to raise_error(IiifPrint::TenantConfig::LeakyAbstractionError)
+        end
+      end
+
+      context 'when the feature is flipped to true' do
+        before { test_strategy.switch!(:default_pdf_viewer, false) }
+        it 'delegates to the configured iiif_service' do
+          expect(instance.iiif_print_service_instance).to receive(:cleanup_derivatives)
+          subject
+        end
+      end
+    end
+  end
+
+  describe IiifPrint::TenantConfig::PdfSplitter do
+    describe '.iiif_print_splitter' do
+      subject { described_class.iiif_print_splitter }
+
+      it { is_expected.to eq(::IiifPrint::SplitPdfs::PagesToJpgsSplitter) }
+    end
+
+    describe '.call' do
+      subject { described_class.call(:arg) }
+
+      context 'when the feature is flipped to false' do
+        before { test_strategy.switch!(:default_pdf_viewer, true) }
+
+        it { is_expected.to eq([]) }
+      end
+
+      context 'when the feature is flipped to true' do
+        before { test_strategy.switch!(:default_pdf_viewer, false) }
+
+        it 'delegates to the configured .iiif_print_splitter' do
+          expect(described_class.iiif_print_splitter).to receive(:call).with(:arg)
+          subject
+        end
+      end
+    end
+  end
+
+  describe IiifPrint::TenantConfig::SkipSplittingPdfService do
+    describe '.conditionally_enqueue' do
+      subject { described_class.conditionally_enqueue }
+
+      it { is_expected.to eq(:tenant_does_not_split_pdfs) }
+    end
+  end
+
+  describe Hyrax::Actors::FileSetActor do
+    let(:instance) { described_class.new(:file_set, :user) }
+
+    ##
+    # The purpose of this spec is to demonstrate that load sequence of modules correctly evaluates
+    describe '#service' do
+      subject { instance.service }
+
+      context 'when the feature is flipped to false' do
+        before { test_strategy.switch!(:default_pdf_viewer, true) }
+
+        it { is_expected.to eq(IiifPrint::TenantConfig::SkipSplittingPdfService) }
+      end
+
+      context 'when the feature is flipped to true' do
+        before { test_strategy.switch!(:default_pdf_viewer, false) }
+
+        it { is_expected.to eq(IiifPrint::SplitPdfs::ChildWorkCreationFromPdfService) }
+      end
+    end
+  end
+
+  ##
+  # Much like the Hyrax::Actors::FileSetActor, we need to ensure that we've registered derivatives
+  # in the correct manner.
+  #
+  # see config/application.rb
+  describe Hyrax::DerivativeService do
+    describe '.services' do
+      subject { described_class.services }
+
+      it { is_expected.to match_array([IiifPrint::TenantConfig::DerivativeService, Hyrax::FileSetDerivativesService]) }
+    end
+  end
+
+  describe Hyrax::WorkShowPresenter do
+    let(:instance) { described_class.new(:solr_doc, :ability) }
+
+    describe '#iiif_media_predicates' do
+      subject { instance.iiif_media_predicates }
+
+      context 'when the feature is flipped to false' do
+        before { test_strategy.switch!(:default_pdf_viewer, true) }
+
+        it { is_expected.to eq(%i[image? audio? video?]) }
+      end
+
+      context 'when the feature is flipped to true' do
+        before { test_strategy.switch!(:default_pdf_viewer, false) }
+
+        it { is_expected.to eq(%i[image? audio? video? pdf?]) }
+      end
+    end
+  end
+end
+# rubocop:enable RSpec/DescribeClass