From 6307a6b78a53e4a67c5dd85890df154d4436da9a Mon Sep 17 00:00:00 2001 From: Erim Icel Date: Fri, 1 May 2026 16:00:17 +0100 Subject: [PATCH] chore: replace Hound with RuboCop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hound is currently configured at the GitHub-App level for this repo and flags violations on PRs without a config file in the codebase. This replaces it with RuboCop, owned in-repo, so contributors can: - Run the same checks locally before pushing (`bundle exec rubocop`) - See the rules and tune them via `.rubocop.yml` - Have the linter pinned to a known version (rubocop ~> 1.86 + rubocop-rspec ~> 3.9, matching what we use elsewhere) Changes: - Add `.rubocop.yml` configured for this small gem (TargetRubyVersion 2.7, Metrics cops disabled, RSpec cops loosened where the existing spec style intentionally uses long describes, `let`-heavy contexts, and `.should` syntax) - Add `rubocop` and `rubocop-rspec` to the dev Gemfile - Add a `lint` job to the CI workflow that runs `rubocop --parallel` - Add `.hound.yml` with `rubocop: enabled: false` so Hound stops duplicating the new in-repo lint job. Once this lands, the Hound app itself can be removed from the repository's GitHub Apps. - Auto-correct existing offences (frozen string literals, double quotes, guard clauses, symbol-shortcut hashes, deprecated `s.test_files`). Specs (29) still pass — none of the auto-corrections change behaviour. Notes / things deliberately not touched here: - `s.required_ruby_version = ">= 2.4.0"` left as-is. Bumping it is a semver-relevant change for gem consumers and belongs in a separate PR. - `s.metadata["rubygems_mfa_required"]` not added. Enabling MFA is a publishing-policy decision for the maintainer, not a lint fix. - `is_not_registered?` / `has_canonical_id?` predicate names left as-is (renaming would change the (admittedly internal) Ruby surface area). Prompt: "can we propse to upstream drop hound and add rubocop and add to CI with open PR ?" Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 15 ++ .hound.yml | 2 + .rubocop.yml | 67 ++++++++ Gemfile | 14 +- Rakefile | 16 +- fcm.gemspec | 14 +- lib/fcm.rb | 95 ++++++------ spec/fcm_spec.rb | 324 ++++++++++++++++++++------------------- spec/spec_helper.rb | 12 +- 9 files changed, 328 insertions(+), 231 deletions(-) create mode 100644 .hound.yml create mode 100644 .rubocop.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b60a827..319c767 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,3 +28,18 @@ jobs: - name: Run tests run: | bundle exec rspec + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + bundler: default + bundler-cache: true + + - name: Run RuboCop + run: bundle exec rubocop --parallel diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 0000000..45e8e7f --- /dev/null +++ b/.hound.yml @@ -0,0 +1,2 @@ +rubocop: + enabled: false diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..5e9b73e --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,67 @@ +plugins: + - rubocop-rspec + +AllCops: + TargetRubyVersion: 2.7 + NewCops: enable + SuggestExtensions: false + Exclude: + - "vendor/**/*" + - "tmp/**/*" + +# This is a small gem; some style cops add noise without value. +Style/Documentation: + Enabled: false +Style/StringLiterals: + EnforcedStyle: double_quotes +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +# Renaming `is_not_registered?` / `has_canonical_id?` would change the gem's +# (admittedly internal) Ruby surface area. Out of scope for a lint pass. +Naming/PredicatePrefix: + Enabled: false + +Layout/LineLength: + Max: 120 + Exclude: + - "fcm.gemspec" + +# Out of scope for a lint pass: bumping required_ruby_version is a semver-relevant +# change for gem consumers and should be considered separately. +Gemspec/RequiredRubyVersion: + Enabled: false + +# Enabling MFA is a publishing-policy decision for the gem maintainer, not +# something a lint pass should silently introduce. +Gemspec/RequireMFA: + Enabled: false + +# Code metrics are coarse signals that fight a lot of legacy code. +Metrics/AbcSize: + Enabled: false +Metrics/ClassLength: + Enabled: false +Metrics/CyclomaticComplexity: + Enabled: false +Metrics/MethodLength: + Enabled: false +Metrics/PerceivedComplexity: + Enabled: false +Metrics/BlockLength: + Exclude: + - "spec/**/*" + - "fcm.gemspec" + +# Specs are intentionally long, descriptive, and use `.should` syntax in +# places — none of these add value here. +RSpec/ExampleLength: + Enabled: false +RSpec/MultipleExpectations: + Enabled: false +RSpec/MultipleMemoizedHelpers: + Enabled: false +RSpec/NestedGroups: + Enabled: false +RSpec/NoExpectationExample: + Enabled: false diff --git a/Gemfile b/Gemfile index f0e87c4..bd6b1c2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,12 @@ +# frozen_string_literal: true + source "https://rubygems.org" gemspec -gem 'rake' -gem 'rspec' -gem 'webmock' -gem 'ci_reporter_rspec' -gem 'googleauth' +gem "ci_reporter_rspec" +gem "googleauth" +gem "rake" +gem "rspec" +gem "rubocop", require: false +gem "rubocop-rspec", require: false +gem "webmock" diff --git a/Rakefile b/Rakefile index ebcf703..008e057 100644 --- a/Rakefile +++ b/Rakefile @@ -1,15 +1,17 @@ -require 'rspec/core/rake_task' +# frozen_string_literal: true + +require "rspec/core/rake_task" require "bundler/gem_tasks" require "rake/tasklib" -require 'ci/reporter/rake/rspec' +require "ci/reporter/rake/rspec" -RSpec::Core::RakeTask.new(:spec => ["ci:setup:rspec"]) do |t| - t.pattern = 'spec/**/*_spec.rb' +RSpec::Core::RakeTask.new(spec: ["ci:setup:rspec"]) do |t| + t.pattern = "spec/**/*_spec.rb" end RSpec::Core::RakeTask.new(:spec) do |spec| - spec.pattern = 'spec/**/*_spec.rb' - spec.rspec_opts = ['--format documentation'] + spec.pattern = "spec/**/*_spec.rb" + spec.rspec_opts = ["--format documentation"] end -task :default => :spec +task default: :spec diff --git a/fcm.gemspec b/fcm.gemspec index b9b5d84..cd6ac58 100644 --- a/fcm.gemspec +++ b/fcm.gemspec @@ -1,5 +1,6 @@ -# -*- encoding: utf-8 -*- -$:.push File.expand_path("../lib", __FILE__) +# frozen_string_literal: true + +$LOAD_PATH.push File.expand_path("lib", __dir__) Gem::Specification.new do |s| s.name = "fcm" @@ -8,17 +9,16 @@ Gem::Specification.new do |s| s.authors = ["Kashif Rasul", "Shoaib Burq"] s.email = ["kashif@decision-labs.com", "shoaib@decision-labs.com"] s.homepage = "https://github.com/decision-labs/fcm" - s.summary = %q{Reliably deliver messages and notifications via FCM} - s.description = %q{fcm provides ruby bindings to Firebase Cloud Messaging (FCM) a cross-platform messaging solution that lets you reliably deliver messages and notifications at no cost to Android, iOS or Web browsers.} + s.summary = "Reliably deliver messages and notifications via FCM" + s.description = "fcm provides ruby bindings to Firebase Cloud Messaging (FCM) a cross-platform messaging solution that lets you reliably deliver messages and notifications at no cost to Android, iOS or Web browsers." s.license = "MIT" s.required_ruby_version = ">= 2.4.0" s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } s.require_paths = ["lib"] - s.add_runtime_dependency("faraday", ">= 1.0.0", "< 3.0") - s.add_runtime_dependency("googleauth", "~> 1") + s.add_dependency("faraday", ">= 1.0.0", "< 3.0") + s.add_dependency("googleauth", "~> 1") end diff --git a/lib/fcm.rb b/lib/fcm.rb index ab39292..9ac01bb 100644 --- a/lib/fcm.rb +++ b/lib/fcm.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "faraday" require "cgi" require "json" @@ -12,7 +14,7 @@ class InvalidCredentialError < StandardError; end GROUP_NOTIFICATION_BASE_URI = "https://android.googleapis.com" INSTANCE_ID_API = "https://iid.googleapis.com" - TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/ + TOPIC_REGEX = /[a-zA-Z0-9\-_.~%]+/.freeze def initialize(json_key_path = "", project_name = "", http_options = {}) @json_key_path = json_key_path @@ -51,7 +53,7 @@ def initialize(json_key_path = "", project_name = "", http_options = {}) def send_notification_v1(message) return if @project_name.empty? - post_body = { 'message': message } + post_body = { message: message } for_uri(BASE_URI_V1) do |connection| response = connection.post( "#{@project_name}/messages:send", post_body.to_json @@ -67,7 +69,7 @@ def create_notification_key(key_name, project_id, registration_ids = []) notification_key_name: key_name) extra_headers = { - "project_id" => project_id, + "project_id" => project_id } for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection| @@ -84,7 +86,7 @@ def add_registration_ids(key_name, project_id, notification_key, registration_id notification_key: notification_key) extra_headers = { - "project_id" => project_id, + "project_id" => project_id } for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection| @@ -101,7 +103,7 @@ def remove_registration_ids(key_name, project_id, notification_key, registration notification_key: notification_key) extra_headers = { - "project_id" => project_id, + "project_id" => project_id } for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection| @@ -116,7 +118,7 @@ def recover_notification_key(key_name, project_id) params = { notification_key_name: key_name } extra_headers = { - "project_id" => project_id, + "project_id" => project_id } for_uri(GROUP_NOTIFICATION_BASE_URI, extra_headers) do |connection| @@ -139,11 +141,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) @@ -165,28 +167,28 @@ def get_instance_id_info(iid_token, options = {}) end def send_to_topic(topic, options = {}) - if topic.gsub(TOPIC_REGEX, '').length.zero? - 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 + return unless topic.gsub(TOPIC_REGEX, "").empty? + + 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 def send_to_topic_condition(condition, options = {}) - if validate_condition?(condition) - 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 + return unless validate_condition?(condition) + + 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 @@ -200,7 +202,7 @@ def for_uri(uri, extra_headers = {}) faraday.adapter Faraday.default_adapter faraday.headers["Content-Type"] = "application/json" faraday.headers["Authorization"] = "Bearer #{jwt_token}" - faraday.headers["access_token_auth"]= "true" + faraday.headers["access_token_auth"] = "true" extra_headers.each do |key, value| faraday.headers[key] = value end @@ -221,9 +223,14 @@ def build_response(response, registration_ids = []) response_hash[:response] = "success" body = JSON.parse(body) unless body.empty? response_hash[:canonical_ids] = build_canonical_ids(body, registration_ids) unless registration_ids.empty? - response_hash[:not_registered_ids] = build_not_registered_ids(body, registration_ids) unless registration_ids.empty? + unless registration_ids.empty? + response_hash[:not_registered_ids] = + build_not_registered_ids(body, registration_ids) + end when 400 - response_hash[:response] = "Only applies for JSON requests. Indicates that the request could not be parsed as JSON, or it contained invalid fields." + response_hash[:response] = + "Only applies for JSON requests. Indicates that the request could not be parsed as JSON, " \ + "or it contained invalid fields." when 401 response_hash[:response] = "There was an error authenticating the sender account." when 503 @@ -236,11 +243,9 @@ def build_response(response, registration_ids = []) def build_canonical_ids(body, registration_ids) canonical_ids = [] - unless body.empty? - if body["canonical_ids"] > 0 - body["results"].each_with_index do |result, index| - canonical_ids << { old: registration_ids[index], new: result["registration_id"] } if has_canonical_id?(result) - end + if !body.empty? && body["canonical_ids"].positive? + body["results"].each_with_index do |result, index| + canonical_ids << { old: registration_ids[index], new: result["registration_id"] } if has_canonical_id?(result) end end canonical_ids @@ -248,11 +253,9 @@ def build_canonical_ids(body, registration_ids) def build_not_registered_ids(body, registration_id) not_registered_ids = [] - unless body.empty? - if body["failure"] > 0 - body["results"].each_with_index do |result, index| - not_registered_ids << registration_id[index] if is_not_registered?(result) - end + if !body.empty? && body["failure"].positive? + body["results"].each_with_index do |result, index| + not_registered_ids << registration_id[index] if is_not_registered?(result) end end not_registered_ids @@ -272,32 +275,32 @@ def validate_condition?(condition) def validate_condition_format?(condition) bad_characters = condition.gsub( - /(topics|in|\s|\(|\)|(&&)|[!]|(\|\|)|'([a-zA-Z0-9\-_.~%]+)')/, + /(topics|in|\s|\(|\)|(&&)|!|(\|\|)|'([a-zA-Z0-9\-_.~%]+)')/, "" ) - bad_characters.length == 0 + bad_characters.empty? end def validate_condition_topics?(condition) topics = condition.scan(/(?:^|\S|\s)'([^']*?)'(?:$|\S|\s)/).flatten - topics.all? { |topic| topic.gsub(TOPIC_REGEX, "").length == 0 } + topics.all? { |topic| topic.gsub(TOPIC_REGEX, "").empty? } end def jwt_token scope = "https://www.googleapis.com/auth/firebase.messaging" @authorizer ||= Google::Auth::ServiceAccountCredentials.make_creds( json_key_io: json_key, - scope: scope, + scope: scope ) token = @authorizer.fetch_access_token! token["access_token"] end def raise_credentials_error(param) - error_msg = 'credentials must be an IO-like ' \ - 'object or path. You passed' + error_msg = "credentials must be an IO-like " \ + "object or path. You passed" - param_klass = param.nil? ? 'nil' : "a #{param.class.name}" + param_klass = param.nil? ? "nil" : "a #{param.class.name}" error_msg += " #{param_klass}." raise InvalidCredentialError, error_msg end diff --git a/spec/fcm_spec.rb b/spec/fcm_spec.rb index 4332ba1..3374368 100644 --- a/spec/fcm_spec.rb +++ b/spec/fcm_spec.rb @@ -1,32 +1,34 @@ +# frozen_string_literal: true + require "spec_helper" -require 'tempfile' +require "tempfile" describe FCM do - let(:project_name) { 'test-project' } - let(:json_key_path) { 'path/to/json/key.json' } - let(:client) { FCM.new(json_key_path) } + let(:project_name) { "test-project" } + let(:json_key_path) { "path/to/json/key.json" } + let(:client) { described_class.new(json_key_path) } let(:mock_token) { "access_token" } let(:mock_headers) do { "Content-Type" => "application/json", - "Authorization" => "Bearer #{mock_token}", + "Authorization" => "Bearer #{mock_token}" } end let(:client_email) do - '83315528762cf7e0-7bbcc3aad87e0083391bc7f234d487' \ - 'c8@developer.gserviceaccount.com' + "83315528762cf7e0-7bbcc3aad87e0083391bc7f234d487" \ + "c8@developer.gserviceaccount.com" end let(:client_x509_cert_url) do - 'https://www.googleapis.com/robot/v1/metadata/x509/' \ - 'fd6b61037dd2bb8585527679" + "-7bbcc3aad87e0083391b' \ - 'c7f234d487c8%40developer.gserviceaccount.com' + "https://www.googleapis.com/robot/v1/metadata/x509/" \ + 'fd6b61037dd2bb8585527679" + "-7bbcc3aad87e0083391b' \ + "c7f234d487c8%40developer.gserviceaccount.com" end let(:large_file_name) do - Array.new(1021) { 'a' }.join('') + '.txt' + "#{Array.new(1021) { "a" }.join}.txt" end let(:creds_error) do @@ -35,17 +37,17 @@ let(:json_credentials) do { - "type": 'service_account', - "project_id": 'example', - "private_key_id": 'c09c4593eee53707ca9f4208fbd6fe72b29fc7ab', - "private_key": OpenSSL::PKey::RSA.new(2048).to_s, - "client_email": client_email, - "client_id": 'acedc3c0a63b3562376386f0.apps.googleusercontent.com', - "auth_uri": 'https://accounts.google.com/o/oauth2/auth', - "token_uri": 'https://oauth2.googleapis.com/token', - "auth_provider_x509_cert_url": 'https://www.googleapis.com/oauth2/v1/certs', - "client_x509_cert_url": client_x509_cert_url, - "universe_domain": 'googleapis.com' + type: "service_account", + project_id: "example", + private_key_id: "c09c4593eee53707ca9f4208fbd6fe72b29fc7ab", + private_key: OpenSSL::PKey::RSA.new(2048).to_s, + client_email: client_email, + client_id: "acedc3c0a63b3562376386f0.apps.googleusercontent.com", + auth_uri: "https://accounts.google.com/o/oauth2/auth", + token_uri: "https://oauth2.googleapis.com/token", + auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs", + client_x509_cert_url: client_x509_cert_url, + universe_domain: "googleapis.com" }.to_json end @@ -54,32 +56,32 @@ # Mock the Google::Auth::ServiceAccountCredentials allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) - .and_return(double(fetch_access_token!: { 'access_token' => mock_token })) + .and_return(double(fetch_access_token!: { "access_token" => mock_token })) end - it 'should initialize' do + it "initializes" do expect { client }.not_to raise_error end describe "credentials path" do - it 'can be a path to a file' do - fcm = FCM.new("README.md") + it "can be a path to a file" do + fcm = described_class.new("README.md") expect(fcm.__send__(:json_key).class).to eq(File) end - it 'raises an error when passed a large path' do + it "raises an error when passed a large path" do expect do - FCM.new(large_file_name).__send__(:json_key) + described_class.new(large_file_name).__send__(:json_key) end.to raise_error(creds_error) end - it 'can be an IO object' do - fcm = FCM.new(StringIO.new('hey')) + it "can be an IO object" do + fcm = described_class.new(StringIO.new("hey")) expect(fcm.__send__(:json_key).class).to eq(StringIO) - temp_file = Tempfile.new('hello_world.json') + temp_file = Tempfile.new("hello_world.json") temp_file.write(json_credentials) - fcm_with_temp_file = FCM.new(temp_file) + fcm_with_temp_file = described_class.new(temp_file) expect do fcm_with_temp_file @@ -88,49 +90,49 @@ temp_file.unlink end - it 'raises an error when passed a non IO-like object' do + it "raises an error when passed a non IO-like object" do expect do - FCM.new(nil, '', {}).__send__(:json_key) - end.to raise_error(creds_error, 'credentials must be' \ - ' an IO-like object or path. You passed nil.') + described_class.new(nil, "", {}).__send__(:json_key) + end.to raise_error(creds_error, "credentials must be " \ + "an IO-like object or path. You passed nil.") expect do - FCM.new(json_credentials, '', {}).__send__(:json_key) - end.to raise_error(creds_error, 'credentials must be' \ - ' an IO-like object or path. You passed a String.') + described_class.new(json_credentials, "", {}).__send__(:json_key) + end.to raise_error(creds_error, "credentials must be " \ + "an IO-like object or path. You passed a String.") expect do - FCM.new({}, '', {}).__send__(:json_key) - end.to raise_error(creds_error, 'credentials must be' \ - ' an IO-like object or path. You passed a Hash.') + described_class.new({}, "", {}).__send__(:json_key) + end.to raise_error(creds_error, "credentials must be " \ + "an IO-like object or path. You passed a Hash.") end - it 'raises an error when passed a non-existent credentials file path' do - fcm = FCM.new('spec/fake_credentials.json', '', {}) + it "raises an error when passed a non-existent credentials file path" do + fcm = described_class.new("spec/fake_credentials.json", "", {}) expect { fcm.__send__(:json_key) }.to raise_error(creds_error) end - it 'raises an error when passed a string of a file that does not exist' do - fcm = FCM.new('example.txt', '', {}) + it "raises an error when passed a string of a file that does not exist" do + fcm = described_class.new("example.txt", "", {}) expect { fcm.__send__(:json_key) }.to raise_error(creds_error) end end describe "#send_v1 or #send_notification_v1" do - let(:client) { FCM.new(json_key_path, project_name) } + let(:client) { described_class.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( - body: { 'message' => send_v1_params }.to_json, + 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: "{}", headers: {}, - status: status_code, + status: status_code ) end @@ -138,222 +140,222 @@ stub_fcm_send_v1_request end - shared_examples 'succesfuly send notification' do - it 'should send notification of HTTP V1 using POST to FCM server' do + shared_examples "succesfuly send notification" do + it "sends 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 + 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' } + describe "send to token" do + let(:token) { "4sdsx" } let(:send_v1_params) do { - 'token' => token, - 'notification' => { - 'title' => 'Breaking News', - 'body' => 'New news story available.' + "token" => token, + "notification" => { + "title" => "Breaking News", + "body" => "New news story available." }, - 'data' => { - 'story_id' => 'story_12345' + "data" => { + "story_id" => "story_12345" }, - 'android' => { - 'notification' => { - 'click_action' => 'TOP_STORY_ACTIVITY', - 'body' => 'Check out the Top Story' + "android" => { + "notification" => { + "click_action" => "TOP_STORY_ACTIVITY", + "body" => "Check out the Top Story" } }, - 'apns' => { - 'payload' => { - 'aps' => { - 'category' => 'NEW_MESSAGE_CATEGORY' + "apns" => { + "payload" => { + "aps" => { + "category" => "NEW_MESSAGE_CATEGORY" } } } } end - include_examples 'succesfuly send notification' + it_behaves_like "succesfuly send notification" - it 'includes all the response' do + 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[: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 - let(:tokens) { ['4sdsx', '4sdsy'] } + describe "send to multiple tokens" do + let(:tokens) { %w[4sdsx 4sdsy] } let(:send_v1_params) do { - 'token' => tokens, - 'notification' => { - 'title' => 'Breaking News', - 'body' => 'New news story available.' + "token" => tokens, + "notification" => { + "title" => "Breaking News", + "body" => "New news story available." } } end - include_examples 'succesfuly send notification' + it_behaves_like "succesfuly send notification" end - describe 'send to topic' do - let(:topic) { 'news' } + describe "send to topic" do + let(:topic) { "news" } let(:send_v1_params) do { - 'topic' => topic, - 'notification' => { - 'title' => 'Breaking News', - 'body' => 'New news story available.' + "topic" => topic, + "notification" => { + "title" => "Breaking News", + "body" => "New news story available." } } end - include_examples 'succesfuly send notification' + it_behaves_like "succesfuly send notification" - context 'when topic is invalid' do - let(:topic) { '/topics/news$' } + context "when topic is invalid" do + let(:topic) { "/topics/news$" } - it 'should raise error' do + it "raises error" do stub_fcm_send_v1_request.should_not have_been_requested end end end - describe 'send to condition' do + 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.' - }, + "condition" => condition, + "notification" => { + "title" => "Breaking News", + "body" => "New news story available." + } } end - include_examples 'succesfuly send notification' + it_behaves_like "succesfuly send notification" end - describe 'send to notification_key' do - let(:notification_key) { 'notification_key' } + 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.' + "notification_key" => notification_key, + "notification" => { + "title" => "Breaking News", + "body" => "New news story available." } } end - include_examples 'succesfuly send notification' + it_behaves_like "succesfuly send notification" end - context 'when project_name is empty' do - let(:project_name) { '' } + 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.' + "token" => "4sdsx", + "notification" => { + "title" => "Breaking News", + "body" => "New news story available." } } end - it 'should not send notification' do + it "does 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 + describe "error handling" do let(:send_v1_params) do { - 'token' => '4sdsx', - 'notification' => { - 'title' => 'Breaking News', - 'body' => 'New news story available.' + "token" => "4sdsx", + "notification" => { + "title" => "Breaking News", + "body" => "New news story available." } } end - context 'when status_code is 400' do + context "when status_code is 400" do let(:status_code) { 400 } - it 'should raise error' do + it "raises 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') + expect(response[:response]).to include("Only applies for JSON requests") end end - context 'when status_code is 401' do + context "when status_code is 401" do let(:status_code) { 401 } - it 'should raise error' do + it "raises 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') + expect(response[:response]).to include("There was an error authenticating") end end - context 'when status_code is 500' do + context "when status_code is 500" do let(:status_code) { 500 } - it 'should raise error' do + it "raises 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') + expect(response[:response]).to include("There was an internal error") end end - context 'when status_code is 503' do + context "when status_code is 503" do let(:status_code) { 503 } - it 'should raise error' do + it "raises 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') + expect(response[:response]).to include("Server is temporarily unavailable") end end end end - describe '#send_to_topic' do - let(:client) { FCM.new(json_key_path, project_name) } + describe "#send_to_topic" do + let(:client) { described_class.new(json_key_path, project_name) } let(:uri) { "#{FCM::BASE_URI_V1}#{project_name}/messages:send" } - let(:topic) { 'news' } + let(:topic) { "news" } let(:params) do { - 'topic' => topic + "topic" => topic }.merge(options) end let(:options) do { - 'data' => { - 'story_id' => 'story_12345' + "data" => { + "story_id" => "story_12345" } } end let(:stub_fcm_send_to_topic_request) do stub_request(:post, uri).with( - body: { 'message' => params }.to_json, + body: { "message" => params }.to_json, headers: mock_headers ).to_return( body: "{}", headers: {}, - status: 200, + status: 200 ) end @@ -361,17 +363,17 @@ stub_fcm_send_to_topic_request end - it 'should send notification to topic using POST to FCM server' do + it "sends notification to topic using POST to FCM server" do client.send_to_topic(topic, options).should eq( - response: 'success', body: '{}', headers: {}, status_code: 200 + response: "success", body: "{}", headers: {}, status_code: 200 ) stub_fcm_send_to_topic_request.should have_been_made.times(1) end - context 'when topic is invalid' do - let(:topic) { '/topics/news$' } + context "when topic is invalid" do + let(:topic) { "/topics/news$" } - it 'should raise error' do + it "raises error" do client.send_to_topic(topic, options) stub_fcm_send_to_topic_request.should_not have_been_requested end @@ -379,32 +381,32 @@ end describe "#send_to_topic_condition" do - let(:client) { FCM.new(json_key_path, project_name) } + let(:client) { described_class.new(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 + "condition" => topic_condition }.merge(options) end let(:options) do { - 'data' => { - 'story_id' => 'story_12345' + "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, + body: { "message" => params }.to_json, headers: mock_headers ).to_return( body: "{}", headers: {}, - status: 200, + status: 200 ) end @@ -412,17 +414,17 @@ stub_fcm_send_to_topic_condition_request end - it 'should send notification to topic_condition using POST to FCM server' do + it "sends 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 + response: "success", body: "{}", headers: {}, status_code: 200 ) stub_fcm_send_to_topic_condition_request.should have_been_made.times(1) end - context 'when topic_condition is invalid' do + context "when topic_condition is invalid" do let(:topic_condition) { "'foo' in topics$" } - it 'should raise error' do + it "raises error" do client.send_to_topic_condition(topic_condition, options) stub_fcm_send_to_topic_condition_request.should_not have_been_requested end @@ -437,19 +439,19 @@ let(:uri) { "#{base_uri}/#{registration_token}" } let(:registration_token) { "42" } - context 'without options' do - it 'calls info endpoint' do + context "without options" do + it "calls info endpoint" do endpoint = stub_request(:get, uri).with(headers: mock_headers) get_info expect(endpoint).to have_been_requested end end - context 'with detail option' do + context "with detail option" do let(:uri) { "#{base_uri}/#{registration_token}?details=true" } let(:options) { { details: true } } - it 'calls info endpoint' do + it "calls info endpoint" do endpoint = stub_request(:get, uri).with(headers: mock_headers) get_info expect(endpoint).to have_been_requested @@ -458,17 +460,17 @@ end describe "topic subscriptions" do - let(:topic) { 'news' } + let(:topic) { "news" } let(:registration_token) { "42" } - let(:registration_token_2) { "43" } - let(:registration_tokens) { [registration_token, registration_token_2] } + let(:registration_token2) { "43" } + let(:registration_tokens) { [registration_token, registration_token2] } describe "#topic_subscription" do subject(:subscribe) { client.topic_subscription(topic, registration_token) } let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1/#{registration_token}/rel/topics/#{topic}" } - it 'subscribes to a topic' do + it "subscribes to a topic" do endpoint = stub_request(:post, uri).with(headers: mock_headers) subscribe expect(endpoint).to have_been_requested @@ -481,7 +483,7 @@ let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchRemove" } let(:params) { { to: "/topics/#{topic}", registration_tokens: [registration_token] } } - it 'unsubscribes from a topic' do + 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 @@ -494,7 +496,7 @@ let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchAdd" } let(:params) { { to: "/topics/#{topic}", registration_tokens: registration_tokens } } - it 'subscribes to a topic' do + 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 @@ -507,7 +509,7 @@ let(:uri) { "#{FCM::INSTANCE_ID_API}/iid/v1:batchRemove" } let(:params) { { to: "/topics/#{topic}", registration_tokens: registration_tokens } } - it 'unsubscribes from a topic' do + 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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0eec70c..3b7d1cf 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,13 +1,15 @@ -require 'rubygems' -require 'bundler/setup' -require 'webmock/rspec' +# frozen_string_literal: true -require 'fcm' +require "rubygems" +require "bundler/setup" +require "webmock/rspec" + +require "fcm" RSpec.configure do |config| config.run_all_when_everything_filtered = true config.expect_with :rspec do |c| - c.syntax = [:should, :expect] + c.syntax = %i[should expect] end # config.filter_run :focus end