From 9a701ce2a02ec093f66c4bbe5e61e5ceca298342 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 2 Sep 2024 17:38:39 +0100 Subject: [PATCH 01/19] Use OAuth2 token instead of server key for v1 --- lib/fcm.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/fcm.rb b/lib/fcm.rb index 08980ce..52f4778 100644 --- a/lib/fcm.rb +++ b/lib/fcm.rb @@ -51,10 +51,7 @@ def send_notification_v1(message) return if @project_name.empty? post_body = { 'message': message } - extra_headers = { - 'Authorization' => "Bearer #{jwt_token}" - } - for_uri(BASE_URI_V1, extra_headers) do |connection| + for_uri(BASE_URI_V1) do |connection| response = connection.post( "#{@project_name}/messages:send", post_body.to_json ) @@ -226,7 +223,8 @@ def for_uri(uri, extra_headers = {}) ) do |faraday| faraday.adapter Faraday.default_adapter faraday.headers["Content-Type"] = "application/json" - faraday.headers['Authorization'] = "key=#{@api_key}" + faraday.headers["Authorization"] = "Bearer #{jwt_token}" + faraday.headers["access_token_auth"]= "true" extra_headers.each do |key, value| faraday.headers[key] = value end From c9d94651ccbba976dfa5eec3edfd517845735d08 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Thu, 5 Sep 2024 13:52:37 +0100 Subject: [PATCH 02/19] Remove deprecated methods --- fcm.gemspec | 2 +- lib/fcm.rb | 34 +++++++--------------------------- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/fcm.gemspec b/fcm.gemspec index 05a211e..9d4f8c4 100644 --- a/fcm.gemspec +++ b/fcm.gemspec @@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__) Gem::Specification.new do |s| s.name = "fcm" - s.version = "1.0.8" + s.version = "2.0.0" s.platform = Gem::Platform::RUBY s.authors = ["Kashif Rasul", "Shoaib Burq"] s.email = ["kashif@decision-labs.com", "shoaib@decision-labs.com"] diff --git a/lib/fcm.rb b/lib/fcm.rb index 52f4778..7141aa3 100644 --- a/lib/fcm.rb +++ b/lib/fcm.rb @@ -12,11 +12,14 @@ class FCM INSTANCE_ID_API = "https://iid.googleapis.com" TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/ - def initialize(api_key, json_key_path = "", project_name = "", client_options = {}) - @api_key = api_key - @client_options = client_options + def initialize(_api_key = nil, json_key_path = "", project_name = "", client_options = {}) @json_key_path = json_key_path @project_name = project_name + @client_options = client_options + + warn <<-WARNING + [DEPRECATION] The use of api_key is deprecated. Please use the Credentials instead. + WARNING end # See https://firebase.google.com/docs/cloud-messaging/send-message @@ -43,7 +46,7 @@ def initialize(api_key, json_key_path = "", project_name = "", client_options = # } # } # } - # fcm = FCM.new(api_key, json_key_path, project_name) + # fcm = FCM.new(_api_key, json_key_path, project_name) # fcm.send_v1( # { "token": "4sdsx",, "to" : "notification": {}.. } # ) @@ -61,29 +64,6 @@ def send_notification_v1(message) alias send_v1 send_notification_v1 - # See https://developers.google.com/cloud-messaging/http for more details. - # { "notification": { - # "title": "Portugal vs. Denmark", - # "text": "5 to 1" - # }, - # "to" : "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1..." - # } - # fcm = FCM.new("API_KEY") - # fcm.send( - # ["4sdsx", "8sdsd"], # registration_ids - # { "notification": { "title": "Portugal vs. Denmark", "text": "5 to 1" }, "to" : "bk3RNwTe3HdFQ3P1..." } - # ) - def send_notification(registration_ids, options = {}) - post_body = build_post_body(registration_ids, options) - - for_uri(BASE_URI) do |connection| - response = connection.post("/fcm/send", post_body.to_json) - build_response(response, registration_ids) - end - end - - alias send send_notification - def create_notification_key(key_name, project_id, registration_ids = []) post_body = build_post_body(registration_ids, operation: "create", notification_key_name: key_name) From 5c38a0abdb71939d903225c3a1db28a231eeaad6 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Thu, 5 Sep 2024 16:40:45 +0100 Subject: [PATCH 03/19] Update fcm_spec.rb --- lib/fcm.rb | 7 +- spec/fcm_spec.rb | 596 +++++++---------------------------------------- 2 files changed, 79 insertions(+), 524 deletions(-) diff --git a/lib/fcm.rb b/lib/fcm.rb index 7141aa3..a8cd2f8 100644 --- a/lib/fcm.rb +++ b/lib/fcm.rb @@ -12,14 +12,9 @@ class FCM INSTANCE_ID_API = "https://iid.googleapis.com" TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/ - def initialize(_api_key = nil, json_key_path = "", project_name = "", client_options = {}) + def initialize(_api_key = nil, json_key_path = "", project_name = "") @json_key_path = json_key_path @project_name = project_name - @client_options = client_options - - warn <<-WARNING - [DEPRECATION] The use of api_key is deprecated. Please use the Credentials instead. - WARNING end # See https://firebase.google.com/docs/cloud-messaging/send-message diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index e976433..b206fed 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -1,9 +1,8 @@ require "spec_helper" describe FCM do - let(:send_url) { "#{FCM::BASE_URI}/fcm/send" } let(:group_notification_base_uri) { "#{FCM::GROUP_NOTIFICATION_BASE_URI}/gcm/notification" } - let(:api_key) { "AIzaSyB-1uEai2WiUapxCs2Q0GZYzPu7Udno5aA" } + let(:api_key) { "LEGACY_KEY" } let(:registration_id) { "42" } let(:registration_ids) { ["42"] } let(:key_name) { "appUser-Chris" } @@ -15,25 +14,43 @@ let(:invalid_condition) { "'TopicA' in topics and some other text ('TopicB' in topics || 'TopicC' in topics)" } let(:invalid_condition_topic) { "'TopicA$' in topics" } - it "should raise an error if the api key is not provided" do - expect { FCM.new }.to raise_error(ArgumentError) + let(:project_name) { 'test-project' } + let(:json_key_path) { 'path/to/json/key.json' } + + let(:mock_token) { "access_token" } + let(:mock_headers) do + { + "Content-Type" => "application/json", + "Authorization" => "Bearer #{mock_token}", + } end - it "should raise error if time_to_live is given" do - # ref: https://firebase.google.com/docs/cloud-messaging/http-server-ref#ttl + before do + # Mock the Google::Auth::ServiceAccountCredentials + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds). + and_return(double(fetch_access_token!: { 'access_token' => mock_token })) end - describe "#send_v1" do - let(:project_name) { "project_name" } - let(:send_v1_url) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } - let(:access_token) { "access_token" } - let(:valid_request_v1_headers) do - { - "Content-Type" => "application/json", - "Authorization" => "Bearer #{access_token}", - } + it "should initialize" do + expect { FCM.new(api_key, json_key_path) }.not_to raise_error + end + + describe "credentials path" do + it "can be a path to a file" do + fcm = FCM.new("test", "README.md") + expect(fcm.__send__(:json_key).class).to eq(File) + end + + it "can be an IO object" do + fcm = FCM.new("test", StringIO.new("hey")) + expect(fcm.__send__(:json_key).class).to eq(StringIO) end + end + describe "#send_v1 or #send_notification_v1" do + let(:client) { FCM.new(api_key, json_key_path, project_name) } + + let(:uri) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } let(:send_v1_params) do { 'token' => '4sdsx', @@ -60,14 +77,10 @@ } end - let(:valid_request_v1_body) do - { 'message' => send_v1_params } - end - let(:stub_fcm_send_v1_request) do - stub_request(:post, send_v1_url).with( - body: valid_request_v1_body.to_json, - headers: valid_request_v1_headers + stub_request(:post, uri).with( + body: { 'message' => send_v1_params }.to_json, + headers: mock_headers ).to_return( # ref: https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream body: "{}", @@ -76,514 +89,35 @@ ) end - let(:authorizer_double) { double("token_fetcher") } - let(:json_key_path) { double("file alike object") } - before do - expect(json_key_path).to receive(:respond_to?).and_return(true) - expect(Google::Auth::ServiceAccountCredentials).to receive_message_chain(:make_creds).and_return(authorizer_double) - expect(authorizer_double).to receive(:fetch_access_token!).and_return({ "access_token" => access_token }) + allow(client).to receive(:json_key) + stub_fcm_send_v1_request end it 'should send notification of HTTP V1 using POST to FCM server' do - fcm = FCM.new(api_key, json_key_path, project_name) - fcm.send_v1(send_v1_params).should eq( + client.send_v1(send_v1_params).should eq( response: 'success', body: '{}', headers: {}, status_code: 200 ) stub_fcm_send_v1_request.should have_been_made.times(1) end end - describe "sending notification" do - let(:valid_request_body) do - { registration_ids: registration_ids } - end - let(:valid_request_body_with_string) do - { registration_ids: registration_id } - end - let(:valid_request_headers) do - { - "Content-Type" => "application/json", - "Authorization" => "key=#{api_key}", - } - end - - let(:stub_fcm_send_request) do - stub_request(:post, send_url).with( - body: valid_request_body.to_json, - headers: valid_request_headers, - ).to_return( - # ref: https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream - body: "{}", - headers: {}, - status: 200, - ) - end - - let(:stub_fcm_send_request_with_string) do - stub_request(:post, send_url).with( - body: valid_request_body_with_string.to_json, - headers: valid_request_headers, - ).to_return( - body: "{}", - headers: {}, - status: 200, - ) - end - - let(:stub_fcm_send_request_with_basic_auth) do - uri = URI.parse(send_url) - uri.user = "a" - uri.password = "b" - stub_request(:post, uri.to_s).to_return(body: "{}", headers: {}, status: 200) - end - - before(:each) do - stub_fcm_send_request - stub_fcm_send_request_with_string - stub_fcm_send_request_with_basic_auth - end - - it "should send notification using POST to FCM server" do - fcm = FCM.new(api_key) - fcm.send(registration_ids).should eq(response: "success", body: "{}", headers: {}, status_code: 200, canonical_ids: [], not_registered_ids: []) - stub_fcm_send_request.should have_been_made.times(1) - end - - it "should send notification using POST to FCM if id provided as string" do - fcm = FCM.new(api_key) - fcm.send(registration_id).should eq(response: "success", body: "{}", headers: {}, status_code: 200, canonical_ids: [], not_registered_ids: []) - stub_fcm_send_request.should have_been_made.times(1) - end - - context "send notification with data" do - let!(:stub_with_data) do - stub_request(:post, send_url) - .with(body: '{"registration_ids":["42"],"data":{"score":"5x1","time":"15:10"}}', - headers: valid_request_headers) - .to_return(status: 200, body: "", headers: {}) - end - before do - end - it "should send the data in a post request to fcm" do - fcm = FCM.new(api_key) - fcm.send(registration_ids, data: { score: "5x1", time: "15:10" }) - stub_with_data.should have_been_requested - end - end - - context "sending notification to a topic" do - let!(:stub_with_valid_topic) do - stub_request(:post, send_url) - .with(body: '{"to":"/topics/TopicA","data":{"score":"5x1","time":"15:10"}}', - headers: valid_request_headers) - .to_return(status: 200, body: "", headers: {}) - end - let!(:stub_with_invalid_topic) do - stub_request(:post, send_url) - .with(body: '{"condition":"/topics/TopicA$","data":{"score":"5x1","time":"15:10"}}', - headers: valid_request_headers) - .to_return(status: 200, body: "", headers: {}) - end - - describe "#send_to_topic" do - it "should send the data in a post request to fcm" do - fcm = FCM.new(api_key) - fcm.send_to_topic(valid_topic, data: { score: "5x1", time: "15:10" }) - stub_with_valid_topic.should have_been_requested - end - - it "should not send to invalid topics" do - fcm = FCM.new(api_key) - fcm.send_to_topic(invalid_topic, data: { score: "5x1", time: "15:10" }) - stub_with_invalid_topic.should_not have_been_requested - end - end - end - - context "sending notification to a topic condition" do - let!(:stub_with_valid_condition) do - stub_request(:post, send_url) - .with(body: '{"condition":"\'TopicA\' in topics && (\'TopicB\' in topics || \'TopicC\' in topics)","data":{"score":"5x1","time":"15:10"}}', - headers: valid_request_headers) - .to_return(status: 200, body: "", headers: {}) - end - let!(:stub_with_invalid_condition) do - stub_request(:post, send_url) - .with(body: '{"condition":"\'TopicA\' in topics and some other text (\'TopicB\' in topics || \'TopicC\' in topics)","data":{"score":"5x1","time":"15:10"}}', - headers: valid_request_headers) - .to_return(status: 200, body: "", headers: {}) - end - let!(:stub_with_invalid_condition_topic) do - stub_request(:post, send_url) - .with(body: '{"condition":"\'TopicA$\' in topics","data":{"score":"5x1","time":"15:10"}}', - headers: valid_request_headers) - .to_return(status: 200, body: "", headers: {}) - end - - describe "#send_to_topic_condition" do - it "should send the data in a post request to fcm" do - fcm = FCM.new(api_key) - fcm.send_to_topic_condition(valid_condition, data: { score: "5x1", time: "15:10" }) - stub_with_valid_condition.should have_been_requested - end - - it "should not send to invalid conditions" do - fcm = FCM.new(api_key) - fcm.send_to_topic_condition(invalid_condition, data: { score: "5x1", time: "15:10" }) - stub_with_invalid_condition.should_not have_been_requested - end - - it "should not send to invalid topics in a condition" do - fcm = FCM.new(api_key) - fcm.send_to_topic_condition(invalid_condition_topic, data: { score: "5x1", time: "15:10" }) - stub_with_invalid_condition_topic.should_not have_been_requested - end - end - end - - context "when send_notification responds with failure" do - let(:mock_request_attributes) do - { - body: valid_request_body.to_json, - headers: valid_request_headers, - } - end - - subject { FCM.new(api_key) } - - context "on failure code 400" do - before do - stub_request(:post, send_url).with( - mock_request_attributes - ).to_return( - # ref: https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream - body: "{}", - headers: {}, - status: 400, - ) - end - it "should not send notification due to 400" do - subject.send(registration_ids).should eq(body: "{}", - headers: {}, - response: "Only applies for JSON requests. Indicates that the request could not be parsed as JSON, or it contained invalid fields.", - status_code: 400) - end - end - - context "on failure code 401" do - before do - stub_request(:post, send_url).with( - mock_request_attributes - ).to_return( - # ref: https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream - body: "{}", - headers: {}, - status: 401, - ) - end - - it "should not send notification due to 401" do - subject.send(registration_ids).should eq(body: "{}", - headers: {}, - response: "There was an error authenticating the sender account.", - status_code: 401) - end - end - - context "on failure code 503" do - before do - stub_request(:post, send_url).with( - mock_request_attributes - ).to_return( - # ref: https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream - body: "{}", - headers: {}, - status: 503, - ) - end - - it "should not send notification due to 503" do - subject.send(registration_ids).should eq(body: "{}", - headers: {}, - response: "Server is temporarily unavailable.", - status_code: 503) - end - end - - context "on failure code 5xx" do - before do - stub_request(:post, send_url).with( - mock_request_attributes - ).to_return( - # ref: https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream - body: '{"body-key" => "Body value"}', - headers: { "header-key" => "Header value" }, - status: 599, - ) - end - - it "should not send notification due to 599" do - subject.send(registration_ids).should eq(body: '{"body-key" => "Body value"}', - headers: { "header-key" => "Header value" }, - response: "There was an internal error in the FCM server while trying to process the request.", - status_code: 599) - end - end - end - - context "when send_notification responds canonical_ids" do - let(:mock_request_attributes) do - { - body: valid_request_body.to_json, - headers: valid_request_headers, - } - end - - let(:valid_response_body_with_canonical_ids) do - { - failure: 0, canonical_ids: 1, results: [{ registration_id: "43", message_id: "0:1385025861956342%572c22801bb3" }], - } - end - - subject { FCM.new(api_key) } - - before do - stub_request(:post, send_url).with( - mock_request_attributes - ).to_return( - # ref: https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream - body: valid_response_body_with_canonical_ids.to_json, - headers: {}, - status: 200, - ) - end - - it "should contain canonical_ids" do - response = subject.send(registration_ids) - - response.should eq(headers: {}, - canonical_ids: [{ old: "42", new: "43" }], - not_registered_ids: [], - status_code: 200, - response: "success", - body: '{"failure":0,"canonical_ids":1,"results":[{"registration_id":"43","message_id":"0:1385025861956342%572c22801bb3"}]}') - end - end - - context "when send_notification responds with NotRegistered" do - subject { FCM.new(api_key) } - - let(:mock_request_attributes) do - { - body: valid_request_body.to_json, - headers: valid_request_headers, - } - end - - let(:valid_response_body_with_not_registered_ids) do - { - canonical_ids: 0, failure: 1, results: [{ error: "NotRegistered" }], - } - end - - before do - stub_request(:post, send_url).with( - mock_request_attributes - ).to_return( - body: valid_response_body_with_not_registered_ids.to_json, - headers: {}, - status: 200, - ) - end - - it "should contain not_registered_ids" do - response = subject.send(registration_ids) - response.should eq( - headers: {}, - canonical_ids: [], - not_registered_ids: registration_ids, - status_code: 200, - response: "success", - body: '{"canonical_ids":0,"failure":1,"results":[{"error":"NotRegistered"}]}', - ) - end - end - end - - describe "sending group notifications" do - # TODO: refactor to should_behave_like - let(:valid_request_headers) do - { - "Authorization" => "key=#{api_key}", - "Content-Type" => "application/json", - "Project-Id" => project_id, - } - end - let(:valid_response_body) do - { notification_key: "APA91bGHXQBB...9QgnYOEURwm0I3lmyqzk2TXQ" } - end - - let(:default_valid_request_body) do - { - registration_ids: registration_ids, - operation: "create", - notification_key_name: key_name, - } - end - - subject { FCM.new(api_key) } - - # ref: https://firebase.google.com/docs/cloud-messaging/notifications#managing-device-groups-on-the-app-server - context "create" do - let(:valid_request_body) do - default_valid_request_body.merge({ - operation: "create", - }) - end - - let(:mock_request_attributes) do - { - body: valid_request_body.to_json, - headers: valid_request_headers, - } - end - - before do - stub_request(:post, group_notification_base_uri).with( - mock_request_attributes - ).to_return( - body: valid_response_body.to_json, - headers: {}, - status: 200, - ) - end - - it "should send a post request" do - response = subject.create(key_name, project_id, registration_ids) - response.should eq( - headers: {}, - status_code: 200, - response: "success", - body: valid_response_body.to_json, - ) - end - end # create context - - context "add" do - let(:valid_request_body) do - default_valid_request_body.merge({ - operation: "add", - notification_key: notification_key, - }) - end - - let(:mock_request_attributes) do - { - body: valid_request_body.to_json, - headers: valid_request_headers, - } - end - - before do - stub_request(:post, group_notification_base_uri).with( - mock_request_attributes - ).to_return( - body: valid_response_body.to_json, - headers: {}, - status: 200, - ) - end - - it "should send a post request" do - response = subject.add(key_name, project_id, notification_key, registration_ids) - response.should eq( - headers: {}, - status_code: 200, - response: "success", - body: valid_response_body.to_json, - ) - end - end # add context - - context "remove" do - let(:valid_request_body) do - default_valid_request_body.merge({ - operation: "remove", - notification_key: notification_key, - }) - end - - let(:mock_request_attributes) do - { - body: valid_request_body.to_json, - headers: valid_request_headers, - } - end - - before do - stub_request(:post, group_notification_base_uri).with( - mock_request_attributes - ).to_return( - body: valid_response_body.to_json, - headers: {}, - status: 200, - ) - end - - it "should send a post request" do - response = subject.remove(key_name, project_id, notification_key, registration_ids) - response.should eq( - headers: {}, - status_code: 200, - response: "success", - body: valid_response_body.to_json, - ) - end - end # remove context - end - - describe "#recover_notification_key" do - it "sends a 'retrieve notification key' request" do - uri = "#{FCM::GROUP_NOTIFICATION_BASE_URI}/gcm/notification" - endpoint = stub_request(:get, uri).with( - headers: { - "Content-Type" => "application/json", - "Authorization" => "key=TEST_SERVER_KEY", - "project_id" => "TEST_PROJECT_ID", - }, - query: { notification_key_name: "TEST_KEY_NAME" }, - ) - client = FCM.new("TEST_SERVER_KEY") - - client.recover_notification_key("TEST_KEY_NAME", "TEST_PROJECT_ID") - - expect(endpoint).to have_been_requested - end - end - - describe "subscribing to a topic" do - # TODO - end - - describe 'getting instance info' do + describe "#get_instance_id_info" do subject(:get_info) { client.get_instance_id_info(registration_id, options) } + let(:client) { FCM.new(api_key, json_key_path) } let(:options) { nil } - let(:client) { FCM.new('TEST_SERVER_KEY') } let(:base_uri) { "#{FCM::INSTANCE_ID_API}/iid/info" } let(:uri) { "#{base_uri}/#{registration_id}" } - let(:mock_request_attributes) do - { headers: { - 'Authorization' => 'key=TEST_SERVER_KEY', - 'Content-Type' => 'application/json' - } } + + before do + allow(client).to receive(:json_key) end context 'without options' do it 'calls info endpoint' do - endpoint = stub_request(:get, uri).with(mock_request_attributes) + endpoint = stub_request(:get, uri).with(headers: mock_headers) get_info expect(endpoint).to have_been_requested end @@ -594,22 +128,48 @@ let(:options) { { details: true } } it 'calls info endpoint' do - endpoint = stub_request(:get, uri).with(mock_request_attributes) + endpoint = stub_request(:get, uri).with(headers: mock_headers) get_info expect(endpoint).to have_been_requested end end end - describe "credentials path" do - it "can be a path to a file" do - fcm = FCM.new("test", "README.md") - expect(fcm.__send__(:json_key).class).to eq(File) + describe "topic subscriptions" do + describe "#subscribe_instance_id_to_topic" do + subject(:subscribe) { client.subscribe_instance_id_to_topic(registration_id, valid_topic) } + + let(:client) { FCM.new(api_key, json_key_path) } + let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchAdd" } + let(:params) { { to: "/topics/#{valid_topic}", registration_tokens: [registration_id] } } + + before do + allow(client).to receive(:json_key) + end + + it 'subscribes to a topic' do + endpoint = stub_request(:post, uri).with(body: params.to_json, headers: mock_headers) + subscribe + expect(endpoint).to have_been_requested + end end - it "can be an IO object" do - fcm = FCM.new("test", StringIO.new("hey")) - expect(fcm.__send__(:json_key).class).to eq(StringIO) + describe "#unsubscribe_instance_id_from_topic" do + subject(:unsubscribe) { client.unsubscribe_instance_id_from_topic(registration_id, valid_topic) } + + let(:client) { FCM.new(api_key, json_key_path) } + let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchRemove" } + let(:params) { { to: "/topics/#{valid_topic}", registration_tokens: [registration_id] } } + + before do + allow(client).to receive(:json_key) + end + + it 'unsubscribes from a topic' do + endpoint = stub_request(:post, uri).with(body: params.to_json, headers: mock_headers) + unsubscribe + expect(endpoint).to have_been_requested + end end end end From 9c299ce0f776234a250470204167c83fd0b2658e Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Thu, 5 Sep 2024 16:57:30 +0100 Subject: [PATCH 04/19] Clean up around fcm_spec --- spec/fcm_spec.rb | 184 +++++++++++++++++++++++++++++++---------------- 1 file changed, 123 insertions(+), 61 deletions(-) diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index b206fed..016ed8a 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -1,21 +1,10 @@ require "spec_helper" describe FCM do - let(:group_notification_base_uri) { "#{FCM::GROUP_NOTIFICATION_BASE_URI}/gcm/notification" } - let(:api_key) { "LEGACY_KEY" } - let(:registration_id) { "42" } - let(:registration_ids) { ["42"] } - let(:key_name) { "appUser-Chris" } - let(:project_id) { "123456789" } # https://developers.google.com/cloud-messaging/gcm#senderid - let(:notification_key) { "APA91bGHXQBB...9QgnYOEURwm0I3lmyqzk2TXQ" } - let(:valid_topic) { "TopicA" } - let(:invalid_topic) { "TopicA$" } - let(:valid_condition) { "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)" } - let(:invalid_condition) { "'TopicA' in topics and some other text ('TopicB' in topics || 'TopicC' in topics)" } - let(:invalid_condition_topic) { "'TopicA$' in topics" } - let(:project_name) { 'test-project' } let(:json_key_path) { 'path/to/json/key.json' } + let(:api_key) { "LEGACY_KEY" } + let(:client) { FCM.new(api_key, json_key_path) } let(:mock_token) { "access_token" } let(:mock_headers) do @@ -26,13 +15,15 @@ end before do + allow(client).to receive(:json_key) + # Mock the Google::Auth::ServiceAccountCredentials allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds). and_return(double(fetch_access_token!: { 'access_token' => mock_token })) end it "should initialize" do - expect { FCM.new(api_key, json_key_path) }.not_to raise_error + expect { client }.not_to raise_error end describe "credentials path" do @@ -51,31 +42,6 @@ let(:client) { FCM.new(api_key, json_key_path, project_name) } let(:uri) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } - let(:send_v1_params) do - { - 'token' => '4sdsx', - 'notification' => { - 'title' => 'Breaking News', - 'body' => 'New news story available.' - }, - 'data' => { - 'story_id' => 'story_12345' - }, - 'android' => { - 'notification' => { - 'click_action' => 'TOP_STORY_ACTIVITY', - 'body' => 'Check out the Top Story' - } - }, - 'apns' => { - 'payload' => { - 'aps' => { - 'category' => 'NEW_MESSAGE_CATEGORY' - } - } - } - } - end let(:stub_fcm_send_v1_request) do stub_request(:post, uri).with( @@ -90,31 +56,137 @@ end before do - allow(client).to receive(:json_key) - stub_fcm_send_v1_request end - it 'should send notification of HTTP V1 using POST to FCM server' do - client.send_v1(send_v1_params).should eq( - response: 'success', body: '{}', headers: {}, status_code: 200 - ) - stub_fcm_send_v1_request.should have_been_made.times(1) + shared_examples "succesfuly send notification" do + it 'should send notification of HTTP V1 using POST to FCM server' do + client.send_v1(send_v1_params).should eq( + response: 'success', body: '{}', headers: {}, status_code: 200 + ) + stub_fcm_send_v1_request.should have_been_made.times(1) + end + end + + describe "send to token" do + let(:token) { '4sdsx' } + let(:send_v1_params) do + { + 'token' => token, + 'notification' => { + 'title' => 'Breaking News', + 'body' => 'New news story available.' + }, + 'data' => { + 'story_id' => 'story_12345' + }, + 'android' => { + 'notification' => { + 'click_action' => 'TOP_STORY_ACTIVITY', + 'body' => 'Check out the Top Story' + } + }, + 'apns' => { + 'payload' => { + 'aps' => { + 'category' => 'NEW_MESSAGE_CATEGORY' + } + } + } + } + end + + include_examples "succesfuly send notification" + end + + describe "send to topic" do + let(:topic) { 'news' } + let(:send_v1_params) do + { + 'topic' => topic, + 'notification' => { + 'title' => 'Breaking News', + 'body' => 'New news story available.' + }, + } + end + + include_examples "succesfuly send notification" + + context "when topic is invalid" do + let(:topic) { '/topics/news$' } + + it 'should raise error' do + stub_fcm_send_v1_request.should_not have_been_requested + end + end + end + end + + describe '#send_with_notification_key' do + let(:notification_key) { 'notification_key_123' } + let(:message_options) do + { + notification: { + title: 'Group Notification', + body: 'This is a test group notification' + }, + data: { + key1: 'value1', + key2: 'value2' + } + } + end + + it 'sends a group notification successfully' do + expected_body = { + to: notification_key, + notification: { + title: 'Group Notification', + body: 'This is a test group notification' + }, + data: { + key1: 'value1', + key2: 'value2' + } + } + + stub_request(:post, "#{FCM::BASE_URI}/fcm/send") + .with( + body: expected_body.to_json, + headers: mock_headers + ) + .to_return(status: 200, body: '{"message_id": 987654321, "success": 3, "failure": 0}', headers: {}) + + response = client.send_with_notification_key(notification_key, message_options) + + expect(response[:status_code]).to eq(200) + expect(response[:response]).to eq('success') + parsed_body = JSON.parse(response[:body]) + expect(parsed_body['message_id']).to eq(987654321) + expect(parsed_body['success']).to eq(3) + expect(parsed_body['failure']).to eq(0) + end + + it 'handles errors when sending a group notification' do + stub_request(:post, "#{FCM::BASE_URI}/fcm/send") + .to_return(status: 400, body: '{"error": "InvalidRegistration"}', headers: {}) + + response = client.send_with_notification_key(notification_key, message_options) + + expect(response[:status_code]).to eq(400) + expect(response[:response]).to eq('Only applies for JSON requests. Indicates that the request could not be parsed as JSON, or it contained invalid fields.') + expect(JSON.parse(response[:body])['error']).to eq('InvalidRegistration') end end describe "#get_instance_id_info" do subject(:get_info) { client.get_instance_id_info(registration_id, options) } - let(:client) { FCM.new(api_key, json_key_path) } let(:options) { nil } let(:base_uri) { "#{FCM::INSTANCE_ID_API}/iid/info" } let(:uri) { "#{base_uri}/#{registration_id}" } - before do - allow(client).to receive(:json_key) - end - context 'without options' do it 'calls info endpoint' do endpoint = stub_request(:get, uri).with(headers: mock_headers) @@ -139,14 +211,9 @@ describe "#subscribe_instance_id_to_topic" do subject(:subscribe) { client.subscribe_instance_id_to_topic(registration_id, valid_topic) } - let(:client) { FCM.new(api_key, json_key_path) } let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchAdd" } let(:params) { { to: "/topics/#{valid_topic}", registration_tokens: [registration_id] } } - before do - allow(client).to receive(:json_key) - end - it 'subscribes to a topic' do endpoint = stub_request(:post, uri).with(body: params.to_json, headers: mock_headers) subscribe @@ -157,14 +224,9 @@ describe "#unsubscribe_instance_id_from_topic" do subject(:unsubscribe) { client.unsubscribe_instance_id_from_topic(registration_id, valid_topic) } - let(:client) { FCM.new(api_key, json_key_path) } let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchRemove" } let(:params) { { to: "/topics/#{valid_topic}", registration_tokens: [registration_id] } } - before do - allow(client).to receive(:json_key) - end - it 'unsubscribes from a topic' do endpoint = stub_request(:post, uri).with(body: params.to_json, headers: mock_headers) unsubscribe From 5c548aedca3c55d648a5e95fe6dcb3076e7856f3 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Thu, 5 Sep 2024 16:59:33 +0100 Subject: [PATCH 05/19] Add missing topic and registration_id specs --- spec/fcm_spec.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index 016ed8a..e7a308b 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -186,6 +186,7 @@ let(:options) { nil } let(:base_uri) { "#{FCM::INSTANCE_ID_API}/iid/info" } let(:uri) { "#{base_uri}/#{registration_id}" } + let(:registration_id) { "42" } context 'without options' do it 'calls info endpoint' do @@ -208,11 +209,14 @@ end describe "topic subscriptions" do + let(:topic) { 'news' } + let(:registration_id) { "42" } + describe "#subscribe_instance_id_to_topic" do - subject(:subscribe) { client.subscribe_instance_id_to_topic(registration_id, valid_topic) } + subject(:subscribe) { client.subscribe_instance_id_to_topic(registration_id, topic) } let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchAdd" } - let(:params) { { to: "/topics/#{valid_topic}", registration_tokens: [registration_id] } } + let(:params) { { to: "/topics/#{topic}", registration_tokens: [registration_id] } } it 'subscribes to a topic' do endpoint = stub_request(:post, uri).with(body: params.to_json, headers: mock_headers) @@ -222,10 +226,10 @@ end describe "#unsubscribe_instance_id_from_topic" do - subject(:unsubscribe) { client.unsubscribe_instance_id_from_topic(registration_id, valid_topic) } + subject(:unsubscribe) { client.unsubscribe_instance_id_from_topic(registration_id, topic) } let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchRemove" } - let(:params) { { to: "/topics/#{valid_topic}", registration_tokens: [registration_id] } } + let(:params) { { to: "/topics/#{topic}", registration_tokens: [registration_id] } } it 'unsubscribes from a topic' do endpoint = stub_request(:post, uri).with(body: params.to_json, headers: mock_headers) From d7f769d45240c4a7982b482fe5facb305ddb5dfe Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Thu, 5 Sep 2024 17:08:17 +0100 Subject: [PATCH 06/19] Update README.md --- README.md | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index e4e239b..4c00a1e 100644 --- a/README.md +++ b/README.md @@ -100,32 +100,6 @@ message = { fcm.send_v1(message) ``` -## HTTP Legacy Version - -To migrate to HTTP v1 see: https://firebase.google.com/docs/cloud-messaging/migrate-v1 - -For your server to send a message to one or more devices, you must first initialise a new `FCM` class with your Firebase Cloud Messaging server key, and then call the `send` method on this and give it 1 or more (up to 1000) registration tokens as an array of strings. You can also optionally send further [HTTP message parameters](https://firebase.google.com/docs/cloud-messaging/http-server-ref) like `data` or `time_to_live` etc. as a hash via the second optional argument to `send`. - -Example sending notifications: - -```ruby -require 'fcm' - -fcm = FCM.new("my_server_key") - -registration_ids= ["12", "13"] # an array of one or more client registration tokens - -# See https://firebase.google.com/docs/cloud-messaging/http-server-ref for all available options. -options = { "notification": { - "title": "Portugal vs. Denmark", - "body": "5 to 1" - } -} -response = fcm.send(registration_ids, options) -``` - -Currently `response` is just a hash containing the response `body`, `headers` and `status_code`. Check [here](https://firebase.google.com/docs/cloud-messaging/server#response) to see how to interpret the responses. - ## Device Group Messaging With [device group messaging](https://firebase.google.com/docs/cloud-messaging/notifications), you can send a single message to multiple instance of an app running on devices belonging to a group. Typically, "group" refers a set of different devices that belong to a single user. However, a group could also represent a set of devices where the app instance functions in a highly correlated manner. To use this feature, you will first need an initialised `FCM` class. @@ -245,6 +219,9 @@ The guide to set up an iOS app to get notifications is here: [Setting up a FCM C ## ChangeLog +### 2.0.0 +- Remove deprecated `send` method (Breaking change) + ### 1.0.8 - caches calls to `Google::Auth::ServiceAccountCredentials` #103 - Allow `faraday` versions from 1 up to 2 #101 From e78c73008fb7cc1567950633e7b8541db6e60efb Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Thu, 5 Sep 2024 17:14:15 +0100 Subject: [PATCH 07/19] Revert version change before publish --- README.md | 2 +- fcm.gemspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4c00a1e..31a7414 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ The guide to set up an iOS app to get notifications is here: [Setting up a FCM C ## ChangeLog -### 2.0.0 +### xxx - Remove deprecated `send` method (Breaking change) ### 1.0.8 diff --git a/fcm.gemspec b/fcm.gemspec index 9d4f8c4..05a211e 100644 --- a/fcm.gemspec +++ b/fcm.gemspec @@ -3,7 +3,7 @@ $:.push File.expand_path("../lib", __FILE__) Gem::Specification.new do |s| s.name = "fcm" - s.version = "2.0.0" + s.version = "1.0.8" s.platform = Gem::Platform::RUBY s.authors = ["Kashif Rasul", "Shoaib Burq"] s.email = ["kashif@decision-labs.com", "shoaib@decision-labs.com"] From 3343fdf058ebca8498edc865df9fa78ab8ede6a3 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Thu, 5 Sep 2024 17:15:20 +0100 Subject: [PATCH 08/19] Add ruby 3.3 support to CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bcc924..b60a827 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - ruby: ['2.7', '3.0', '3.1'] + ruby: ['2.7', '3.0', '3.1', '3.3'] steps: - uses: actions/checkout@master From 3894d81ea33f00075b0ae71abded8a83ee471c90 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 9 Sep 2024 11:32:43 +0100 Subject: [PATCH 09/19] Update send_to topic and condition methods --- lib/fcm.rb | 41 ++++++++++++++++--------------- spec/fcm_spec.rb | 64 +++++++++--------------------------------------- 2 files changed, 32 insertions(+), 73 deletions(-) diff --git a/lib/fcm.rb b/lib/fcm.rb index a8cd2f8..b966407 100644 --- a/lib/fcm.rb +++ b/lib/fcm.rb @@ -122,11 +122,6 @@ def recover_notification_key(key_name, project_id) end end - def send_with_notification_key(notification_key, options = {}) - body = { to: notification_key }.merge(options) - execute_notification(body) - end - def topic_subscription(topic, registration_id) for_uri(INSTANCE_ID_API) do |connection| response = connection.post("/iid/v1/#{registration_id}/rel/topics/#{topic}") @@ -151,12 +146,6 @@ def manage_topics_relationship(topic, registration_ids, action) end end - def send_to_topic(topic, options = {}) - if topic.gsub(TOPIC_REGEX, "").length == 0 - send_with_notification_key("/topics/" + topic, options) - end - end - def get_instance_id_info(iid_token, options = {}) params = options @@ -182,10 +171,29 @@ def batch_unsubscribe_instance_ids_from_topic(instance_ids, topic_name) manage_topics_relationship(topic_name, instance_ids, "Remove") end + def send_to_topic(topic, options = {}) + if topic.gsub(TOPIC_REGEX, "").length == 0 + body = { 'message': { 'topic': topic }.merge(options) } + + for_uri(BASE_URI_V1) do |connection| + response = connection.post( + "#{@project_name}/messages:send", body.to_json + ) + build_response(response) + end + end + end + def send_to_topic_condition(condition, options = {}) if validate_condition?(condition) - body = { condition: condition }.merge(options) - execute_notification(body) + body = { 'message': { 'condition': condition }.merge(options) } + + for_uri(BASE_URI_V1) do |connection| + response = connection.post( + "#{@project_name}/messages:send", body.to_json + ) + build_response(response) + end end end @@ -257,13 +265,6 @@ def build_not_registered_ids(body, registration_id) not_registered_ids end - def execute_notification(body) - for_uri(BASE_URI) do |connection| - response = connection.post("/fcm/send", body.to_json) - build_response(response) - end - end - def has_canonical_id?(result) !result["registration_id"].nil? end diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index e7a308b..24f16a1 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -121,62 +121,20 @@ end end end - end - describe '#send_with_notification_key' do - let(:notification_key) { 'notification_key_123' } - let(:message_options) do - { - notification: { - title: 'Group Notification', - body: 'This is a test group notification' - }, - data: { - key1: 'value1', - key2: 'value2' - } - } - end - - it 'sends a group notification successfully' do - expected_body = { - to: notification_key, - notification: { - title: 'Group Notification', - body: 'This is a test group notification' - }, - data: { - key1: 'value1', - key2: 'value2' + describe "send to condition" do + let(:condition) { "'foo' in topics" } + let(:send_v1_params) do + { + 'condition' => condition, + 'notification' => { + 'title' => 'Breaking News', + 'body' => 'New news story available.' + }, } - } - - stub_request(:post, "#{FCM::BASE_URI}/fcm/send") - .with( - body: expected_body.to_json, - headers: mock_headers - ) - .to_return(status: 200, body: '{"message_id": 987654321, "success": 3, "failure": 0}', headers: {}) - - response = client.send_with_notification_key(notification_key, message_options) - - expect(response[:status_code]).to eq(200) - expect(response[:response]).to eq('success') - parsed_body = JSON.parse(response[:body]) - expect(parsed_body['message_id']).to eq(987654321) - expect(parsed_body['success']).to eq(3) - expect(parsed_body['failure']).to eq(0) - end - - it 'handles errors when sending a group notification' do - stub_request(:post, "#{FCM::BASE_URI}/fcm/send") - .to_return(status: 400, body: '{"error": "InvalidRegistration"}', headers: {}) - - response = client.send_with_notification_key(notification_key, message_options) + end - expect(response[:status_code]).to eq(400) - expect(response[:response]).to eq('Only applies for JSON requests. Indicates that the request could not be parsed as JSON, or it contained invalid fields.') - expect(JSON.parse(response[:body])['error']).to eq('InvalidRegistration') + include_examples "succesfuly send notification" end end From 4076d71fbfdd2447a819b25e994163b75439713a Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 9 Sep 2024 11:36:54 +0100 Subject: [PATCH 10/19] Add extra test for methods send_to topic and condition --- spec/fcm_spec.rb | 84 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index 24f16a1..61c2acc 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -138,6 +138,90 @@ end end + describe "#send_to_topic" do + let(:client) { FCM.new(api_key, json_key_path, project_name) } + + let(:uri) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } + + let(:topic) { 'news' } + let(:params) do + { + 'topic' => topic + }.merge(options) + end + let(:options) do + { + 'data' => { + 'story_id' => 'story_12345' + } + } + end + + let(:stub_fcm_send_to_topic_request) do + stub_request(:post, uri).with( + body: { 'message' => params }.to_json, + headers: mock_headers + ).to_return( + body: "{}", + headers: {}, + status: 200, + ) + end + + before do + stub_fcm_send_to_topic_request + end + + it 'should send notification to topic using POST to FCM server' do + client.send_to_topic(topic, options).should eq( + response: 'success', body: '{}', headers: {}, status_code: 200 + ) + stub_fcm_send_to_topic_request.should have_been_made.times(1) + end + end + + describe "#send_to_topic_condition" do + let(:client) { FCM.new(api_key, json_key_path, project_name) } + + let(:uri) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } + + let(:topic_condition) { "'foo' in topics" } + let(:params) do + { + 'condition' => topic_condition + }.merge(options) + end + let(:options) do + { + 'data' => { + 'story_id' => 'story_12345' + } + } + end + + let(:stub_fcm_send_to_topic_condition_request) do + stub_request(:post, uri).with( + body: { 'message' => params }.to_json, + headers: mock_headers + ).to_return( + body: "{}", + headers: {}, + status: 200, + ) + end + + before do + stub_fcm_send_to_topic_condition_request + end + + it 'should send notification to topic_condition using POST to FCM server' do + client.send_to_topic_condition(topic_condition, options).should eq( + response: 'success', body: '{}', headers: {}, status_code: 200 + ) + stub_fcm_send_to_topic_condition_request.should have_been_made.times(1) + end + end + describe "#get_instance_id_info" do subject(:get_info) { client.get_instance_id_info(registration_id, options) } From 423b13e113bf83a0aef27d105c2a9bf85833bd7a Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 9 Sep 2024 13:07:54 +0100 Subject: [PATCH 11/19] Remove API key --- lib/fcm.rb | 4 ++-- spec/fcm_spec.rb | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/fcm.rb b/lib/fcm.rb index b966407..b7477d4 100644 --- a/lib/fcm.rb +++ b/lib/fcm.rb @@ -12,7 +12,7 @@ class FCM INSTANCE_ID_API = "https://iid.googleapis.com" TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/ - def initialize(_api_key = nil, json_key_path = "", project_name = "") + def initialize(json_key_path = "", project_name = "") @json_key_path = json_key_path @project_name = project_name end @@ -41,7 +41,7 @@ def initialize(_api_key = nil, json_key_path = "", project_name = "") # } # } # } - # fcm = FCM.new(_api_key, json_key_path, project_name) + # fcm = FCM.new(json_key_path, project_name) # fcm.send_v1( # { "token": "4sdsx",, "to" : "notification": {}.. } # ) diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index 61c2acc..be5d30a 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -3,8 +3,7 @@ describe FCM do let(:project_name) { 'test-project' } let(:json_key_path) { 'path/to/json/key.json' } - let(:api_key) { "LEGACY_KEY" } - let(:client) { FCM.new(api_key, json_key_path) } + let(:client) { FCM.new(json_key_path) } let(:mock_token) { "access_token" } let(:mock_headers) do @@ -28,18 +27,18 @@ describe "credentials path" do it "can be a path to a file" do - fcm = FCM.new("test", "README.md") + fcm = FCM.new("README.md") expect(fcm.__send__(:json_key).class).to eq(File) end it "can be an IO object" do - fcm = FCM.new("test", StringIO.new("hey")) + fcm = FCM.new(StringIO.new("hey")) expect(fcm.__send__(:json_key).class).to eq(StringIO) end end describe "#send_v1 or #send_notification_v1" do - let(:client) { FCM.new(api_key, json_key_path, project_name) } + let(:client) { FCM.new(json_key_path, project_name) } let(:uri) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } @@ -139,7 +138,7 @@ end describe "#send_to_topic" do - let(:client) { FCM.new(api_key, json_key_path, project_name) } + let(:client) { FCM.new(json_key_path, project_name) } let(:uri) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } @@ -181,7 +180,7 @@ end describe "#send_to_topic_condition" do - let(:client) { FCM.new(api_key, json_key_path, project_name) } + let(:client) { FCM.new(json_key_path, project_name) } let(:uri) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } From 3ae0087161f5d5da3992191390a4ca6c240020c3 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 9 Sep 2024 13:43:50 +0100 Subject: [PATCH 12/19] Clean up on topic subscriptions --- .gitignore | 1 + lib/fcm.rb | 36 ++++++++++++------------------------ 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index a7f76bf..794e566 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,4 @@ Gemfile.lock .rvmrc spec/reports *.gem +.env diff --git a/lib/fcm.rb b/lib/fcm.rb index b7477d4..1374540 100644 --- a/lib/fcm.rb +++ b/lib/fcm.rb @@ -122,23 +122,27 @@ def recover_notification_key(key_name, project_id) end end - def topic_subscription(topic, registration_id) + def topic_subscription(topic, registration_token) for_uri(INSTANCE_ID_API) do |connection| - response = connection.post("/iid/v1/#{registration_id}/rel/topics/#{topic}") + response = connection.post("/iid/v1/#{registration_token}/rel/topics/#{topic}") build_response(response) end end - def batch_topic_subscription(topic, registration_ids) - manage_topics_relationship(topic, registration_ids, "Add") + def topic_unsubscription(topic, registration_token) + batch_topic_unsubscription(topic, [registration_token]) end - def batch_topic_unsubscription(topic, registration_ids) - manage_topics_relationship(topic, registration_ids, "Remove") + def batch_topic_subscription(topic, registration_tokens) + manage_topics_relationship(topic, registration_tokens, "Add") end - def manage_topics_relationship(topic, registration_ids, action) - body = { to: "/topics/#{topic}", registration_tokens: registration_ids } + def batch_topic_unsubscription(topic, registration_tokens) + manage_topics_relationship(topic, registration_tokens, "Remove") + end + + def manage_topics_relationship(topic, registration_tokens, action) + body = { to: "/topics/#{topic}", registration_tokens: registration_tokens } for_uri(INSTANCE_ID_API) do |connection| response = connection.post("/iid/v1:batch#{action}", body.to_json) @@ -155,22 +159,6 @@ def get_instance_id_info(iid_token, options = {}) end end - def subscribe_instance_id_to_topic(iid_token, topic_name) - batch_subscribe_instance_ids_to_topic([iid_token], topic_name) - end - - def unsubscribe_instance_id_from_topic(iid_token, topic_name) - batch_unsubscribe_instance_ids_from_topic([iid_token], topic_name) - end - - def batch_subscribe_instance_ids_to_topic(instance_ids, topic_name) - manage_topics_relationship(topic_name, instance_ids, "Add") - end - - def batch_unsubscribe_instance_ids_from_topic(instance_ids, topic_name) - manage_topics_relationship(topic_name, instance_ids, "Remove") - end - def send_to_topic(topic, options = {}) if topic.gsub(TOPIC_REGEX, "").length == 0 body = { 'message': { 'topic': topic }.merge(options) } From 0fa18c734d28a7810373093a1fd55e07d16dd5e7 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 9 Sep 2024 14:00:36 +0100 Subject: [PATCH 13/19] Adding more specs for batch topic actions --- spec/fcm_spec.rb | 53 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index be5d30a..48ff685 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -222,12 +222,12 @@ end describe "#get_instance_id_info" do - subject(:get_info) { client.get_instance_id_info(registration_id, options) } + subject(:get_info) { client.get_instance_id_info(registration_token, options) } let(:options) { nil } let(:base_uri) { "#{FCM::INSTANCE_ID_API}/iid/info" } - let(:uri) { "#{base_uri}/#{registration_id}" } - let(:registration_id) { "42" } + let(:uri) { "#{base_uri}/#{registration_token}" } + let(:registration_token) { "42" } context 'without options' do it 'calls info endpoint' do @@ -238,7 +238,7 @@ end context 'with detail option' do - let(:uri) { "#{base_uri}/#{registration_id}?details=true" } + let(:uri) { "#{base_uri}/#{registration_token}?details=true" } let(:options) { { details: true } } it 'calls info endpoint' do @@ -251,26 +251,27 @@ describe "topic subscriptions" do let(:topic) { 'news' } - let(:registration_id) { "42" } + let(:registration_token) { "42" } + let(:registration_token_2) { "43" } + let(:registration_tokens) { [registration_token, registration_token_2] } - describe "#subscribe_instance_id_to_topic" do - subject(:subscribe) { client.subscribe_instance_id_to_topic(registration_id, topic) } + describe "#topic_subscription" do + subject(:subscribe) { client.topic_subscription(topic, registration_token) } - let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchAdd" } - let(:params) { { to: "/topics/#{topic}", registration_tokens: [registration_id] } } + let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1/#{registration_token}/rel/topics/#{topic}" } it 'subscribes to a topic' do - endpoint = stub_request(:post, uri).with(body: params.to_json, headers: mock_headers) + endpoint = stub_request(:post, uri).with(headers: mock_headers) subscribe expect(endpoint).to have_been_requested end end - describe "#unsubscribe_instance_id_from_topic" do - subject(:unsubscribe) { client.unsubscribe_instance_id_from_topic(registration_id, topic) } + describe "#topic_unsubscription" do + subject(:unsubscribe) { client.topic_unsubscription(topic, registration_token) } let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchRemove" } - let(:params) { { to: "/topics/#{topic}", registration_tokens: [registration_id] } } + let(:params) { { to: "/topics/#{topic}", registration_tokens: [registration_token] } } it 'unsubscribes from a topic' do endpoint = stub_request(:post, uri).with(body: params.to_json, headers: mock_headers) @@ -278,5 +279,31 @@ expect(endpoint).to have_been_requested end end + + describe "#batch_topic_subscription" do + subject(:batch_subscribe) { client.batch_topic_subscription(topic, registration_tokens) } + + let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchAdd" } + let(:params) { { to: "/topics/#{topic}", registration_tokens: registration_tokens } } + + it 'subscribes to a topic' do + endpoint = stub_request(:post, uri).with(body: params.to_json, headers: mock_headers) + batch_subscribe + expect(endpoint).to have_been_requested + end + end + + describe "#batch_topic_unsubscription" do + subject(:batch_unsubscribe) { client.batch_topic_unsubscription(topic, registration_tokens) } + + let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchRemove" } + let(:params) { { to: "/topics/#{topic}", registration_tokens: registration_tokens } } + + it 'unsubscribes from a topic' do + endpoint = stub_request(:post, uri).with(body: params.to_json, headers: mock_headers) + batch_unsubscribe + expect(endpoint).to have_been_requested + end + end end end From d6666d79b8e4554df2d4527914de6cd5fed289f4 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 9 Sep 2024 14:00:42 +0100 Subject: [PATCH 14/19] Update README.md --- README.md | 99 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 31a7414..2194915 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ To use this gem, you need to instantiate a client with your firebase credentials ```ruby fcm = FCM.new( - API_TOKEN, GOOGLE_APPLICATION_CREDENTIALS_PATH, FIREBASE_PROJECT_ID ) @@ -40,7 +39,6 @@ The easiest way to provide them is to pass here an absolute path to a file with ```ruby fcm = FCM.new( - API_TOKEN, '/path/to/credentials.json', FIREBASE_PROJECT_ID ) @@ -50,7 +48,6 @@ As per their secret nature, you might not want to have them in your repository. ```ruby fcm = FCM.new( - API_TOKEN, StringIO.new(ENV.fetch('FIREBASE_CREDENTIALS')), FIREBASE_PROJECT_ID ) @@ -65,13 +62,13 @@ To migrate to HTTP v1 see: https://firebase.google.com/docs/cloud-messaging/migr ```ruby fcm = FCM.new( - API_TOKEN, GOOGLE_APPLICATION_CREDENTIALS_PATH, FIREBASE_PROJECT_ID ) message = { - 'topic': "89023", # OR token if you want to send to a specific device - # 'token': "000iddqd", + 'token': "000iddqd", # send to a specific device + # 'topic': "yourTopic", + # 'condition': "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)", 'data': { payload: { data: { @@ -97,7 +94,7 @@ message = { } } -fcm.send_v1(message) +fcm.send_v1(message) # or fcm.send_notification_v1(message) ``` ## Device Group Messaging @@ -115,14 +112,18 @@ params = {key_name: "appUser-Chris", response = fcm.create(*params.values) ``` -### Send to Notification Key +### Send to Notification device group -Now you can send a message to a particular `notification_key` via the `send_with_notification_key` method. This allows the server to send a single [data](https://firebase.google.com/docs/cloud-messaging/concept-options#data_messages) payload or/and [notification](https://firebase.google.com/docs/cloud-messaging/concept-options#notifications) payload to multiple app instances (typically on multiple devices) owned by a single user (instead of sending to some registration tokens). Note: the maximum number of members allowed for a `notification_key` is 20. +To send messages to device groups, use the HTTP v1 API, +Sending messages to a device group is very similar to sending messages to an individual device, using the same method to authorize send requests. Set the token field to the group notification key ```ruby -response = fcm.send_with_notification_key("notification_key", - data: {score: "3x1"}, - collapse_key: "updated_score") +message = { + 'token': "NOTIFICATION_KEY", # send to a device group + # ...data +} + +fcm.send_v1(message) ``` ### Add/Remove Registration Tokens @@ -145,23 +146,51 @@ response = fcm.remove(*params.values) ## Send Messages to Topics -FCM [topic messaging](https://firebase.google.com/docs/cloud-messaging/topic-messaging) allows your app server to send a message to multiple devices that have opted in to a particular topic. Based on the publish/subscribe model, topic messaging supports unlimited subscriptions per app. Sending to a topic is very similar to sending to an individual device or to a user group, in the sense that you can use the `fcm.send_with_notification_key()` method where the `notification_key` matches the regular expression `"/topics/[a-zA-Z0-9-_.~%]+"`: +FCM [topic messaging](https://firebase.google.com/docs/cloud-messaging/topic-messaging) allows your app server to send a message to multiple devices that have opted in to a particular topic. Based on the publish/subscribe model, one app instance can be subscribed to no more than 2000 topics. Sending to a topic is very similar to sending to an individual device or to a user group, in the sense that you can use the `fcm.send_v1` method where the `topic` matches the regular expression `"/topics/[a-zA-Z0-9-_.~%]+"`: ```ruby -response = fcm.send_with_notification_key("/topics/yourTopic", - notification: {body: "This is a FCM Topic Message!"}) +message = { + 'topic': "yourTopic", # send to a device group + # ...data +} + +fcm.send_v1(message) ``` -Or you can use the helper: +Or you can use the `fcm.send_to_topic` helper: ```ruby response = fcm.send_to_topic("yourTopic", notification: {body: "This is a FCM Topic Message!"}) ``` +## Send Messages to Topics with Conditions + +FCM [topic condition messaging](https://firebase.google.com/docs/cloud-messaging/android/topic-messaging#build_send_requests) to send a message to a combination of topics, specify a condition, which is a boolean expression that specifies the target topics. + +```ruby +message = { + 'condition': "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)", # send to topic condition + # ...data +} + +fcm.send_v1(message) +``` + +Or you can use the `fcm.send_to_topic_condition` helper: + +```ruby +response = fcm.send_to_topic_condition( + "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)", + notification: { + body: "This is an FCM Topic Message sent to a condition!" + } +) +``` + ### Sending to Multiple Topics -To send to combinations of multiple topics, the FCM [docs](https://firebase.google.com/docs/cloud-messaging/send-message#send_messages_to_topics_2) require that you set a **condition** key (instead of the `to:` key) to a boolean condition that specifies the target topics. For example, to send messages to devices that subscribed to _TopicA_ and either _TopicB_ or _TopicC_: +To send to combinations of multiple topics, require that you set a **condition** key to a boolean condition that specifies the target topics. For example, to send messages to devices that subscribed to _TopicA_ and either _TopicB_ or _TopicC_: ``` 'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics) @@ -197,18 +226,38 @@ Given a registration token and a topic name, you can add the token to the topic ```ruby topic = "YourTopic" -registration_id= "12" # a client registration tokens -response = fcm.topic_subscription(topic, registration_id) +registration_token= "12" # a client registration token +response = fcm.topic_subscription(topic, registration_token) +# or unsubscription +response = fcm.topic_unsubscription(topic, registration_token) ``` Or you can manage relationship maps for multiple app instances [Google Instance ID server API. Manage relationship](https://developers.google.com/instance-id/reference/server#manage_relationship_maps_for_multiple_app_instances) ```ruby topic = "YourTopic" -registration_ids= ["4", "8", "15", "16", "23", "42"] # an array of one or more client registration tokens -response = fcm.batch_topic_subscription(topic, registration_ids) +registration_tokens= ["4", "8", "15", "16", "23", "42"] # an array of one or more client registration tokens +response = fcm.batch_topic_subscription(topic, registration_tokens) # or unsubscription -response = fcm.batch_topic_unsubscription(topic, registration_ids) +response = fcm.batch_topic_unsubscription(topic, registration_tokens) +``` + +## Get Information about the Instance ID + +Given a registration token, you can retrieve information about the token using the [Google Instance ID server API](https://developers.google.com/instance-id/reference/server). + +```ruby +registration_token= "12" # a client registration token +response = fcm.get_instance_id_info(registration_token) +``` + +To get detailed information about the instance ID, you can pass an optional +`options` hash to the `get_instance_id_info` method: + +```ruby +registration_token= "12" # a client registration token +options = { "details" => true } +response = fcm.get_instance_id_info(registration_token, options) ``` ## Mobile Clients @@ -219,8 +268,12 @@ The guide to set up an iOS app to get notifications is here: [Setting up a FCM C ## ChangeLog -### xxx +### 2.0.0 +- Remove deprecated `API_KEY` (Breaking change) - Remove deprecated `send` method (Breaking change) +- Remove deprecated `send_with_notification_key` method (Breaking change) +- Add HTTP v1 API support for `send_to_topic_condition` method +- Add HTTP v1 API support for `send_to_topic` method ### 1.0.8 - caches calls to `Google::Auth::ServiceAccountCredentials` #103 From d3ca3c942e47b84de96a8b0bfd190112afeb89d1 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 9 Sep 2024 14:03:07 +0100 Subject: [PATCH 15/19] Update README.md --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2194915..d7e6702 100644 --- a/README.md +++ b/README.md @@ -269,9 +269,16 @@ The guide to set up an iOS app to get notifications is here: [Setting up a FCM C ## ChangeLog ### 2.0.0 -- Remove deprecated `API_KEY` (Breaking change) -- Remove deprecated `send` method (Breaking change) -- Remove deprecated `send_with_notification_key` method (Breaking change) +#### Breaking Changes +- Remove deprecated `API_KEY` +- Remove deprecated `send` method +- Remove deprecated `send_with_notification_key` method +- Remove `subscribe_instance_id_to_topic` method +- Remove `unsubscribe_instance_id_from_topic` method +- Remove `batch_subscribe_instance_ids_to_topic` method +- Remove `batch_unsubscribe_instance_ids_from_topic` method + +#### Supported Features - Add HTTP v1 API support for `send_to_topic_condition` method - Add HTTP v1 API support for `send_to_topic` method From ddd3adc1adde887dca86c9bca19263e18c7c8938 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 9 Sep 2024 14:09:40 +0100 Subject: [PATCH 16/19] Adjusting fcm.rb --- lib/fcm.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/fcm.rb b/lib/fcm.rb index 1374540..616512b 100644 --- a/lib/fcm.rb +++ b/lib/fcm.rb @@ -124,7 +124,9 @@ def recover_notification_key(key_name, project_id) def topic_subscription(topic, registration_token) for_uri(INSTANCE_ID_API) do |connection| - response = connection.post("/iid/v1/#{registration_token}/rel/topics/#{topic}") + response = connection.post( + "/iid/v1/#{registration_token}/rel/topics/#{topic}" + ) build_response(response) end end @@ -134,11 +136,11 @@ def topic_unsubscription(topic, registration_token) end def batch_topic_subscription(topic, registration_tokens) - manage_topics_relationship(topic, registration_tokens, "Add") + manage_topics_relationship(topic, registration_tokens, 'Add') end def batch_topic_unsubscription(topic, registration_tokens) - manage_topics_relationship(topic, registration_tokens, "Remove") + manage_topics_relationship(topic, registration_tokens, 'Remove') end def manage_topics_relationship(topic, registration_tokens, action) @@ -154,13 +156,13 @@ def get_instance_id_info(iid_token, options = {}) params = options for_uri(INSTANCE_ID_API) do |connection| - response = connection.get("/iid/info/" + iid_token, params) + response = connection.get("/iid/info/#{iid_token}", params) build_response(response) end end def send_to_topic(topic, options = {}) - if topic.gsub(TOPIC_REGEX, "").length == 0 + if topic.gsub(TOPIC_REGEX, '').zero? body = { 'message': { 'topic': topic }.merge(options) } for_uri(BASE_URI_V1) do |connection| From 15b5cfbe437675362108435989ff44cfa77aec85 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Mon, 9 Sep 2024 15:08:30 +0100 Subject: [PATCH 17/19] Add extra send_to specs --- README.md | 12 +++++++++--- spec/fcm_spec.rb | 32 +++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d7e6702..964b41a 100644 --- a/README.md +++ b/README.md @@ -101,14 +101,20 @@ fcm.send_v1(message) # or fcm.send_notification_v1(message) With [device group messaging](https://firebase.google.com/docs/cloud-messaging/notifications), you can send a single message to multiple instance of an app running on devices belonging to a group. Typically, "group" refers a set of different devices that belong to a single user. However, a group could also represent a set of devices where the app instance functions in a highly correlated manner. To use this feature, you will first need an initialised `FCM` class. +The maximum number of members allowed for a notification key is 20. +https://firebase.google.com/docs/cloud-messaging/android/device-group#managing_device_groups + ### Generate a Notification Key for device group Then you will need a notification key which you can create for a particular `key_name` which needs to be uniquely named per app in case you have multiple apps for the same `project_id`. This ensures that notifications only go to the intended target app. The `create` method will do this and return the token `notification_key`, that represents the device group, in the response: +`project_id` is the SENDER_ID in your cloud settings. +https://firebase.google.com/docs/cloud-messaging/concept-options#senderid + ```ruby -params = {key_name: "appUser-Chris", +params = { key_name: "appUser-Chris", project_id: "my_project_id", - registration_ids: ["4", "8", "15", "16", "23", "42"]} + registration_ids: ["4", "8", "15", "16", "23", "42"] } response = fcm.create(*params.values) ``` @@ -161,7 +167,7 @@ Or you can use the `fcm.send_to_topic` helper: ```ruby response = fcm.send_to_topic("yourTopic", - notification: {body: "This is a FCM Topic Message!"}) + notification: { body: "This is a FCM Topic Message!"} ) ``` ## Send Messages to Topics with Conditions diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index 48ff685..8cd3a54 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -98,6 +98,21 @@ include_examples "succesfuly send notification" end + describe "send to multiple tokens" do + let(:tokens) { ['4sdsx', '4sdsy'] } + let(:send_v1_params) do + { + 'token' => tokens, + 'notification' => { + 'title' => 'Breaking News', + 'body' => 'New news story available.' + } + } + end + + include_examples 'succesfuly send notification' + end + describe "send to topic" do let(:topic) { 'news' } let(:send_v1_params) do @@ -106,7 +121,7 @@ 'notification' => { 'title' => 'Breaking News', 'body' => 'New news story available.' - }, + } } end @@ -135,6 +150,21 @@ include_examples "succesfuly send notification" end + + describe "send to notification_key" do + let(:notification_key) { 'notification_key' } + let(:send_v1_params) do + { + 'notification_key' => notification_key, + 'notification' => { + 'title' => 'Breaking News', + 'body' => 'New news story available.' + } + } + end + + include_examples "succesfuly send notification" + end end describe "#send_to_topic" do From 7c6dcbe13f817b64366b33c7198e8fc483e6e33a Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Wed, 11 Sep 2024 08:57:07 +0100 Subject: [PATCH 18/19] Add missing .length on gsub matcher --- lib/fcm.rb | 2 +- spec/fcm_spec.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/fcm.rb b/lib/fcm.rb index 616512b..8c32219 100644 --- a/lib/fcm.rb +++ b/lib/fcm.rb @@ -162,7 +162,7 @@ def get_instance_id_info(iid_token, options = {}) end def send_to_topic(topic, options = {}) - if topic.gsub(TOPIC_REGEX, '').zero? + if topic.gsub(TOPIC_REGEX, '').length.zero? body = { 'message': { 'topic': topic }.merge(options) } for_uri(BASE_URI_V1) do |connection| diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index 8cd3a54..8e90ede 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -207,6 +207,15 @@ ) stub_fcm_send_to_topic_request.should have_been_made.times(1) end + + context "when topic is invalid" do + let(:topic) { '/topics/news$' } + + it 'should raise error' do + client.send_to_topic(topic, options) + stub_fcm_send_to_topic_request.should_not have_been_requested + end + end end describe "#send_to_topic_condition" do From 1abcd2cf45cc2fa0d5fb870cbc3f7824a04fd352 Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Wed, 11 Sep 2024 09:14:12 +0100 Subject: [PATCH 19/19] Adding more test cases --- spec/fcm_spec.rb | 118 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 14 deletions(-) diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index 8e90ede..a3f24e1 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -41,6 +41,7 @@ let(:client) { FCM.new(json_key_path, project_name) } let(:uri) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } + let(:status_code) { 200 } let(:stub_fcm_send_v1_request) do stub_request(:post, uri).with( @@ -50,7 +51,7 @@ # ref: https://firebase.google.com/docs/cloud-messaging/http-server-ref#interpret-downstream body: "{}", headers: {}, - status: 200, + status: status_code, ) end @@ -58,7 +59,7 @@ stub_fcm_send_v1_request end - shared_examples "succesfuly send notification" do + shared_examples 'succesfuly send notification' do it 'should send notification of HTTP V1 using POST to FCM server' do client.send_v1(send_v1_params).should eq( response: 'success', body: '{}', headers: {}, status_code: 200 @@ -67,7 +68,7 @@ end end - describe "send to token" do + describe 'send to token' do let(:token) { '4sdsx' } let(:send_v1_params) do { @@ -95,10 +96,20 @@ } end - include_examples "succesfuly send notification" + include_examples 'succesfuly send notification' + + it 'includes all the response' do + response = client.send_v1(send_v1_params) + expect(response[:status_code]).to eq(status_code) + expect(response[:response]).to eq('success') + expect(response[:body]).to eq('{}') + expect(response[:headers]).to eq({}) + expect(response[:canonical_ids]).to be_nil + expect(response[:not_registered_ids]).to be_nil + end end - describe "send to multiple tokens" do + describe 'send to multiple tokens' do let(:tokens) { ['4sdsx', '4sdsy'] } let(:send_v1_params) do { @@ -113,7 +124,7 @@ include_examples 'succesfuly send notification' end - describe "send to topic" do + describe 'send to topic' do let(:topic) { 'news' } let(:send_v1_params) do { @@ -125,9 +136,9 @@ } end - include_examples "succesfuly send notification" + include_examples 'succesfuly send notification' - context "when topic is invalid" do + context 'when topic is invalid' do let(:topic) { '/topics/news$' } it 'should raise error' do @@ -136,7 +147,7 @@ end end - describe "send to condition" do + describe 'send to condition' do let(:condition) { "'foo' in topics" } let(:send_v1_params) do { @@ -148,10 +159,10 @@ } end - include_examples "succesfuly send notification" + include_examples 'succesfuly send notification' end - describe "send to notification_key" do + describe 'send to notification_key' do let(:notification_key) { 'notification_key' } let(:send_v1_params) do { @@ -163,11 +174,81 @@ } end - include_examples "succesfuly send notification" + include_examples 'succesfuly send notification' + end + + context 'when project_name is empty' do + let(:project_name) { '' } + let(:send_v1_params) do + { + 'token' => '4sdsx', + 'notification' => { + 'title' => 'Breaking News', + 'body' => 'New news story available.' + } + } + end + + it 'should not send notification' do + client.send_v1(send_v1_params) + stub_fcm_send_v1_request.should_not have_been_requested + end + end + + describe 'error handling' do + let(:send_v1_params) do + { + 'token' => '4sdsx', + 'notification' => { + 'title' => 'Breaking News', + 'body' => 'New news story available.' + } + } + end + + context 'when status_code is 400' do + let(:status_code) { 400 } + + it 'should raise error' do + response = client.send_v1(send_v1_params) + expect(response[:status_code]).to eq(status_code) + expect(response[:response]).to include('Only applies for JSON requests') + end + end + + context 'when status_code is 401' do + let(:status_code) { 401 } + + it 'should raise error' do + response = client.send_v1(send_v1_params) + expect(response[:status_code]).to eq(status_code) + expect(response[:response]).to include('There was an error authenticating') + end + end + + context 'when status_code is 500' do + let(:status_code) { 500 } + + it 'should raise error' do + response = client.send_v1(send_v1_params) + expect(response[:status_code]).to eq(status_code) + expect(response[:response]).to include('There was an internal error') + end + end + + context 'when status_code is 503' do + let(:status_code) { 503 } + + it 'should raise error' do + response = client.send_v1(send_v1_params) + expect(response[:status_code]).to eq(status_code) + expect(response[:response]).to include('Server is temporarily unavailable') + end + end end end - describe "#send_to_topic" do + describe '#send_to_topic' do let(:client) { FCM.new(json_key_path, project_name) } let(:uri) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } @@ -208,7 +289,7 @@ stub_fcm_send_to_topic_request.should have_been_made.times(1) end - context "when topic is invalid" do + context 'when topic is invalid' do let(:topic) { '/topics/news$' } it 'should raise error' do @@ -258,6 +339,15 @@ ) stub_fcm_send_to_topic_condition_request.should have_been_made.times(1) end + + context 'when topic_condition is invalid' do + let(:topic_condition) { "'foo' in topics$" } + + it 'should raise error' do + client.send_to_topic_condition(topic_condition, options) + stub_fcm_send_to_topic_condition_request.should_not have_been_requested + end + end end describe "#get_instance_id_info" do