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