diff --git a/app/models/apple/config.rb b/app/models/apple/config.rb index d37affa66..b3402e9a2 100644 --- a/app/models/apple/config.rb +++ b/app/models/apple/config.rb @@ -2,10 +2,6 @@ module Apple class Config < ApplicationRecord - DEFAULT_FEED_SLUG = "apple-delegated-delivery-subscriptions" - DEFAULT_TITLE = "Apple Delegated Delivery Subscriptions" - DEFAULT_AUDIO_FORMAT = {"f" => "flac", "b" => 16, "c" => 2, "s" => 44100}.freeze - belongs_to :feed belongs_to :key, class_name: "Apple::Key", optional: true, validate: true, autosave: true @@ -24,27 +20,9 @@ class Config < ApplicationRecord delegate :public_feed, to: :podcast, allow_nil: true alias_method :private_feed, :feed - def self.find_or_build_private_feed(podcast) - if (existing = podcast.feeds.find_by(slug: DEFAULT_FEED_SLUG, title: DEFAULT_TITLE)) - # TODO, handle partitions on apple models via the apple_config - # Until then it's not safe to have multiple apple_configs for the same podcast - Rails.logger.error("Found existing private feed for #{podcast.title}!") - Rails.logger.error("Do you want to continue? (y/N)") - raise "Stopping find_or_build_private_feed" if $stdin.gets.chomp.downcase != "y" - - return existing - end - default_feed = podcast.default_feed - - Feed.new( - display_episodes_count: default_feed.display_episodes_count, - slug: DEFAULT_FEED_SLUG, - title: DEFAULT_TITLE, - audio_format: DEFAULT_AUDIO_FORMAT, - include_zones: ["billboard", "sonic_id"], - tokens: [FeedToken.new(label: DEFAULT_TITLE)], - podcast: podcast - ) + def self.find_or_build_apple_feed(podcast) + existing_feed = Feeds::AppleSubscription.find_by_podcast_id(podcast.id) + existing_feed.present? ? existing_feed : Feeds::AppleSubscription.new(podcast_id: podcast.id) end # TODO: this a helper for onboarding via console, retrofit when the UX catches up @@ -55,7 +33,7 @@ def self.build_apple_config(podcast, key) raise "Stopping build_apple_config" if $stdin.gets.chomp.downcase != "y" end - Apple::Config.new(feed: find_or_build_private_feed(podcast), key: key) + Apple::Config.new(feed: find_or_build_apple_feed(podcast), key: key) end def self.mark_as_delivered!(apple_publisher) diff --git a/app/models/feed.rb b/app/models/feed.rb index 1b0ccdd4d..8ea3eecf1 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -25,8 +25,6 @@ class Feed < ApplicationRecord alias_attribute :tokens, :feed_tokens accepts_nested_attributes_for :feed_tokens, allow_destroy: true, reject_if: ->(ft) { ft[:token].blank? } - has_one :apple_config, class_name: "::Apple::Config", dependent: :destroy, autosave: true, validate: true - has_many :feed_images, -> { order("created_at DESC") }, autosave: true, dependent: :destroy, inverse_of: :feed has_many :itunes_images, -> { order("created_at DESC") }, autosave: true, dependent: :destroy, inverse_of: :feed has_many :itunes_categories, validate: true, autosave: true, dependent: :destroy @@ -167,7 +165,7 @@ def normalize_category(cat) end def publish_to_apple? - !!apple_config&.publish_to_apple? + false end def include_tags=(tags) diff --git a/app/models/feeds/apple_subscription.rb b/app/models/feeds/apple_subscription.rb new file mode 100644 index 000000000..7aa74be0f --- /dev/null +++ b/app/models/feeds/apple_subscription.rb @@ -0,0 +1,72 @@ +class Feeds::AppleSubscription < Feed + DEFAULT_FEED_SLUG = "apple-delegated-delivery-subscriptions" + DEFAULT_TITLE = "Apple Delegated Delivery Subscriptions" + DEFAULT_AUDIO_FORMAT = {"f" => "flac", "b" => 16, "c" => 2, "s" => 44100}.freeze + DEFAULT_ZONES = ["billboard", "sonic_id"] + DEFAULT_TOKENS = [FeedToken.new(label: DEFAULT_TITLE)] + + after_initialize :set_defaults + + has_one :apple_config, class_name: "::Apple::Config", dependent: :destroy, autosave: true, validate: true + has_one :key, + through: :apple_config, + class_name: "Apple::Key", + dependent: :destroy, + foreign_key: :key_id + + accepts_nested_attributes_for :apple_config, allow_destroy: true, reject_if: :all_blank + accepts_nested_attributes_for :key, allow_destroy: true, reject_if: :all_blank + + validate :unchanged_defaults + validate :only_apple_feed + validate :must_be_private + + def set_defaults + self.slug ||= DEFAULT_FEED_SLUG + self.title ||= DEFAULT_TITLE + self.audio_format ||= DEFAULT_AUDIO_FORMAT + self.display_episodes_count ||= podcast&.default_feed&.display_episodes_count + self.include_zones ||= DEFAULT_ZONES + self.tokens ||= DEFAULT_TOKENS + + super + end + + def self.model_name + Feed.model_name + end + + def unchanged_defaults + return unless persisted? + + if title_changed? + errors.add(:title, "cannot change once set") + end + if slug_changed? + errors.add(:slug, "cannot change once set") + end + if file_name_changed? + errors.add(:file_name, "cannot change once set") + end + if audio_format_changed? + errors.add(:audio_format, "cannot change once set") + end + end + + def only_apple_feed + existing_feed = Feeds::AppleSubscription.where(podcast_id: podcast_id).where.not(id: id) + if existing_feed.any? + errors.add(:podcast, "cannot have more than one apple subscription") + end + end + + def must_be_private + if private != true + errors.add(:private, "must be a private feed") + end + end + + def publish_to_apple? + !!apple_config&.publish_to_apple? + end +end diff --git a/db/migrate/20240502172959_add_type_to_feeds.rb b/db/migrate/20240502172959_add_type_to_feeds.rb new file mode 100644 index 000000000..1149d5fb3 --- /dev/null +++ b/db/migrate/20240502172959_add_type_to_feeds.rb @@ -0,0 +1,7 @@ +class AddTypeToFeeds < ActiveRecord::Migration[7.0] + def change + add_column :feeds, :type, :string + + Feed.where(id: Apple::Config.select(:feed_id)).update_all(type: "Feeds::AppleSubscription") + end +end diff --git a/db/schema.rb b/db/schema.rb index f43b108cc..f3a2348ee 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[7.0].define(version: 2024_04_24_214558) do +ActiveRecord::Schema[7.0].define(version: 2024_05_02_172959) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" enable_extension "uuid-ossp" @@ -151,8 +151,8 @@ t.string "feedburner_orig_link" t.string "feedburner_orig_enclosure_link" t.boolean "is_perma_link" - t.datetime "source_updated_at", precision: nil t.string "keyword_xid" + t.datetime "source_updated_at", precision: nil t.integer "season_number" t.integer "episode_number" t.string "itunes_type", default: "full" @@ -229,6 +229,7 @@ t.boolean "include_donation_url", default: true t.text "exclude_tags" t.datetime "deleted_at", precision: nil + t.string "type" t.integer "lock_version", default: 0, null: false t.index ["podcast_id", "slug"], name: "index_feeds_on_podcast_id_and_slug", unique: true, where: "(slug IS NOT NULL)" t.index ["podcast_id"], name: "index_feeds_on_podcast_id" diff --git a/test/factories/feed_factory.rb b/test/factories/feed_factory.rb index cc1a947cd..ce70babe9 100644 --- a/test/factories/feed_factory.rb +++ b/test/factories/feed_factory.rb @@ -44,5 +44,15 @@ factory :public_feed do private { false } end + + factory :apple_feed, class: "Feeds::AppleSubscription" do + type { "Feeds::AppleSubscription" } + private { true } + tokens { [FeedToken.new(label: "apple-private")] } + + after(:build) do |feed, _evaluator| + feed.apple_config = build(:apple_config) + end + end end end diff --git a/test/jobs/publish_apple_job_test.rb b/test/jobs/publish_apple_job_test.rb index 8fd396277..c5d6c7b10 100644 --- a/test/jobs/publish_apple_job_test.rb +++ b/test/jobs/publish_apple_job_test.rb @@ -4,24 +4,23 @@ let(:episode) { create(:episode, prx_uri: "/api/v1/stories/87683") } let(:podcast) { episode.podcast } let(:feed) { podcast.default_feed } - let(:private_feed) { create(:private_feed, podcast: podcast) } - let(:apple_config) { create(:apple_config, feed: private_feed) } + let(:apple_feed) { create(:apple_feed, podcast: podcast) } describe "publishing to apple" do it "does not publish to apple unless publish_enabled?" do - apple_config.update(publish_enabled: false) + apple_feed.apple_config.update(publish_enabled: false) # test that the `publish_to_apple` method is not called PublishAppleJob.stub(:publish_to_apple, ->(x) { raise "should not be called" }) do - assert_nil PublishAppleJob.perform_now(apple_config) + assert_nil PublishAppleJob.perform_now(apple_feed.apple_config) end end it "does publish to apple if publish_enabled?" do - apple_config.update(publish_enabled: true) + apple_feed.apple_config.update(publish_enabled: true) PublishAppleJob.stub(:publish_to_apple, :it_published!) do - assert_equal :it_published!, PublishAppleJob.perform_now(apple_config) + assert_equal :it_published!, PublishAppleJob.perform_now(apple_feed.apple_config) end end end diff --git a/test/jobs/publish_feed_job_test.rb b/test/jobs/publish_feed_job_test.rb index a49885157..5ac462022 100644 --- a/test/jobs/publish_feed_job_test.rb +++ b/test/jobs/publish_feed_job_test.rb @@ -81,41 +81,41 @@ end describe "publishing to apple" do - it "does not schedule publishing to apple if there is no apple config" do - assert_nil private_feed.apple_config + it "does not schedule publishing to apple if the feed is non-apple" do assert_nil job.publish_apple(podcast, private_feed) end describe "when the apple config is present" do - let(:apple_config) { create(:apple_config, feed: private_feed) } + let(:apple_feed) { create(:apple_feed, podcast: podcast) } it "does not schedule publishing to apple if the config is marked as not publishable" do - apple_config.update!(publish_enabled: false) - assert_equal apple_config, private_feed.apple_config.reload - assert_nil job.publish_apple(podcast, private_feed) + apple_feed.apple_config.update!(publish_enabled: false) + assert_nil job.publish_apple(podcast, apple_feed) end it "does run the apple publishing if the config is present and marked as publishable" do - assert_equal apple_config, private_feed.apple_config.reload + assert apple_feed.apple_config.present? + assert apple_feed.apple_config.publish_enabled PublishAppleJob.stub(:perform_now, :publishing_apple!) do - assert_equal :publishing_apple!, job.publish_apple(podcast, private_feed) + assert_equal :publishing_apple!, job.publish_apple(podcast, apple_feed) end end it "Performs the apple publishing job based regardless of sync_blocks_rss flag" do - assert_equal apple_config, private_feed.apple_config.reload + assert apple_feed.apple_config.present? + assert apple_feed.apple_config.publish_enabled # stub the two possible ways the job can be called # perform_later is not used. PublishAppleJob.stub(:perform_later, :perform_later) do PublishAppleJob.stub(:perform_now, :perform_now) do - apple_config.update!(sync_blocks_rss: true) + apple_feed.apple_config.update!(sync_blocks_rss: true) - assert_equal :perform_now, job.publish_apple(podcast, private_feed) + assert_equal :perform_now, job.publish_apple(podcast, apple_feed) - apple_config.update!(sync_blocks_rss: false) + apple_feed.apple_config.update!(sync_blocks_rss: false) feed.reload - assert_equal :perform_now, job.publish_apple(podcast, private_feed) + assert_equal :perform_now, job.publish_apple(podcast, apple_feed) end end end @@ -128,26 +128,28 @@ PublishingPipelineState.start!(feed.podcast) end it "raises an error if the apple publishing fails" do - assert_equal apple_config, private_feed.apple_config.reload + assert apple_feed.apple_config.present? + assert apple_feed.apple_config.publish_enabled PublishAppleJob.stub(:perform_now, ->(*, **) { raise "some apple error" }) do # it raises - assert_raises(RuntimeError) { job.publish_apple(podcast, private_feed) } + assert_raises(RuntimeError) { job.publish_apple(podcast, apple_feed) } assert_equal ["created", "started", "error_apple"].sort, PublishingPipelineState.where(podcast: feed.podcast).latest_pipelines.pluck(:status).sort end end it "does not raise an error if the apple publishing is not blocking RSS" do - assert_equal apple_config, private_feed.apple_config.reload - private_feed.apple_config.update!(sync_blocks_rss: false) + assert apple_feed.apple_config.present? + assert apple_feed.apple_config.publish_enabled + apple_feed.apple_config.update!(sync_blocks_rss: false) mock = Minitest::Mock.new mock.expect(:call, nil, [RuntimeError]) PublishAppleJob.stub(:perform_now, ->(*, **) { raise "some apple error" }) do NewRelic::Agent.stub(:notice_error, mock) do - job.publish_apple(podcast, private_feed) + job.publish_apple(podcast, apple_feed) end end assert_equal ["created", "started", "error_apple"].sort, PublishingPipelineState.where(podcast: feed.podcast).latest_pipelines.pluck(:status).sort diff --git a/test/models/feed/apple_subscription_test.rb b/test/models/feed/apple_subscription_test.rb new file mode 100644 index 000000000..14f285c1a --- /dev/null +++ b/test/models/feed/apple_subscription_test.rb @@ -0,0 +1,82 @@ +require "test_helper" + +describe Feeds::AppleSubscription do + let(:podcast) { create(:podcast) } + let(:feed_1) { podcast.default_feed } + let(:apple_feed) { build(:apple_feed, podcast: podcast) } + + describe "#valid?" do + it "cannot change the default properties once saved" do + apple_feed.title = "new apple feed" + apple_feed.slug = "new-apple-slug" + apple_feed.file_name = "new_file.xml" + apple_feed.audio_format = {f: "flac", b: 16, c: 2, s: 44100} + assert apple_feed.valid? + apple_feed.save! + + apple_feed.title = "changed apple feed" + refute apple_feed.valid? + apple_feed.title = "new apple feed" + assert apple_feed.valid? + + apple_feed.slug = "changed-apple-slug" + refute apple_feed.valid? + apple_feed.slug = "new-apple-slug" + assert apple_feed.valid? + + apple_feed.file_name = "changed_file_name.xml" + refute apple_feed.valid? + apple_feed.file_name = "new_file.xml" + assert apple_feed.valid? + + apple_feed.audio_format = {f: "wav", b: 128, c: 2, s: 44100} + refute apple_feed.valid? + apple_feed.audio_format = {f: "flac", b: 16, c: 2, s: 44100} + assert apple_feed.valid? + end + + it "cannot have more than one apple feed on a single podcast" do + second_apple = build(:apple_feed, podcast: podcast) + assert second_apple.valid? + + apple_feed.save! + assert apple_feed.valid? + refute second_apple.valid? + end + + it "must be a private feed" do + apple_feed.private = false + refute apple_feed.valid? + end + end + + describe "#apple_configs" do + it "has apple credentials" do + assert apple_feed.apple_config.present? + assert apple_feed.apple_config.valid? + + apple_feed.save! + assert_equal feed_1, apple_feed.apple_config.public_feed + end + end + + describe "#publish_to_apple?" do + it "returns true if the feed has apple credentials" do + apple_feed.save! + + refute feed_1.publish_to_apple? + assert apple_feed.publish_to_apple? + end + + it "returns false if the creds are not marked publish_enabled?" do + apple_feed.apple_config.publish_enabled = false + apple_feed.save! + refute apple_feed.publish_to_apple? + end + + it "returns false if the feed is not an Apple Subscription feed" do + refute_equal feed_1.type, "Feeds::AppleSubscription" + refute feed_1.publish_to_apple? + end + end +end diff --git a/test/models/feed_test.rb b/test/models/feed_test.rb index a20d76191..e442e634c 100644 --- a/test/models/feed_test.rb +++ b/test/models/feed_test.rb @@ -324,28 +324,9 @@ end end - describe "#apple_configs" do - it "has apple credentials" do - creds = create(:apple_config, feed: feed2) - assert_equal creds, feed2.apple_config - assert_equal feed1, feed2.apple_config.public_feed - end - end - describe "#publish_to_apple?" do - it "returns true if the feed has apple credentials" do - create(:apple_config, feed: feed2, publish_enabled: true) - refute feed1.publish_to_apple? - assert feed2.publish_to_apple? - end - - it "returns false if the creds are not marked publish_enabled?" do - create(:apple_config, feed: feed2, publish_enabled: false) - refute feed2.publish_to_apple? - end - - it "returns false if the feed does not have apple credentials" do - refute feed2.apple_config + it "returns false if the feed is not an Apple Subscription feed" do + refute_equal feed2.type, "Feeds::AppleSubscription" refute feed2.publish_to_apple? end end diff --git a/test/models/publishing_pipeline_state_test.rb b/test/models/publishing_pipeline_state_test.rb index 4239e4e77..4f3803f0c 100644 --- a/test/models/publishing_pipeline_state_test.rb +++ b/test/models/publishing_pipeline_state_test.rb @@ -298,11 +298,10 @@ describe "Apple publishing" do let(:f1) { podcast.default_feed } let(:f2) { create(:private_feed, podcast: podcast) } - let(:f3) { create(:private_feed, podcast: podcast) } - let(:apple_config) { create(:apple_config, feed: f3, publish_enabled: true) } + let(:f3) { create(:apple_feed, podcast: podcast) } it "can publish via the apple configs" do - assert [f1, f2, f3, apple_config] + assert [f1, f2, f3] PublishAppleJob.stub(:perform_now, "published apple!") do PublishFeedJob.stub_any_instance(:save_file, "saved rss!") do diff --git a/test/test_helper.rb b/test/test_helper.rb index b870b61a7..fedf3d230 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,6 +19,7 @@ ENV["APPLE_KEY_PEM_B64"] = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUhHWUUvUVBZVWtkVUFmczcyZ1FUQkE5aTVBNkRndklFOGlpV3RrQzFScDdvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFaHFJSFVZUDN3QmxMdnMvQVpLM1ZHdW0vai8rMkhnVVF6dDc4TFQ0blMrckkxSlZJT0ZyVQpSVUZ6NmtSZ0pFeGxyZjdvSGZxZkxZanZGM0JvT3pmbWx3PT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQ==" ENV["APPLE_API_BRIDGE_URL"] = "http://localhost:3000" +ENV["SLACK_CHANNEL_ID"] = "" ENV["RAILS_ENV"] ||= "test" require_relative "../config/environment"