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
This commit is contained in:
rhymes 2021-05-07 08:03:16 +02:00 committed by GitHub
parent de488f331d
commit cf9f6094cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 241 additions and 97 deletions

View file

@ -18,8 +18,6 @@ addons:
- $(ls cypress/screenshots/*.* | tr "\n" ":")
- $(ls cypress/videos/*.* | tr "\n" ":")
debug: true
services:
- redis
env:
global:
- RAILS_ENV=test

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -5,7 +5,7 @@
<div class="form-group">
<%= 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" %>
</div>
<div class="form-group">

View file

@ -26,7 +26,7 @@
<% @apps.each do |app| %>
<tr>
<td><%= app.app_bundle %></td>
<td><%= app.platform %></td>
<td><%= Device.platforms[app.platform] %></td>
<td><%= app.devices.count %></td>
<% if app.auth_credentials.present? %>
<td><span class="crayons-icon" role="img" aria-label="Authentication Key is available">✅</span></td>

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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

View file

@ -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])

View file

@ -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

View file

@ -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

View file

@ -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

BIN
vendor/cache/fakeredis-0.8.0.gem vendored Normal file

Binary file not shown.