Skip to content

Commit

Permalink
Merge pull request #1013 from PRX/feat/apple_feed_subclass
Browse files Browse the repository at this point in the history
Feat/apple feed subclass
  • Loading branch information
radical-ube committed May 14, 2024
2 parents 80c15bd + 5f2d37d commit 360f44d
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 79 deletions.
30 changes: 4 additions & 26 deletions app/models/apple/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions app/models/feed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -167,7 +165,7 @@ def normalize_category(cat)
end

def publish_to_apple?
!!apple_config&.publish_to_apple?
false
end

def include_tags=(tags)
Expand Down
72 changes: 72 additions & 0 deletions app/models/feeds/apple_subscription.rb
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions db/migrate/20240502172959_add_type_to_feeds.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions test/factories/feed_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 5 additions & 6 deletions test/jobs/publish_apple_job_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 20 additions & 18 deletions test/jobs/publish_feed_job_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
82 changes: 82 additions & 0 deletions test/models/feed/apple_subscription_test.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 360f44d

Please sign in to comment.