Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 2 additions & 22 deletions lib/action_push_native/service/fcm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def initialize(config)
end

def push(notification)
response = httpx_session.post("v1/projects/#{config.fetch(:project_id)}/messages:send", json: payload_from(notification), headers: { authorization: "Bearer #{access_token}" })
response = httpx_session.post("v1/projects/#{config.fetch(:project_id)}/messages:send", json: payload_from(notification))
handle_error(response) if response.error
end

Expand All @@ -22,20 +22,7 @@ def push(notification)

def httpx_session
self.class.httpx_sessions ||= {}
self.class.httpx_sessions[config] ||= build_httpx_session
end

# FCM suggests at least a 10s timeout for requests, we set 15 to add some buffer.
# https://firebase.google.com/docs/cloud-messaging/scale-fcm#timeouts
DEFAULT_REQUEST_TIMEOUT = 15.seconds
DEFAULT_POOL_SIZE = 5

def build_httpx_session
HTTPX.
plugin(:persistent, close_on_fork: true).
with(timeout: { request_timeout: config[:request_timeout] || DEFAULT_REQUEST_TIMEOUT }).
with(pool_options: { max_connections: config[:connection_pool_size] || DEFAULT_POOL_SIZE }).
with(origin: "https://fcm.googleapis.com")
self.class.httpx_sessions[config] ||= HttpxSession.new(config)
end

def payload_from(notification)
Expand Down Expand Up @@ -75,13 +62,6 @@ def stringify(hash)
hash.compact.transform_values(&:to_s)
end

def access_token
authorizer = Google::Auth::ServiceAccountCredentials.make_creds \
json_key_io: StringIO.new(config.fetch(:encryption_key)),
scope: "https://www.googleapis.com/auth/firebase.messaging"
authorizer.fetch_access_token!["access_token"]
end

def handle_error(response)
if response.is_a?(HTTPX::ErrorResponse)
handle_network_error(response.error)
Expand Down
27 changes: 27 additions & 0 deletions lib/action_push_native/service/fcm/httpx_session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

class ActionPushNative::Service::Fcm::HttpxSession
# FCM suggests at least a 10s timeout for requests, we set 15 to add some buffer.
# https://firebase.google.com/docs/cloud-messaging/scale-fcm#timeouts
DEFAULT_REQUEST_TIMEOUT = 15.seconds
DEFAULT_POOL_SIZE = 5

def initialize(config)
@session = \
HTTPX.
plugin(:persistent, close_on_fork: true).
with(timeout: { request_timeout: config[:request_timeout] || DEFAULT_REQUEST_TIMEOUT }).
with(pool_options: { max_connections: config[:connection_pool_size] || DEFAULT_POOL_SIZE }).
with(origin: "https://fcm.googleapis.com")
@token_provider = ActionPushNative::Service::Fcm::TokenProvider.new(config)
end

def post(*uri, **options)
options[:headers] ||= {}
options[:headers][:authorization] = "Bearer #{token_provider.fresh_access_token}"
session.post(*uri, **options)
end

private
attr_reader :token_provider, :session
end
33 changes: 33 additions & 0 deletions lib/action_push_native/service/fcm/token_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

class ActionPushNative::Service::Fcm::TokenProvider
EXPIRED = -1

def initialize(config)
@config = config
@expires_at = EXPIRED
end

def fresh_access_token
regenerate_if_expired
token
end

private
attr_reader :config, :token, :expires_at

def regenerate_if_expired
regenerate if Time.now.utc >= expires_at
end

REFRESH_BUFFER = 1.minutes

def regenerate
authorizer = Google::Auth::ServiceAccountCredentials.make_creds \
json_key_io: StringIO.new(config.fetch(:encryption_key)),
scope: "https://www.googleapis.com/auth/firebase.messaging"
oauth2 = authorizer.fetch_access_token!
@token = oauth2["access_token"]
@expires_at = oauth2["expires_in"].seconds.from_now.utc - REFRESH_BUFFER
end
end
4 changes: 3 additions & 1 deletion test/jobs/action_push_native/notification_job_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ class NotificationJobTest < ActiveSupport::TestCase
device = action_push_native_devices(:pixel9)
stub_request(:post, "https://fcm.googleapis.com/v1/projects/your_project_id/messages:send").
to_raise(SocketError.new)
ActionPushNative::Service::Fcm.any_instance.stubs(:access_token).returns("fake_access_token")
authorizer = stub("authorizer")
authorizer.stubs(:fetch_access_token!).returns({ "access_token" => "fake_access_token", "expires_in" => 3599 })
Google::Auth::ServiceAccountCredentials.stubs(:make_creds).returns(authorizer)

assert_enqueued_jobs 1, only: ActionPushNative::NotificationJob do
ActionPushNative::NotificationJob.perform_later("ApplicationPushNotification", @notification_attributes, device)
Expand Down
18 changes: 17 additions & 1 deletion test/lib/action_push_native/service/fcm_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ class FcmTest < ActiveSupport::TestCase
end
end

test "access tokens are refreshed" do
@fcm.httpx_sessions = {}
stub_request(:post, "https://fcm.googleapis.com/v1/projects/your_project_id/messages:send")

authorizer = stub("authorizer")
authorizer.stubs(:fetch_access_token!).once.returns({ "access_token" => "fake_access_token", "expires_in" => 3599 })
Google::Auth::ServiceAccountCredentials.stubs(:make_creds).returns(authorizer)
@fcm.push(@notification)
@fcm.push(@notification)

authorizer.stubs(:fetch_access_token!).once.returns({ "access_token" => "fake_access_token", "expires_in" => 3599 })
travel 3600 do
@fcm.push(@notification)
end
end

private
def build_notification
ActionPushNative::Notification.
Expand All @@ -93,7 +109,7 @@ def build_notification

def stub_authorizer
authorizer = stub("authorizer")
authorizer.stubs(:fetch_access_token!).returns({ "access_token" => "fake_access_token" })
authorizer.stubs(:fetch_access_token!).returns({ "access_token" => "fake_access_token", "expires_in" => 3599 })
Google::Auth::ServiceAccountCredentials.stubs(:make_creds).returns(authorizer)
end
end
Expand Down