diff --git a/.travis.yml b/.travis.yml index d1b273bcf..ccd280bfb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,8 +18,6 @@ addons: - $(ls cypress/screenshots/*.* | tr "\n" ":") - $(ls cypress/videos/*.* | tr "\n" ":") debug: true -services: - - redis env: global: - RAILS_ENV=test diff --git a/Gemfile b/Gemfile index 9fe8356c8..6c1d9737b 100644 --- a/Gemfile +++ b/Gemfile @@ -159,6 +159,7 @@ end group :test do gem "exifr", ">= 1.3.6" # EXIF Reader is a module to read EXIF from JPEG and TIFF images gem "factory_bot_rails", "~> 6.1" # factory_bot is a fixtures replacement with a straightforward definition syntax, support for multiple build strategies + gem "fakeredis", "~> 0.8.0" # Fake (In-memory) driver for redis-rb. Useful for testing environment and machines without Redis. gem "launchy", "~> 2.5" # Launchy is helper class for launching cross-platform applications in a fire and forget manner. gem "pundit-matchers", "~> 1.6" # A set of RSpec matchers for testing Pundit authorisation policies gem "rspec-retry", "~> 0.6" # retry intermittently failing rspec examples diff --git a/Gemfile.lock b/Gemfile.lock index 716551bc1..7f4704d85 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -253,6 +253,8 @@ GEM railties (>= 5.0.0) faker (2.17.0) i18n (>= 1.6, < 2) + fakeredis (0.8.0) + redis (~> 4.1) faraday (1.4.1) faraday-excon (~> 1.1) faraday-net_http (~> 1.0) @@ -878,6 +880,7 @@ DEPENDENCIES exifr (>= 1.3.6) factory_bot_rails (~> 6.1) faker (~> 2.17) + fakeredis (~> 0.8.0) fastly (~> 3.0) feedjira (~> 3.1) field_test (~> 0.4) diff --git a/app/controllers/devices_controller.rb b/app/controllers/devices_controller.rb index abfe60414..8d0034eb7 100644 --- a/app/controllers/devices_controller.rb +++ b/app/controllers/devices_controller.rb @@ -5,7 +5,7 @@ class DevicesController < ApplicationController # See: https://github.com/forem/forem/pull/12419/files#r563906038 skip_before_action :verify_authenticity_token, only: [:destroy] - rescue_from ActiveRecord::ActiveRecordError do |exc| + rescue_from ActiveRecord::ActiveRecordError, ArgumentError do |exc| render json: { error: exc.message, status: 422 }, status: :unprocessable_entity end diff --git a/app/models/consumer_app.rb b/app/models/consumer_app.rb index 7b3c8a3fc..adb83fb24 100644 --- a/app/models/consumer_app.rb +++ b/app/models/consumer_app.rb @@ -2,12 +2,12 @@ class ConsumerApp < ApplicationRecord resourcify FOREM_BUNDLE = "com.forem.app".freeze - SUPPORTED_PLATFORMS = [Device::ANDROID, Device::IOS].freeze - FOREM_APP_PLATFORMS = [Device::IOS].freeze + FOREM_APP_PLATFORMS = %w[ios].freeze - validates :app_bundle, presence: true - validates :platform, presence: true - validates :app_bundle, uniqueness: { scope: :platform } + enum platform: { android: Device::ANDROID, ios: Device::IOS } + + validates :app_bundle, presence: true, uniqueness: { scope: :platform } + validates :platform, inclusion: { in: platforms.keys } has_many :devices, dependent: :destroy @@ -47,7 +47,7 @@ class ConsumerApp < ApplicationRecord # https://github.com/rpush/rpush/wiki/Using-Redis#find_by_name-cannot-be-used-in-rpush-redis # rubocop:disable Rails/FindBy def clear_rpush_app - case platform_was + case ConsumerApp.platforms[platform_was] when Device::IOS Rpush::Apns2::App.where(name: app_bundle_was).first&.destroy when Device::ANDROID diff --git a/app/models/device.rb b/app/models/device.rb index 5929b66b6..eecaa31a9 100644 --- a/app/models/device.rb +++ b/app/models/device.rb @@ -2,21 +2,23 @@ class Device < ApplicationRecord # @fdoxyz to remove app_bundle from Device soon self.ignored_columns = ["app_bundle"] - belongs_to :user belongs_to :consumer_app + belongs_to :user IOS = "iOS".freeze ANDROID = "Android".freeze + enum platform: { android: ANDROID, ios: IOS } + + validates :platform, inclusion: { in: platforms.keys } + validates :token, presence: true validates :token, uniqueness: { scope: %i[user_id platform consumer_app_id] } - validates :platform, inclusion: { in: [IOS, ANDROID] } def create_notification(title, body, payload) - case platform - when IOS - ios_notification(title, body, payload) - when ANDROID + if android? android_notification(title, body, payload) + elsif ios? + ios_notification(title, body, payload) end end diff --git a/app/queries/consumer_apps/find_or_create_by_query.rb b/app/queries/consumer_apps/find_or_create_by_query.rb index 145ee210f..6e99a2a85 100644 --- a/app/queries/consumer_apps/find_or_create_by_query.rb +++ b/app/queries/consumer_apps/find_or_create_by_query.rb @@ -11,16 +11,24 @@ module ConsumerApps def call # This guard clause handles the Forem app special case - return forem_app_target if @app_bundle == ConsumerApp::FOREM_BUNDLE + return forem_consumer_app if @app_bundle == ConsumerApp::FOREM_BUNDLE # All other ConsumerApp are simply fetched as usual - ConsumerApp.find_by(app_bundle: @app_bundle, platform: @platform) + relation.find_by(app_bundle: @app_bundle) end private - def forem_app_target - ConsumerApp.create_or_find_by(app_bundle: ConsumerApp::FOREM_BUNDLE, platform: @platform) + attr_reader :platform + + def relation + # taking advantage of enum scopes in Rails + # see https://api.rubyonrails.org/classes/ActiveRecord/Enum.html + ConsumerApp.public_send(platform) + end + + def forem_consumer_app + relation.create_or_find_by(app_bundle: ConsumerApp::FOREM_BUNDLE) end end end diff --git a/app/queries/consumer_apps/rpush_app_query.rb b/app/queries/consumer_apps/rpush_app_query.rb index cfce83dbe..c4c77847a 100644 --- a/app/queries/consumer_apps/rpush_app_query.rb +++ b/app/queries/consumer_apps/rpush_app_query.rb @@ -7,6 +7,7 @@ module ConsumerApps def initialize(app_bundle:, platform:) @app_bundle = app_bundle @platform = platform + @consumer_app = ConsumerApps::FindOrCreateByQuery.call( app_bundle: @app_bundle, platform: @platform, @@ -14,25 +15,26 @@ module ConsumerApps end def call - case @consumer_app&.platform - when Device::IOS - ios_app = Rpush::Apns2::App.where(name: @app_bundle).first - ios_app || recreate_ios_app! - when Device::ANDROID - android_app = Rpush::Gcm::App.where(name: @app_bundle).first + if consumer_app.android? + android_app = Rpush::Gcm::App.where(name: app_bundle).first android_app || recreate_android_app! + elsif consumer_app.ios? + ios_app = Rpush::Apns2::App.where(name: app_bundle).first + ios_app || recreate_ios_app! end end private + attr_reader :app_bundle, :consumer_app + def recreate_ios_app! app = Rpush::Apns2::App.new - app.name = @app_bundle - app.certificate = @consumer_app.auth_credentials.to_s.gsub("\\n", "\n") + app.name = app_bundle + app.certificate = consumer_app.auth_credentials.to_s.gsub("\\n", "\n") app.environment = Rails.env app.password = "" - app.bundle_id = @app_bundle + app.bundle_id = app_bundle app.connections = 1 app.save! app @@ -40,8 +42,8 @@ module ConsumerApps def recreate_android_app! app = Rpush::Gcm::App.new - app.name = @app_bundle - app.auth_key = @consumer_app.auth_credentials.to_s + app.name = app_bundle + app.auth_key = consumer_app.auth_credentials.to_s app.connections = 1 app.save! app diff --git a/app/services/push_notifications/send.rb b/app/services/push_notifications/send.rb index d9e4b0a73..4717d0fc7 100644 --- a/app/services/push_notifications/send.rb +++ b/app/services/push_notifications/send.rb @@ -12,7 +12,9 @@ module PushNotifications end def call - Device.where(user_id: @user_ids).find_each do |device| + relation = Device.where(user_id: @user_ids) + + relation.find_each do |device| device.create_notification(@title, @body, @payload) end @@ -21,7 +23,7 @@ module PushNotifications # has a constraint that will execute the job only once (30 seconds from now). So if we need to send 1 PN # every 5 seconds we only execute this once every 30s (no duplicate/unnecessary processing). # If no PNs are scheduled to be sent for a 6h span then 0 jobs are executed. - PushNotifications::DeliverWorker.perform_in(30.seconds) + PushNotifications::DeliverWorker.perform_in(30.seconds) if relation.any? end end end diff --git a/app/views/admin/consumer_apps/_form.html.erb b/app/views/admin/consumer_apps/_form.html.erb index 8b9352de8..7975be2ff 100644 --- a/app/views/admin/consumer_apps/_form.html.erb +++ b/app/views/admin/consumer_apps/_form.html.erb @@ -5,7 +5,7 @@
<%= label_tag :platform, "Platform:" %> - <%= select_tag :platform, options_for_select(ConsumerApp::SUPPORTED_PLATFORMS, selected: @app.platform, class: "crayons-select"), class: "crayons-select" %> + <%= select_tag :platform, options_for_select(ConsumerApp.platforms.invert, selected: @app.platform, class: "crayons-select"), class: "crayons-select" %>
diff --git a/app/views/admin/consumer_apps/index.html.erb b/app/views/admin/consumer_apps/index.html.erb index e693f8f03..f69d484f9 100644 --- a/app/views/admin/consumer_apps/index.html.erb +++ b/app/views/admin/consumer_apps/index.html.erb @@ -26,7 +26,7 @@ <% @apps.each do |app| %> <%= app.app_bundle %> - <%= app.platform %> + <%= Device.platforms[app.platform] %> <%= app.devices.count %> <% if app.auth_credentials.present? %> diff --git a/config/initializers/rpush.rb b/config/initializers/rpush.rb index 4ca037780..50e9d14d2 100644 --- a/config/initializers/rpush.rb +++ b/config/initializers/rpush.rb @@ -68,7 +68,7 @@ Rpush.reflect do |on| if notification.error_description == "BadDeviceToken" # When adding new platforms we'll need to check to only clear out devices # scoped to that specific platform. i.e. notification.class.to_s =~ /Apns.+::Notification/ - Device.where(token: notification.device_token, platform: "iOS").destroy_all + Device.ios.where(token: notification.device_token).destroy_all end HoneyBadger.notify(error_message: diff --git a/spec/factories/consumer_apps.rb b/spec/factories/consumer_apps.rb index 2f2b2fc4a..adcc7cc9f 100644 --- a/spec/factories/consumer_apps.rb +++ b/spec/factories/consumer_apps.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :consumer_app do auth_key { Faker::Alphanumeric.alpha(number: 10) } active { true } - platform { Device::IOS } + platform { :ios } app_bundle { Faker::Internet.domain_name(subdomain: true) } end end diff --git a/spec/factories/devices.rb b/spec/factories/devices.rb index e141d64c7..28344a073 100644 --- a/spec/factories/devices.rb +++ b/spec/factories/devices.rb @@ -3,6 +3,6 @@ FactoryBot.define do association :user, strategy: :create association :consumer_app, strategy: :create sequence(:token) { |n| "unique_token_#{n}" } - platform { Device::IOS } + platform { :ios } end end diff --git a/spec/models/consumer_app_spec.rb b/spec/models/consumer_app_spec.rb index ca5aa27e3..e177d1475 100644 --- a/spec/models/consumer_app_spec.rb +++ b/spec/models/consumer_app_spec.rb @@ -1,60 +1,103 @@ require "rails_helper" RSpec.describe ConsumerApp, type: :model do - let!(:consumer_app) { create(:consumer_app, platform: Device::IOS) } + let(:consumer_app_android) { create(:consumer_app, platform: :android) } + let(:consumer_app_ios) { create(:consumer_app, platform: :ios) } + + describe "validations" do + subject { consumer_app_android } + + describe "builtin validations" do + it { is_expected.to have_many(:devices).dependent(:destroy) } + + it { is_expected.to validate_presence_of(:app_bundle) } + it { is_expected.to validate_uniqueness_of(:app_bundle).scoped_to(:platform) } + end + end describe "operational?" do - context "with non-forem apps" do - it "returns false if not active in DB or credentials are unavailable" do + context "with non-Forem apps" do + it "returns false if not active in DB or if credentials are unavailable" do inactive_consumer_app = create(:consumer_app, active: false) - expect(inactive_consumer_app.operational?).to be false + expect(inactive_consumer_app.operational?).to be(false) consumer_app_without_credentials = create(:consumer_app, auth_key: nil) - expect(consumer_app_without_credentials.operational?).to be false + expect(consumer_app_without_credentials.operational?).to be(false) end - it "returns true if both active && credentials are available" do - expect(consumer_app.operational?).to be true + it "returns true if both active and credentials are available for Android" do + expect(consumer_app_android.operational?).to be(true) + end + + it "returns true if both active and credentials are available for iOS" do + expect(consumer_app_ios.operational?).to be(true) end end - context "with forem apps" do - it "returns true/false based on the ENV variable for the forem apps" do + context "with Forem apps" do + it "returns true/false based on the ENV variable for the Forem apps" do forem_consumer_app = ConsumerApps::FindOrCreateByQuery.call( app_bundle: ConsumerApp::FOREM_BUNDLE, platform: ConsumerApp::FOREM_APP_PLATFORMS.sample, ) allow(ApplicationConfig).to receive(:[]).with("RPUSH_IOS_PEM").and_return("asdf123") - expect(forem_consumer_app.operational?).to be true + expect(forem_consumer_app.operational?).to be(true) allow(ApplicationConfig).to receive(:[]).with("RPUSH_IOS_PEM").and_return(nil) - expect(forem_consumer_app.operational?).to be false + expect(forem_consumer_app.operational?).to be(false) end end end describe "after an update" do - it "recreates the Redis-backed Rpush app" do + it "recreates the Rpush app for Android", :aggregate_failures do + mock_rpush(consumer_app_android) + rpush_app = ConsumerApps::RpushAppQuery.call( - app_bundle: consumer_app.app_bundle, - platform: consumer_app.platform, + app_bundle: consumer_app_android.app_bundle, + platform: consumer_app_android.platform, ) - auth_key = rpush_app.certificate - # The Redis-backed Rpush App has the ConsumerApp's auth_credentials - expect(rpush_app).to be_instance_of(Rpush::Apns2::App) - expect(auth_key).to eq(consumer_app.auth_credentials) + # The Rpush App has the ConsumerApp's auth_credentials + expect(rpush_app).to be_instance_of(Rpush::Gcm::App) + expect(rpush_app.auth_key).to eq(consumer_app_android.auth_credentials) - consumer_app.auth_key = "new_auth_key" - expect(consumer_app.save).to be true + consumer_app_android.auth_key = "new_auth_key" + expect(consumer_app_android.save).to be(true) # After update fetch again rpush_app = ConsumerApps::RpushAppQuery.call( - app_bundle: consumer_app.app_bundle, - platform: consumer_app.platform, + app_bundle: consumer_app_android.app_bundle, + platform: consumer_app_android.platform, ) - # The Redis-backed Rpush App has the new ConsumerApp's auth_credentials + # The Rpush App has the new ConsumerApp's auth_credentials + expect(rpush_app).to be_instance_of(Rpush::Gcm::App) + expect(rpush_app.auth_key).to eq("new_auth_key") + end + + it "recreates the Rpush app for iOS", :aggregate_failures do + mock_rpush(consumer_app_ios) + + rpush_app = ConsumerApps::RpushAppQuery.call( + app_bundle: consumer_app_ios.app_bundle, + platform: consumer_app_ios.platform, + ) + + # The Rpush App has the ConsumerApp's auth_credentials + expect(rpush_app).to be_instance_of(Rpush::Apns2::App) + expect(rpush_app.certificate).to eq(consumer_app_ios.auth_credentials) + + consumer_app_ios.auth_key = "new_auth_key" + expect(consumer_app_ios.save).to be(true) + + # After update fetch again + rpush_app = ConsumerApps::RpushAppQuery.call( + app_bundle: consumer_app_ios.app_bundle, + platform: consumer_app_ios.platform, + ) + + # The Rpush App has the new ConsumerApp's auth_credentials expect(rpush_app).to be_instance_of(Rpush::Apns2::App) expect(rpush_app.certificate).to eq("new_auth_key") end diff --git a/spec/models/device_spec.rb b/spec/models/device_spec.rb new file mode 100644 index 000000000..e73f5835f --- /dev/null +++ b/spec/models/device_spec.rb @@ -0,0 +1,17 @@ +require "rails_helper" + +RSpec.describe Device, type: :model do + let(:device) { create(:device) } + + describe "validations" do + subject { device } + + describe "builtin validations" do + it { is_expected.to belong_to(:consumer_app) } + it { is_expected.to belong_to(:user) } + + it { is_expected.to validate_presence_of(:token) } + it { is_expected.to validate_uniqueness_of(:token).scoped_to(%i[user_id platform consumer_app_id]) } + end + end +end diff --git a/spec/queries/consumer_apps/find_or_create_by_query_spec.rb b/spec/queries/consumer_apps/find_or_create_by_query_spec.rb index 0c2d5ee10..04626b089 100644 --- a/spec/queries/consumer_apps/find_or_create_by_query_spec.rb +++ b/spec/queries/consumer_apps/find_or_create_by_query_spec.rb @@ -8,7 +8,7 @@ RSpec.describe ConsumerApps::FindOrCreateByQuery, type: :query do expect do app = described_class.call( app_bundle: ConsumerApp::FOREM_BUNDLE, - platform: Device::IOS, + platform: :ios, ) expect(app).to be_instance_of(ConsumerApp) end.to change(ConsumerApp, :count).by(1) diff --git a/spec/queries/consumer_apps/rpush_app_query_spec.rb b/spec/queries/consumer_apps/rpush_app_query_spec.rb index 2a7ae7207..e9c38baee 100644 --- a/spec/queries/consumer_apps/rpush_app_query_spec.rb +++ b/spec/queries/consumer_apps/rpush_app_query_spec.rb @@ -2,11 +2,13 @@ require "rails_helper" RSpec.describe ConsumerApps::RpushAppQuery, type: :query do let(:consumer_app) do - ConsumerApps::FindOrCreateByQuery.call(app_bundle: ConsumerApp::FOREM_BUNDLE, platform: Device::IOS) + ConsumerApps::FindOrCreateByQuery.call(app_bundle: ConsumerApp::FOREM_BUNDLE, platform: :ios) end - describe "Redis-backed rpush app" do + describe "Rpush app" do it "is recreated after updating a ConsumerApp" do + mock_rpush(consumer_app) + # Fetch rpush app associated to the target rpush_app = described_class.call( app_bundle: consumer_app.app_bundle, diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index d3b87d759..6d9f2f41b 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -10,6 +10,7 @@ abort("The Rails environment is running in production mode!") if Rails.env.produ # Add additional requires below this line. Rails is not loaded until this point! +require "fakeredis/rspec" require "pundit/matchers" require "pundit/rspec" require "webmock/rspec" @@ -72,6 +73,7 @@ RSpec.configure do |config| config.include Devise::Test::IntegrationHelpers, type: :request config.include FactoryBot::Syntax::Methods config.include OmniauthHelpers + config.include RpushHelpers config.include SidekiqTestHelpers config.after(:each, type: :system) do diff --git a/spec/requests/admin/consumer_apps_spec.rb b/spec/requests/admin/consumer_apps_spec.rb index 88e59eff2..52816704f 100644 --- a/spec/requests/admin/consumer_apps_spec.rb +++ b/spec/requests/admin/consumer_apps_spec.rb @@ -1,12 +1,12 @@ require "rails_helper" require "requests/shared_examples/internal_policy_dependant_request" -RSpec.describe "/admin/consumer_apps", type: :request do +RSpec.describe "Admin - Consumer Apps", type: :request do let(:get_resource) { get admin_consumer_apps_path } let(:params) do { app_bundle: Faker::Internet.domain_name(subdomain: true), - platform: Device::IOS, + platform: "ios", auth_key: "sample_data" } end @@ -55,11 +55,15 @@ RSpec.describe "/admin/consumer_apps", type: :request do end describe "PUT /admin/consumer_apps" do - let!(:consumer_app) { create(:consumer_app, app_bundle: "com.bundle.test") } + let(:consumer_app) { create(:consumer_app, app_bundle: "com.bundle.test") } it "updates the ConsumerApp" do + mock_rpush(consumer_app) + put admin_consumer_app_path(consumer_app.id), params: params + consumer_app.reload + expect(consumer_app.app_bundle).to eq(params[:app_bundle]) expect(consumer_app.platform).to eq(params[:platform]) expect(consumer_app.auth_key).to eq(params[:auth_key]) @@ -106,11 +110,15 @@ RSpec.describe "/admin/consumer_apps", type: :request do end describe "PUT /admin/consumer_apps" do - let!(:consumer_app) { create(:consumer_app, app_bundle: "com.bundle.test") } + let(:consumer_app) { create(:consumer_app, app_bundle: "com.bundle.test") } it "updates the ConsumerApp" do + mock_rpush(consumer_app) + put admin_consumer_app_path(consumer_app.id), params: params + consumer_app.reload + expect(consumer_app.app_bundle).to eq(params[:app_bundle]) expect(consumer_app.platform).to eq(params[:platform]) expect(consumer_app.auth_key).to eq(params[:auth_key]) diff --git a/spec/requests/devices_spec.rb b/spec/requests/devices_spec.rb index 69fd04850..71c40864c 100644 --- a/spec/requests/devices_spec.rb +++ b/spec/requests/devices_spec.rb @@ -11,7 +11,7 @@ RSpec.describe "Devices", type: :request do describe "POST /users/devices" do context "when device persisted" do it "increases device count" do - post "/users/devices", params: { + post devices_path, params: { token: "123", platform: "Android", app_bundle: consumer_app.app_bundle @@ -31,13 +31,14 @@ RSpec.describe "Devices", type: :request do end it "does not increase device count" do - post "/users/devices", params: params + post devices_path, params: params expect(user.devices.count).to eq(0) end it "returns an error" do - post "/users/devices", params: params - expect(response.status).to eq(400) + post devices_path, params: params + + expect(response).to have_http_status(:unprocessable_entity) expect(response.body).to include("error") end end @@ -55,14 +56,16 @@ RSpec.describe "Devices", type: :request do end it "returns an error" do - delete "/users/devices/123" + delete device_path(123) + expect(response.status).to eq(404) expect(response.parsed_body["error"]).to eq("Not Found") expect(response.parsed_body["status"]).to eq(404) end it "return an error if device id doesn't match params" do - delete "/users/devices/123", params: incomplete_params + delete device_path(123), params: incomplete_params + expect(response.status).to eq(404) expect(response.parsed_body["error"]).to eq("Not Found") expect(response.parsed_body["status"]).to eq(404) @@ -79,7 +82,8 @@ RSpec.describe "Devices", type: :request do end it "deletes the device" do - delete "/users/devices/#{device.user.id}", params: params + delete device_path(device.user.id), params: params + expect(user.devices.count).to eq(0) expect(response.status).to eq(204) expect(Device.find_by(id: device.id)).to be_nil diff --git a/spec/services/push_notifications/send_spec.rb b/spec/services/push_notifications/send_spec.rb index 3f763e11a..f1b7985c3 100644 --- a/spec/services/push_notifications/send_spec.rb +++ b/spec/services/push_notifications/send_spec.rb @@ -21,14 +21,10 @@ RSpec.describe PushNotifications::Send, type: :service do end context "with no devices for user" do - before do - user.devices.delete - end - it "does nothing", :aggregate_failures do - expect(user.devices.count).to eq(0) - expect { described_class.call(**params) } - .not_to change { Rpush::Client::Redis::Notification.all.count } + described_class.call(**params) + + sidekiq_assert_no_enqueued_jobs(only: PushNotifications::DeliverWorker) end end @@ -36,44 +32,63 @@ RSpec.describe PushNotifications::Send, type: :service do before do allow(ApplicationConfig).to receive(:[]).with("RPUSH_IOS_PEM").and_return("dGVzdGluZw==") allow(ApplicationConfig).to receive(:[]).with("COMMUNITY_NAME").and_return("Forem") - create(:device, user: user) end it "creates a notification and enqueues it" do - expect { described_class.call(**params) } - .to change { Rpush::Client::Redis::Notification.all.count }.by(1) - .and change(PushNotifications::DeliverWorker.jobs, :size).by(1) + device = create(:device, user: user) + mocked_objects = mock_rpush(device.consumer_app) + + described_class.call(**params) + + expect(mocked_objects[:rpush_notification]).to have_received(:save!).once + + sidekiq_assert_enqueued_jobs(1, only: PushNotifications::DeliverWorker) end it "creates a single notification for each of the user's devices when they have multiple" do - create(:device, user: user) + consumer_app = create(:consumer_app) + devices = create_list(:device, 2, user: user, consumer_app: consumer_app) + mocked_objects = mock_rpush(consumer_app) - expect { described_class.call(**params) } - .to change { Rpush::Client::Redis::Notification.all.count }.by(2) - .and change(PushNotifications::DeliverWorker.jobs, :size).by(1) + described_class.call(**params) + + expect(mocked_objects[:rpush_notification]).to have_received(:save!).exactly(devices.size).times + + sidekiq_assert_enqueued_jobs(1, only: PushNotifications::DeliverWorker) end end context "with devices for multiple users" do + let(:consumer_app) { create(:consumer_app) } + before do allow(ApplicationConfig).to receive(:[]).with("RPUSH_IOS_PEM").and_return("dGVzdGluZw==") allow(ApplicationConfig).to receive(:[]).with("COMMUNITY_NAME").and_return("Forem") - create(:device, user: user) - create(:device, user: user2) + + create(:device, user: user, consumer_app: consumer_app) + create(:device, user: user2, consumer_app: consumer_app) end it "creates a notification and enqueues it" do - expect { described_class.call(**many_targets_params) } - .to change { Rpush::Client::Redis::Notification.all.count }.by(2) - .and change { PushNotifications::DeliverWorker.jobs.size }.by(1) + mocked_objects = mock_rpush(consumer_app) + + described_class.call(**many_targets_params) + + expect(mocked_objects[:rpush_notification]).to have_received(:save!).exactly(2).times + + sidekiq_assert_enqueued_jobs(1, only: PushNotifications::DeliverWorker) end it "creates a single notification for each of the user's devices when they have multiple" do create(:device, user: user) - expect { described_class.call(**many_targets_params) } - .to change { Rpush::Client::Redis::Notification.all.count }.by(3) - .and change { PushNotifications::DeliverWorker.jobs.size }.by(1) + mocked_objects = mock_rpush(consumer_app) + + described_class.call(**many_targets_params) + + expect(mocked_objects[:rpush_notification]).to have_received(:save!).exactly(3).times + + sidekiq_assert_enqueued_jobs(1, only: PushNotifications::DeliverWorker) end end end diff --git a/spec/support/rpush_helpers.rb b/spec/support/rpush_helpers.rb new file mode 100644 index 000000000..cffb71656 --- /dev/null +++ b/spec/support/rpush_helpers.rb @@ -0,0 +1,37 @@ +module RpushHelpers + def mock_rpush(consumer_app) + if consumer_app.android? + rpush_class = Rpush::Gcm::App + rpush_notification_class = Rpush::Gcm::Notification + auth_key = :auth_key + elsif consumer_app.ios? + rpush_class = Rpush::Apns2::App + rpush_notification_class = Rpush::Apns2::Notification + auth_key = :certificate + end + + rpush_app = rpush_class.new( + :name => consumer_app.app_bundle, + auth_key => consumer_app.auth_credentials, + ) + allow(rpush_app).to receive(:destroy) + + rpush_notification = rpush_notification_class.new + allow(rpush_notification).to receive(:save!) + allow(rpush_notification_class).to receive(:new).and_return(rpush_notification) + + relation = double + allow(relation).to receive(:first).and_return(rpush_app) + allow(rpush_class).to receive(:where).and_return(relation) + + original = consumer_app.method(:save) + allow(consumer_app).to receive(:save) do + rpush_app.public_send("#{auth_key}=", consumer_app.auth_credentials) + original.call + end + + { + rpush_notification: rpush_notification + } + end +end diff --git a/vendor/cache/fakeredis-0.8.0.gem b/vendor/cache/fakeredis-0.8.0.gem new file mode 100644 index 000000000..17f84b52b Binary files /dev/null and b/vendor/cache/fakeredis-0.8.0.gem differ