Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/apple feed subclass #1013

Merged
merged 9 commits into from
May 14, 2024
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)
cavis marked this conversation as resolved.
Show resolved Hide resolved
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
cavis marked this conversation as resolved.
Show resolved Hide resolved

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
Loading