From cf9f6094cde82ac360d76757395f90421b7ee30e Mon Sep 17 00:00:00 2001 From: rhymes Date: Fri, 7 May 2021 08:03:16 +0200 Subject: [PATCH] Have specs run without Redis and refactor ConsumerApp and Device (#13647) * Add fakeredis gem * Mock Pusher::PushNotifications in specs * Remove Redis from Travis * Add Android tests to ConsumerApp specs * Use mock for push notification tests and refactor ConsumerApp and Device code * Fix remaining broken specs * Use symbols * Cleanup rpush helpers * Pusher::PushNotifications is no more * Use human friendly platform in Admin::ConsumerApps index page --- .travis.yml | 2 - Gemfile | 1 + Gemfile.lock | 3 + app/controllers/devices_controller.rb | 2 +- app/models/consumer_app.rb | 12 +-- app/models/device.rb | 14 +-- .../consumer_apps/find_or_create_by_query.rb | 16 +++- app/queries/consumer_apps/rpush_app_query.rb | 24 ++--- app/services/push_notifications/send.rb | 6 +- app/views/admin/consumer_apps/_form.html.erb | 2 +- app/views/admin/consumer_apps/index.html.erb | 2 +- config/initializers/rpush.rb | 2 +- spec/factories/consumer_apps.rb | 2 +- spec/factories/devices.rb | 2 +- spec/models/consumer_app_spec.rb | 89 +++++++++++++----- spec/models/device_spec.rb | 17 ++++ .../find_or_create_by_query_spec.rb | 2 +- .../consumer_apps/rpush_app_query_spec.rb | 6 +- spec/rails_helper.rb | 2 + spec/requests/admin/consumer_apps_spec.rb | 16 +++- spec/requests/devices_spec.rb | 18 ++-- spec/services/push_notifications/send_spec.rb | 61 +++++++----- spec/support/rpush_helpers.rb | 37 ++++++++ vendor/cache/fakeredis-0.8.0.gem | Bin 0 -> 41984 bytes 24 files changed, 241 insertions(+), 97 deletions(-) create mode 100644 spec/models/device_spec.rb create mode 100644 spec/support/rpush_helpers.rb create mode 100644 vendor/cache/fakeredis-0.8.0.gem 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 0000000000000000000000000000000000000000..17f84b52b0ba1c888fdba3a3f863f9bb0ede5daa GIT binary patch literal 41984 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�bsDsE4t|vKD853u?&w)HbJNJ3| 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 literal 0 HcmV?d00001