From 8b36b41c9878c200a412da81267457ef2f025d09 Mon Sep 17 00:00:00 2001 From: Hua Jie Chen Date: Thu, 20 Aug 2026 19:11:57 -0400 Subject: [PATCH] Fix GoogleCredentialCache raising on a Hash-shaped json_key fetch_fresh_token wraps json_key in StringIO before handing it to Google::Auth::ServiceAccountCredentials, which requires a String. Any app whose json_key comes from a native json/jsonb DB column gets it back as an already-deserialized Hash, so every real send through such an app raised TypeError: no implicit conversion of Hash into String. Coerce to JSON text only when it isn't already a String. --- lib/rpush/daemon/google_credential_cache.rb | 5 ++- .../daemon/google_credential_cache_spec.rb | 45 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 spec/unit/daemon/google_credential_cache_spec.rb diff --git a/lib/rpush/daemon/google_credential_cache.rb b/lib/rpush/daemon/google_credential_cache.rb index cadc58f04..b10bd758e 100644 --- a/lib/rpush/daemon/google_credential_cache.rb +++ b/lib/rpush/daemon/google_credential_cache.rb @@ -27,7 +27,10 @@ def access_token(scope, json_key) private def fetch_fresh_token(scope, json_key) - json_key_io = json_key ? StringIO.new(json_key) : nil + # A caller's json_key attribute may come back as an already-parsed Hash + # (e.g. from a native json/jsonb DB column, which ActiveRecord deserializes + # on read) rather than the raw JSON text StringIO requires. + json_key_io = json_key ? StringIO.new(json_key.is_a?(String) ? json_key : json_key.to_json) : nil log_debug("FCM - Obtaining access token.") authorizer = Google::Auth::ServiceAccountCredentials.make_creds(scope: scope, json_key_io: json_key_io) authorizer.fetch_access_token diff --git a/spec/unit/daemon/google_credential_cache_spec.rb b/spec/unit/daemon/google_credential_cache_spec.rb new file mode 100644 index 000000000..6699a74bd --- /dev/null +++ b/spec/unit/daemon/google_credential_cache_spec.rb @@ -0,0 +1,45 @@ +require 'unit_spec_helper' + +describe Rpush::Daemon::GoogleCredentialCache do + subject { described_class.instance } + + let(:scope) { 'https://www.googleapis.com/auth/firebase.messaging' } + let(:json_key_hash) do + { + 'type' => 'service_account', + 'project_id' => 'curry-pizza-house', + 'private_key' => "-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----\n", + 'client_email' => 'fcm-push-sender@thanx-shared-infra.iam.gserviceaccount.com', + 'token_uri' => 'https://oauth2.googleapis.com/token' + } + end + let(:authorizer) { double(fetch_access_token: { 'access_token' => 'fake-token' }) } + + before do + subject.instance_variable_set(:@credentials_cache, {}) + end + + describe '#access_token' do + context 'when json_key is a Hash (matches production: rpush_apps.json_key is a native json column, which ActiveRecord deserializes to a Hash on read)' do + it 'builds the credential stream from it instead of raising' do + expect(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) do |scope:, json_key_io:| + expect(JSON.parse(json_key_io.read)).to eq(json_key_hash) + authorizer + end + + expect(subject.access_token(scope, json_key_hash)).to eq('access_token' => 'fake-token') + end + end + + context 'when json_key is already a String' do + it 'still builds the credential stream correctly' do + expect(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) do |scope:, json_key_io:| + expect(JSON.parse(json_key_io.read)).to eq(json_key_hash) + authorizer + end + + expect(subject.access_token(scope, json_key_hash.to_json)).to eq('access_token' => 'fake-token') + end + end + end +end