├── .coveralls.yml ├── .rspec ├── lib ├── pusher_push_notifications.rb └── pusher │ ├── push_notifications │ ├── version.rb │ ├── user_id.rb │ ├── token.rb │ ├── use_cases │ │ ├── delete_user.rb │ │ ├── generate_token.rb │ │ ├── publish_to_users.rb │ │ └── publish.rb │ └── client.rb │ └── push_notifications.rb ├── bin ├── setup └── console ├── Rakefile ├── .env.sample ├── .gitignore ├── Gemfile ├── pull_request_template.md ├── .github ├── workflows │ ├── publish.yml │ ├── test.yml │ ├── gh-release.yml │ └── release.yml └── stale.yml ├── .rubocop.yml ├── CHANGELOG.md ├── spec ├── spec_helper.rb ├── cassettes │ ├── delete │ │ ├── user.yml │ │ └── user │ │ │ ├── id_empty.yml │ │ │ └── id_too_long.yml │ └── publishes │ │ ├── users │ │ ├── invalid_payload.yml │ │ ├── valid_payload.yml │ │ └── valid_users.yml │ │ ├── interests │ │ ├── invalid_payload.yml │ │ ├── valid_payload.yml │ │ └── valid_interests.yml │ │ ├── invalid_instance_id.yml │ │ └── invalid_secret_key.yml └── pusher │ └── push_notifications │ ├── delete_user_spec.rb │ ├── generate_token_spec.rb │ ├── publish_to_users_spec.rb │ ├── configuration_spec.rb │ ├── publish_to_interests_spec.rb │ └── client_spec.rb ├── LICENSE.txt ├── pusher-push-notifications.gemspec ├── Gemfile.lock └── README.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: secret 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/pusher_push_notifications.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './pusher/push_notifications' 4 | 5 | module Pusher 6 | end 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/pusher/push_notifications/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Pusher 4 | module PushNotifications 5 | VERSION = '2.0.2' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # Replace with your own Pusher Instance ID and Secret Key 2 | 3 | export PUSHER_INSTANCE_ID=97c56dfe-58f5-408b-ab3a-158e51a860f2 4 | export PUSHER_SECRET_KEY=3B397552E080252048FE03009C1253A 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | .DS_Store 14 | .idea 15 | .vscode 16 | .env 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Specify your gem's dependencies in pusher-push_notifications.gemspec 8 | gemspec 9 | -------------------------------------------------------------------------------- /lib/pusher/push_notifications/user_id.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Pusher 4 | module PushNotifications 5 | class UserId 6 | MAX_USER_ID_LENGTH = 164 7 | MAX_NUM_USER_IDS = 1000 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Add a short description of the change. If this is related to an issue, please add a reference to the issue. 4 | 5 | ## CHANGELOG 6 | 7 | * [CHANGED] Describe your change here. Look at CHANGELOG.md to see the format. 8 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: RubyGems release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v1 13 | 14 | - name: Publish gem 15 | uses: dawidd6/action-publish-gem@v1 16 | with: 17 | api_key: ${{secrets.RUBYGEMS_API_KEY}} 18 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Documentation: 2 | Enabled: false 3 | 4 | AllCops: 5 | TargetRubyVersion: 2.4.0 6 | 7 | Metrics/LineLength: 8 | Enabled: 9 | Max: 80 10 | 11 | Style/GuardClause: 12 | Enabled: false 13 | 14 | Metrics/MethodLength: 15 | Enabled: false 16 | 17 | Metrics/AbcSize: 18 | Enabled: false 19 | 20 | Metrics/ModuleLength: 21 | Exclude: 22 | - "spec/**/*.rb" 23 | 24 | Metrics/BlockLength: 25 | Exclude: 26 | - "spec/**/*.rb" 27 | - "pusher-push-notifications.gemspec" 28 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | 6 | require 'dotenv' 7 | Dotenv.load 8 | 9 | require 'pusher/push_notifications' 10 | 11 | Pusher::PushNotifications.configure do |config| 12 | config.instance_id = ENV['PUSHER_INSTANCE_ID'] 13 | config.secret_key = ENV['PUSHER_SECRET_KEY'] 14 | end 15 | 16 | # You can add fixtures and/or initialization code here to make experimenting 17 | # with your gem easier. You can also use a different console, if you like. 18 | 19 | # (If you use this, don't forget to add pry to your Gemfile!) 20 | require 'pry' 21 | Pry.start 22 | -------------------------------------------------------------------------------- /lib/pusher/push_notifications/token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'jwt' 4 | 5 | module Pusher 6 | module PushNotifications 7 | class Token 8 | extend Forwardable 9 | def initialize(config: PushNotifications) 10 | @config = config 11 | end 12 | 13 | def generate(user) 14 | exp = Time.now.to_i + 24 * 60 * 60 # Current time + 24h 15 | iss = "https://#{instance_id}.pushnotifications.pusher.com" 16 | payload = { 'sub' => user, 'exp' => exp, 'iss' => iss } 17 | JWT.encode payload, secret_key, 'HS256' 18 | end 19 | 20 | private 21 | 22 | attr_reader :config 23 | 24 | def_delegators :@config, :instance_id, :secret_key 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/pusher/push_notifications/use_cases/delete_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Pusher 4 | module PushNotifications 5 | module UseCases 6 | class DeleteUser 7 | class UserDeletionError < RuntimeError; end 8 | 9 | # Contacts the Beams service 10 | # to remove all the devices of the given user. 11 | def self.delete_user(client, user:) 12 | raise UserDeletionError, 'User Id cannot be empty.' if user.empty? 13 | 14 | if user.length > UserId::MAX_USER_ID_LENGTH 15 | raise UserDeletionError, 'User id length too long ' \ 16 | "(expected fewer than #{UserId::MAX_USER_ID_LENGTH + 1} characters)" 17 | end 18 | 19 | client.delete(user) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | ruby-version: ['2.6', '2.7', '3.0'] 16 | 17 | env: 18 | PUSHER_INSTANCE_ID: 1b880590-6301-4bb5-b34f-45db1c5f5644 19 | PUSHER_SECRET_KEY: abc 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby-version }} 27 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 28 | - name: Run tests 29 | run: bundle exec rake 30 | - name: Run rubocop 31 | run: bundle exec rubocop 32 | -------------------------------------------------------------------------------- /lib/pusher/push_notifications/use_cases/generate_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Pusher 4 | module PushNotifications 5 | module UseCases 6 | class GenerateToken 7 | class GenerateTokenError < RuntimeError; end 8 | 9 | # Creates a signed JWT for a user id. 10 | def self.generate_token(user:) 11 | raise GenerateTokenError, 'User Id cannot be empty.' if user.empty? 12 | 13 | if user.length > UserId::MAX_USER_ID_LENGTH 14 | raise GenerateTokenError, 'User id length too long ' \ 15 | "(expected fewer than #{UserId::MAX_USER_ID_LENGTH + 1} characters)" 16 | end 17 | 18 | jwt_token = PushNotifications::Token.new 19 | 20 | { 'token' => jwt_token.generate(user) } 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.2 4 | 5 | * [FIXED] Multiple Beams instances no longer use last configuration. 6 | 7 | ## 2.0.1 8 | 9 | * [CHANGED] Update Gemfile lock on release. 10 | 11 | ## 2.0.0 12 | 13 | * [ADDED] Support for Ruby 3.0. 14 | * [REMOVED] Support for Ruby 2.5 and below. 15 | * [CHANGED] Refactor tests to avoid caze dependency. 16 | 17 | ## 1.3.0 18 | 19 | * [ADDED] Support for multiple instances of Beams clients to exist for more advanced use cases. 20 | 21 | ## 1.2.1 22 | 23 | * [FIXED] Endpoint also allows the scheme to be configured to enable for local development. 24 | 25 | ## 1.2.0 26 | 27 | * [ADDED] Support for "endpoint" overrides to allow for internal integration testing. 28 | 29 | ## 1.1.0 30 | 31 | * [ADDED] Support for "Authenticated Users" feature: publishToUsers, generateToken and deleteUser. 32 | 33 | ## 1.0.0 34 | 35 | * [ADDED] General availability (GA) release. 36 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dotenv' 4 | Dotenv.load 5 | 6 | require 'bundler/setup' 7 | require 'pry-byebug' 8 | require 'pusher/push_notifications' 9 | require 'vcr' 10 | require 'webmock' 11 | 12 | if ENV['COVERAGE'] 13 | require 'coveralls' 14 | Coveralls.wear! 15 | end 16 | 17 | require 'simplecov' 18 | SimpleCov.start 19 | 20 | if ENV['CI'] == 'true' 21 | require 'codecov' 22 | SimpleCov.formatter = SimpleCov::Formatter::Codecov 23 | end 24 | 25 | VCR.configure do |config| 26 | config.cassette_library_dir = 'spec/cassettes' 27 | config.hook_into :webmock 28 | end 29 | 30 | RSpec.configure do |config| 31 | config.example_status_persistence_file_path = '.rspec_status' 32 | 33 | config.disable_monkey_patching! 34 | 35 | config.before(:suite) do 36 | Pusher::PushNotifications.configure do |c| 37 | c.instance_id = ENV['PUSHER_INSTANCE_ID'] 38 | c.secret_key = ENV['PUSHER_SECRET_KEY'] 39 | end 40 | end 41 | 42 | config.expect_with :rspec do |c| 43 | c.syntax = :expect 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /.github/workflows/gh-release.yml: -------------------------------------------------------------------------------- 1 | name: Github Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | create-release: 9 | name: Create Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Setup git 15 | run: | 16 | git config user.email "pusher-ci@pusher.com" 17 | git config user.name "Pusher CI" 18 | - name: Prepare description 19 | run: | 20 | csplit -s CHANGELOG.md "/##/" {1} 21 | cat xx01 > CHANGELOG.tmp 22 | - name: Prepare tag 23 | run: | 24 | export TAG=$(head -1 CHANGELOG.tmp | cut -d' ' -f2) 25 | echo "TAG=$TAG" >> $GITHUB_ENV 26 | - name: Create Release 27 | uses: actions/create-release@v1 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | with: 31 | tag_name: ${{ env.TAG }} 32 | release_name: ${{ env.TAG }} 33 | body_path: CHANGELOG.tmp 34 | draft: false 35 | prerelease: false 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Lucas Medeiros 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 90 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 7 9 | 10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 11 | onlyLabels: [] 12 | 13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 14 | exemptLabels: 15 | - pinned 16 | - security 17 | 18 | # Set to true to ignore issues with an assignee (defaults to false) 19 | exemptAssignees: true 20 | 21 | # Comment to post when marking as stale. Set to `false` to disable 22 | markComment: > 23 | This issue has been automatically marked as stale because it has not had 24 | recent activity. It will be closed if no further activity occurs. If you'd 25 | like this issue to stay open please leave a comment indicating how this issue 26 | is affecting you. Thank you. 27 | -------------------------------------------------------------------------------- /lib/pusher/push_notifications/use_cases/publish_to_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Pusher 4 | module PushNotifications 5 | module UseCases 6 | class PublishToUsers 7 | class UsersPublishError < RuntimeError; end 8 | 9 | # Publish the given payload to the specified users. 10 | def self.publish_to_users(client, users:, payload: {}) 11 | users.each do |user| 12 | raise UsersPublishError, 'User Id cannot be empty.' if user.empty? 13 | 14 | next unless user.length > UserId::MAX_USER_ID_LENGTH 15 | 16 | raise UsersPublishError, 'User id length too long ' \ 17 | "(expected fewer than #{UserId::MAX_USER_ID_LENGTH + 1}" \ 18 | ' characters)' 19 | end 20 | 21 | raise UsersPublishError, 'Must supply at least one user id.' if users.count < 1 22 | 23 | if users.length > UserId::MAX_NUM_USER_IDS 24 | raise UsersPublishError, "Number of user ids #{users.length} "\ 25 | "exceeds maximum of #{UserId::MAX_NUM_USER_IDS}." 26 | end 27 | 28 | data = { users: users }.merge!(payload) 29 | client.post('publishes/users', data) 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/cassettes/delete/user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: https://1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com/customer_api/v1/instances/1b880590-6301-4bb5-b34f-45db1c5f5644/users/Stop!'+said+Fred+user 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept: 11 | - application/json 12 | Accept-Encoding: 13 | - gzip, deflate 14 | User-Agent: 15 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 16 | Content-Type: 17 | - application/json 18 | Authorization: 19 | - Bearer F8AC0B756E50DF235F642D6F0DC2CDE0328CD9184B3874C5E91AB2189BB722FE 20 | X-Pusher-Library: 21 | - pusher-push-notifications-ruby 1.0.0 22 | Host: 23 | - 1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com 24 | response: 25 | status: 26 | code: 200 27 | message: OK 28 | headers: 29 | Server: 30 | - Cowboy 31 | Connection: 32 | - keep-alive 33 | Date: 34 | - Thu, 21 Feb 2019 13:41:35 GMT 35 | Content-Length: 36 | - '0' 37 | Via: 38 | - 1.1 vegur 39 | body: 40 | encoding: UTF-8 41 | string: '' 42 | http_version: 43 | recorded_at: Thu, 21 Feb 2019 13:41:35 GMT 44 | recorded_with: VCR 3.0.3 45 | -------------------------------------------------------------------------------- /spec/pusher/push_notifications/delete_user_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pusher::PushNotifications, '.delete_user' do 6 | def delete_user 7 | Pusher::PushNotifications.delete_user(user: user) 8 | end 9 | 10 | let(:user) { "Stop!' said Fred user" } 11 | 12 | context 'when user id is empty' do 13 | let(:user) { '' } 14 | 15 | it 'user deletion request will fail' do 16 | expect { delete_user }.to raise_error( 17 | Pusher::PushNotifications::UseCases::DeleteUser::UserDeletionError 18 | ).with_message( 19 | 'User Id cannot be empty.' 20 | ) 21 | end 22 | end 23 | 24 | context 'when user id is too long' do 25 | max_user_id_length = Pusher::PushNotifications::UserId::MAX_USER_ID_LENGTH 26 | let(:user) { 'a' * (max_user_id_length + 1) } 27 | 28 | it 'user deletion request will fail' do 29 | expect { delete_user }.to raise_error( 30 | Pusher::PushNotifications::UseCases::DeleteUser::UserDeletionError 31 | ).with_message( 32 | 'User id length too long (expected fewer than 165 characters)' 33 | ) 34 | end 35 | end 36 | 37 | context 'when user id is valid' do 38 | it 'will delete the user' do 39 | VCR.use_cassette('delete/user') do 40 | response = delete_user 41 | 42 | expect(response).to be_ok 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/cassettes/delete/user/id_empty.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: https://1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com/customer_api/v1/instances/1b880590-6301-4bb5-b34f-45db1c5f5644/users/ 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept: 11 | - application/json 12 | Accept-Encoding: 13 | - gzip, deflate 14 | User-Agent: 15 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 16 | Content-Type: 17 | - application/json 18 | Authorization: 19 | - Bearer F8AC0B756E50DF235F642D6F0DC2CDE0328CD9184B3874C5E91AB2189BB722FE 20 | X-Pusher-Library: 21 | - pusher-push-notifications-ruby 1.0.0 22 | Host: 23 | - 1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com 24 | response: 25 | status: 26 | code: 404 27 | message: Not Found 28 | headers: 29 | Server: 30 | - Cowboy 31 | Connection: 32 | - keep-alive 33 | Content-Type: 34 | - text/plain; charset=utf-8 35 | X-Content-Type-Options: 36 | - nosniff 37 | Date: 38 | - Thu, 21 Feb 2019 13:41:34 GMT 39 | Content-Length: 40 | - '19' 41 | Via: 42 | - 1.1 vegur 43 | body: 44 | encoding: UTF-8 45 | string: '404 page not found 46 | 47 | ' 48 | http_version: 49 | recorded_at: Thu, 21 Feb 2019 13:41:34 GMT 50 | recorded_with: VCR 3.0.3 51 | -------------------------------------------------------------------------------- /spec/cassettes/publishes/users/invalid_payload.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com/publish_api/v1/instances/1b880590-6301-4bb5-b34f-45db1c5f5644/publishes/users 6 | body: 7 | encoding: UTF-8 8 | string: '{"invalid":"payload"}' 9 | headers: 10 | Accept: 11 | - application/json 12 | Accept-Encoding: 13 | - gzip, deflate 14 | User-Agent: 15 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 16 | Content-Type: 17 | - application/json 18 | Authorization: 19 | - Bearer F8AC0B756E50DF235F642D6F0DC2CDE0328CD9184B3874C5E91AB2189BB722FE 20 | X-Pusher-Library: 21 | - pusher-push-notifications-ruby 1.0.0 22 | Content-Length: 23 | - '21' 24 | Host: 25 | - 1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com 26 | response: 27 | status: 28 | code: 422 29 | message: Unprocessable Entity 30 | headers: 31 | Server: 32 | - Cowboy 33 | Connection: 34 | - keep-alive 35 | Content-Type: 36 | - application/json 37 | Date: 38 | - Thu, 21 Feb 2019 13:41:33 GMT 39 | Content-Length: 40 | - '67' 41 | Via: 42 | - 1.1 vegur 43 | body: 44 | encoding: UTF-8 45 | string: '{"error":"Bad Request","description":"`users`: users is required"} 46 | 47 | ' 48 | http_version: 49 | recorded_at: Thu, 21 Feb 2019 13:41:34 GMT 50 | recorded_with: VCR 3.0.3 51 | -------------------------------------------------------------------------------- /spec/cassettes/publishes/interests/invalid_payload.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com/publish_api/v1/instances/1b880590-6301-4bb5-b34f-45db1c5f5644/publishes 6 | body: 7 | encoding: UTF-8 8 | string: '{"invalid":"payload"}' 9 | headers: 10 | Accept: 11 | - application/json 12 | Accept-Encoding: 13 | - gzip, deflate 14 | User-Agent: 15 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 16 | Content-Type: 17 | - application/json 18 | Authorization: 19 | - Bearer F8AC0B756E50DF235F642D6F0DC2CDE0328CD9184B3874C5E91AB2189BB722FE 20 | X-Pusher-Library: 21 | - pusher-push-notifications-ruby 1.0.0 22 | Content-Length: 23 | - '21' 24 | Host: 25 | - 1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com 26 | response: 27 | status: 28 | code: 422 29 | message: Unprocessable Entity 30 | headers: 31 | Server: 32 | - Cowboy 33 | Connection: 34 | - keep-alive 35 | Content-Type: 36 | - application/json 37 | Date: 38 | - Thu, 21 Feb 2019 13:41:33 GMT 39 | Content-Length: 40 | - '75' 41 | Via: 42 | - 1.1 vegur 43 | body: 44 | encoding: UTF-8 45 | string: '{"error":"Bad Request","description":"`interests`: interests is required"} 46 | 47 | ' 48 | http_version: 49 | recorded_at: Thu, 21 Feb 2019 13:41:33 GMT 50 | recorded_with: VCR 3.0.3 51 | -------------------------------------------------------------------------------- /spec/cassettes/publishes/invalid_instance_id.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://abe1212381f60.pushnotifications.pusher.com/publish_api/v1/instances/abe1212381f60/publishes 6 | body: 7 | encoding: UTF-8 8 | string: '{"interests":["hello"],"apns":{"aps":{"alert":{"title":"Hello","body":"Hello, 9 | world!"}}},"fcm":{"notification":{"title":"Hello","body":"Hello, world!"}}}' 10 | headers: 11 | Accept: 12 | - application/json 13 | Accept-Encoding: 14 | - gzip, deflate 15 | User-Agent: 16 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 17 | Content-Type: 18 | - application/json 19 | Authorization: 20 | - Bearer F8AC0B756E50DF235F642D6F0DC2CDE0328CD9184B3874C5E91AB2189BB722FE 21 | X-Pusher-Library: 22 | - pusher-push-notifications-ruby 1.0.0 23 | Content-Length: 24 | - '153' 25 | Host: 26 | - abe1212381f60.pushnotifications.pusher.com 27 | response: 28 | status: 29 | code: 404 30 | message: Not Found 31 | headers: 32 | Server: 33 | - Cowboy 34 | Connection: 35 | - keep-alive 36 | Content-Type: 37 | - application/json 38 | Date: 39 | - Thu, 21 Feb 2019 13:41:32 GMT 40 | Content-Length: 41 | - '75' 42 | Via: 43 | - 1.1 vegur 44 | body: 45 | encoding: UTF-8 46 | string: '{"error":"Instance not found","description":"Could not find the instance"} 47 | 48 | ' 49 | http_version: 50 | recorded_at: Thu, 21 Feb 2019 13:41:32 GMT 51 | recorded_with: VCR 3.0.3 52 | -------------------------------------------------------------------------------- /spec/cassettes/publishes/invalid_secret_key.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com/publish_api/v1/instances/1b880590-6301-4bb5-b34f-45db1c5f5644/publishes 6 | body: 7 | encoding: UTF-8 8 | string: '{"interests":["hello"],"apns":{"aps":{"alert":{"title":"Hello","body":"Hello, 9 | world!"}}},"fcm":{"notification":{"title":"Hello","body":"Hello, world!"}}}' 10 | headers: 11 | Accept: 12 | - application/json 13 | Accept-Encoding: 14 | - gzip, deflate 15 | User-Agent: 16 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 17 | Content-Type: 18 | - application/json 19 | Authorization: 20 | - Bearer wrong-secret-key 21 | X-Pusher-Library: 22 | - pusher-push-notifications-ruby 1.0.0 23 | Content-Length: 24 | - '153' 25 | Host: 26 | - 1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com 27 | response: 28 | status: 29 | code: 401 30 | message: Unauthorized 31 | headers: 32 | Server: 33 | - Cowboy 34 | Connection: 35 | - keep-alive 36 | Content-Type: 37 | - application/json 38 | Date: 39 | - Thu, 21 Feb 2019 13:41:32 GMT 40 | Content-Length: 41 | - '62' 42 | Via: 43 | - 1.1 vegur 44 | body: 45 | encoding: UTF-8 46 | string: '{"error":"Unauthorized","description":"Incorrect Secret Key"} 47 | 48 | ' 49 | http_version: 50 | recorded_at: Thu, 21 Feb 2019 13:41:32 GMT 51 | recorded_with: VCR 3.0.3 52 | -------------------------------------------------------------------------------- /spec/cassettes/delete/user/id_too_long.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: delete 5 | uri: https://1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com/customer_api/v1/instances/1b880590-6301-4bb5-b34f-45db1c5f5644/users/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept: 11 | - application/json 12 | Accept-Encoding: 13 | - gzip, deflate 14 | User-Agent: 15 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 16 | Content-Type: 17 | - application/json 18 | Authorization: 19 | - Bearer F8AC0B756E50DF235F642D6F0DC2CDE0328CD9184B3874C5E91AB2189BB722FE 20 | X-Pusher-Library: 21 | - pusher-push-notifications-ruby 1.0.0 22 | Host: 23 | - 1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com 24 | response: 25 | status: 26 | code: 400 27 | message: Bad Request 28 | headers: 29 | Server: 30 | - Cowboy 31 | Connection: 32 | - keep-alive 33 | Content-Type: 34 | - application/json 35 | Date: 36 | - Thu, 21 Feb 2019 13:41:35 GMT 37 | Content-Length: 38 | - '81' 39 | Via: 40 | - 1.1 vegur 41 | body: 42 | encoding: UTF-8 43 | string: '{"error":"Bad request","description":"''User id'' is too long, max 44 | length is 164"} 45 | 46 | ' 47 | http_version: 48 | recorded_at: Thu, 21 Feb 2019 13:41:35 GMT 49 | recorded_with: VCR 3.0.3 50 | -------------------------------------------------------------------------------- /spec/cassettes/publishes/interests/valid_payload.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com/publish_api/v1/instances/1b880590-6301-4bb5-b34f-45db1c5f5644/publishes 6 | body: 7 | encoding: UTF-8 8 | string: '{"interests":["hello"],"apns":{"aps":{"alert":{"title":"Hello","body":"Hello, 9 | world!"}}},"fcm":{"notification":{"title":"Hello","body":"Hello, world!"}}}' 10 | headers: 11 | Accept: 12 | - application/json 13 | Accept-Encoding: 14 | - gzip, deflate 15 | User-Agent: 16 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 17 | Content-Type: 18 | - application/json 19 | Authorization: 20 | - Bearer F8AC0B756E50DF235F642D6F0DC2CDE0328CD9184B3874C5E91AB2189BB722FE 21 | X-Pusher-Library: 22 | - pusher-push-notifications-ruby 1.0.0 23 | Content-Length: 24 | - '153' 25 | Host: 26 | - 1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com 27 | response: 28 | status: 29 | code: 200 30 | message: OK 31 | headers: 32 | Server: 33 | - Cowboy 34 | Connection: 35 | - keep-alive 36 | Date: 37 | - Thu, 21 Feb 2019 13:41:33 GMT 38 | Content-Length: 39 | - '59' 40 | Content-Type: 41 | - text/plain; charset=utf-8 42 | Via: 43 | - 1.1 vegur 44 | body: 45 | encoding: UTF-8 46 | string: '{"publishId":"pubid-ead50f20-eba6-49ad-91d7-bafe15aee8fd"} 47 | 48 | ' 49 | http_version: 50 | recorded_at: Thu, 21 Feb 2019 13:41:33 GMT 51 | recorded_with: VCR 3.0.3 52 | -------------------------------------------------------------------------------- /spec/cassettes/publishes/users/valid_payload.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com/publish_api/v1/instances/1b880590-6301-4bb5-b34f-45db1c5f5644/publishes/users 6 | body: 7 | encoding: UTF-8 8 | string: '{"users":["jonathan","jordan","luis","luka","mina"],"apns":{"aps":{"alert":{"title":"Hello","body":"Hello, 9 | world!"}}},"fcm":{"notification":{"title":"Hello","body":"Hello, world!"}}}' 10 | headers: 11 | Accept: 12 | - application/json 13 | Accept-Encoding: 14 | - gzip, deflate 15 | User-Agent: 16 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 17 | Content-Type: 18 | - application/json 19 | Authorization: 20 | - Bearer F8AC0B756E50DF235F642D6F0DC2CDE0328CD9184B3874C5E91AB2189BB722FE 21 | X-Pusher-Library: 22 | - pusher-push-notifications-ruby 1.0.0 23 | Content-Length: 24 | - '182' 25 | Host: 26 | - 1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com 27 | response: 28 | status: 29 | code: 200 30 | message: OK 31 | headers: 32 | Server: 33 | - Cowboy 34 | Connection: 35 | - keep-alive 36 | Date: 37 | - Thu, 21 Feb 2019 13:41:34 GMT 38 | Content-Length: 39 | - '59' 40 | Content-Type: 41 | - text/plain; charset=utf-8 42 | Via: 43 | - 1.1 vegur 44 | body: 45 | encoding: UTF-8 46 | string: '{"publishId":"pubid-e164f94d-e938-4aa5-a430-d27f963fe1c0"} 47 | 48 | ' 49 | http_version: 50 | recorded_at: Thu, 21 Feb 2019 13:41:34 GMT 51 | recorded_with: VCR 3.0.3 52 | -------------------------------------------------------------------------------- /lib/pusher/push_notifications/use_cases/publish.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Pusher 4 | module PushNotifications 5 | module UseCases 6 | class Publish 7 | class PublishError < RuntimeError; end 8 | 9 | # Publish the given payload to the specified interests. 10 | # DEPRECATED: Please use publish_to_interests instead. 11 | def self.publish(client, interests:, payload: {}) 12 | warn "[DEPRECATION] `publish` is deprecated. \ 13 | Please use `publish_to_interests` instead." 14 | publish_to_interests(client, interests: interests, payload: payload) 15 | end 16 | 17 | # Publish the given payload to the specified interests. 18 | def self.publish_to_interests(client, interests:, payload: {}) 19 | valid_interest_pattern = /^(_|-|=|@|,|\.|:|[A-Z]|[a-z]|[0-9])*$/ 20 | 21 | interests.each do |interest| 22 | next if valid_interest_pattern.match?(interest) 23 | 24 | raise PublishError, 25 | "Invalid interest name \nMax #{UserId::MAX_USER_ID_LENGTH}" \ 26 | ' characters and can only contain ASCII upper/lower-case' \ 27 | ' letters, numbers or one of _-=@,.:' 28 | end 29 | 30 | raise PublishError, 'Must provide at least one interest' if interests.empty? 31 | 32 | if interests.length > 100 33 | raise PublishError, "Number of interests #{interests.length}" \ 34 | ' exceeds maximum of 100' 35 | end 36 | 37 | data = { interests: interests }.merge!(payload) 38 | client.post('publishes', data) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/pusher/push_notifications/generate_token_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'jwt' 5 | 6 | RSpec.describe Pusher::PushNotifications, '.generate_token' do 7 | def generate_token 8 | described_class.generate_token(user: user) 9 | end 10 | 11 | let(:user) { 'Elmo' } 12 | let(:instance_id) { described_class.instance_id } 13 | let(:secret_key) { described_class.secret_key } 14 | 15 | context 'when user id is empty' do 16 | let(:user) { '' } 17 | 18 | it 'token generation will fail' do 19 | expect { generate_token }.to raise_error( 20 | Pusher::PushNotifications::UseCases::GenerateToken::GenerateTokenError 21 | ).with_message( 22 | 'User Id cannot be empty.' 23 | ) 24 | end 25 | end 26 | 27 | context 'when user id is too long' do 28 | max_user_id_length = Pusher::PushNotifications::UserId::MAX_USER_ID_LENGTH 29 | let(:user) { 'a' * (max_user_id_length + 1) } 30 | 31 | it 'user deletion request will fail' do 32 | expect { generate_token }.to raise_error( 33 | Pusher::PushNotifications::UseCases::GenerateToken::GenerateTokenError 34 | ).with_message( 35 | 'User id length too long (expected fewer than 165 characters)' 36 | ) 37 | end 38 | end 39 | 40 | context 'when user id is valid' do 41 | it 'will generate valid token' do 42 | expect do 43 | JWT.decode generate_token['token'], secret_key, true 44 | end.to_not raise_error 45 | end 46 | it 'will contain user id in the \'sub\' (subject) claim' do 47 | decoded_token = JWT.decode generate_token['token'], nil, false 48 | expect(decoded_token.first['sub']).to eq(user) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /pusher-push-notifications.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'pusher/push_notifications/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.required_ruby_version = '>= 2.4.2' 9 | spec.name = 'pusher-push-notifications' 10 | spec.version = Pusher::PushNotifications::VERSION 11 | spec.authors = ['Lucas Medeiros', 'Pusher'] 12 | spec.email = ['lucastoc@gmail.com', 'support@pusher.com'] 13 | 14 | spec.summary = 'Pusher Push Notifications Ruby server SDK' 15 | spec.homepage = 'https://github.com/pusher/push-notifications-ruby' 16 | spec.license = 'MIT' 17 | 18 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 19 | f.match(%r{^(test|spec|features)/}) 20 | end 21 | spec.bindir = 'exe' 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.require_paths = ['lib'] 24 | 25 | spec.add_dependency 'jwt', '~> 2.1', '>= 2.1.0' 26 | spec.add_dependency 'rest-client', '~> 2.0', '>= 2.0.2' 27 | 28 | spec.add_development_dependency 'bundler', '~> 2.2' 29 | spec.add_development_dependency 'codecov', '~> 0' 30 | spec.add_development_dependency 'coveralls', '~> 0.8.21' 31 | spec.add_development_dependency 'dotenv', '~> 2.2', '>= 2.2.1' 32 | spec.add_development_dependency 'pry-byebug', '~> 3.6', '>= 3.6.0' 33 | spec.add_development_dependency 'rake', '~> 13.0' 34 | spec.add_development_dependency 'rb-readline', '~> 0' 35 | spec.add_development_dependency 'rspec', '~> 3.0' 36 | spec.add_development_dependency 'rubocop', '>= 0.49.0' 37 | spec.add_development_dependency 'vcr', '~> 3.0', '>= 3.0.3' 38 | spec.add_development_dependency 'webmock', '~> 3.0', '>= 3.0.1' 39 | end 40 | -------------------------------------------------------------------------------- /lib/pusher/push_notifications/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'forwardable' 4 | require 'json' 5 | require 'rest-client' 6 | 7 | module Pusher 8 | module PushNotifications 9 | class Client 10 | extend Forwardable 11 | 12 | Response = Struct.new(:status, :content, :ok?) 13 | 14 | def initialize(config: PushNotifications) 15 | @config = config 16 | end 17 | 18 | def post(resource, payload = {}) 19 | url = build_publish_url(resource) 20 | body = payload.to_json 21 | 22 | RestClient::Request.execute( 23 | method: :post, url: url, 24 | payload: body, headers: headers 25 | ) do |response| 26 | status = response.code 27 | if json?(response.body) 28 | body = JSON.parse(response.body) 29 | Response.new(status, body, status == 200) 30 | else 31 | Response.new(status, nil, false) 32 | end 33 | end 34 | end 35 | 36 | def delete(user) 37 | url_encoded_user_id = CGI.escape(user) 38 | url = build_users_url(url_encoded_user_id) 39 | 40 | RestClient::Request.execute( 41 | method: :delete, url: url, 42 | headers: headers 43 | ) do |response| 44 | status = response.code 45 | case status 46 | when 200 47 | Response.new(status, nil, true) 48 | else 49 | Response.new(status, nil, false) 50 | end 51 | end 52 | end 53 | 54 | private 55 | 56 | attr_reader :config 57 | 58 | def_delegators :@config, :instance_id, :secret_key, :endpoint 59 | 60 | def build_publish_url(resource) 61 | "#{endpoint}/publish_api/v1/instances/#{instance_id}/#{resource}" 62 | end 63 | 64 | def build_users_url(user) 65 | "#{endpoint}/customer_api/v1/instances/#{instance_id}/users/#{user}" 66 | end 67 | 68 | def headers 69 | { 70 | content_type: 'application/json', 71 | accept: :json, 72 | Authorization: "Bearer #{secret_key}", 73 | 'X-Pusher-Library': 74 | 'pusher-push-notifications-ruby ' \ 75 | "#{Pusher::PushNotifications::VERSION}" 76 | } 77 | end 78 | 79 | def json?(response) 80 | JSON.parse(response) 81 | true 82 | rescue JSON::ParserError 83 | false 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/pusher/push_notifications.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative './push_notifications/client' 4 | require_relative './push_notifications/use_cases/publish' 5 | require_relative './push_notifications/use_cases/publish_to_users' 6 | require_relative './push_notifications/use_cases/delete_user' 7 | require_relative './push_notifications/use_cases/generate_token' 8 | require_relative './push_notifications/version' 9 | require_relative './push_notifications/user_id' 10 | require_relative './push_notifications/token' 11 | 12 | module Pusher 13 | module PushNotifications 14 | class PushError < RuntimeError; end 15 | 16 | class << self 17 | attr_reader :instance_id, :secret_key 18 | 19 | def configure 20 | yield(self) 21 | # returning a duplicate of `self` to allow multiple clients to be 22 | # configured without needing to reconfigure the singleton instance 23 | dup 24 | end 25 | 26 | def instance_id=(instance_id) 27 | raise PushError, 'Invalid instance id' if instance_id.nil? || instance_id.delete(' ').empty? 28 | 29 | @instance_id = instance_id 30 | end 31 | 32 | def secret_key=(secret_key) 33 | raise PushError, 'Invalid secret key' if secret_key.nil? || secret_key.delete(' ').empty? 34 | 35 | @secret_key = secret_key 36 | end 37 | 38 | def endpoint=(endpoint) 39 | raise PushError, 'Invalid endpoint override' if !endpoint.nil? && endpoint.delete(' ').empty? 40 | 41 | @endpoint = endpoint 42 | end 43 | 44 | def endpoint 45 | return @endpoint unless @endpoint.nil? 46 | 47 | "https://#{@instance_id}.pushnotifications.pusher.com" 48 | end 49 | 50 | def publish(interests:, payload: {}) 51 | UseCases::Publish.publish(client, interests: interests, payload: payload) 52 | end 53 | 54 | def publish_to_interests(interests:, payload: {}) 55 | UseCases::Publish.publish_to_interests(client, interests: interests, payload: payload) 56 | end 57 | 58 | def publish_to_users(users:, payload: {}) 59 | UseCases::PublishToUsers.publish_to_users(client, users: users, payload: payload) 60 | end 61 | 62 | def delete_user(user:) 63 | UseCases::DeleteUser.delete_user(client, user: user) 64 | end 65 | 66 | def generate_token(user:) 67 | UseCases::GenerateToken.generate_token(user: user) 68 | end 69 | 70 | private 71 | 72 | def client 73 | @client ||= Client.new(config: self) 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/cassettes/publishes/users/valid_users.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com/publish_api/v1/instances/1b880590-6301-4bb5-b34f-45db1c5f5644/publishes/users 6 | body: 7 | encoding: UTF-8 8 | string: '{"users":["user-26","user-68","user-2","user-55","user-17","user-37","user-91","user-52","user-23","user-1","user-57","user-60","user-100","user-54","user-82","user-12","user-47","user-74","user-71","user-51","user-14","user-80","user-65","user-59","user-9","user-72","user-70","user-44","user-10","user-28","user-3","user-5","user-76","user-61","user-11","user-50","user-30","user-85","user-86","user-95","user-77","user-4","user-56","user-90","user-87","user-21","user-73","user-29","user-22","user-32","user-7","user-81","user-19","user-63","user-20","user-79","user-92","user-78","user-69","user-38","user-18","user-99","user-16","user-97","user-34","user-93","user-64","user-48","user-39","user-96","user-42","user-6","user-89","user-25","user-58","user-45","user-43","user-83","user-31","user-88","user-46","user-67","user-53","user-13","user-15","user-41","user-62","user-8","user-36","user-40","user-84","user-94","user-49","user-27","user-75","user-35","user-98","user-66","user-33","user-24"],"apns":{"aps":{"alert":{"title":"Hello","body":"Hello, 9 | world!"}}},"fcm":{"notification":{"title":"Hello","body":"Hello, world!"}}}' 10 | headers: 11 | Accept: 12 | - application/json 13 | Accept-Encoding: 14 | - gzip, deflate 15 | User-Agent: 16 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 17 | Content-Type: 18 | - application/json 19 | Authorization: 20 | - Bearer F8AC0B756E50DF235F642D6F0DC2CDE0328CD9184B3874C5E91AB2189BB722FE 21 | X-Pusher-Library: 22 | - pusher-push-notifications-ruby 1.0.0 23 | Content-Length: 24 | - '1133' 25 | Host: 26 | - 1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com 27 | response: 28 | status: 29 | code: 200 30 | message: OK 31 | headers: 32 | Server: 33 | - Cowboy 34 | Connection: 35 | - keep-alive 36 | Date: 37 | - Thu, 21 Feb 2019 13:41:36 GMT 38 | Content-Length: 39 | - '59' 40 | Content-Type: 41 | - text/plain; charset=utf-8 42 | Via: 43 | - 1.1 vegur 44 | body: 45 | encoding: UTF-8 46 | string: '{"publishId":"pubid-dcbbd331-9c65-415d-98ff-1afbdf57ddac"} 47 | 48 | ' 49 | http_version: 50 | recorded_at: Thu, 21 Feb 2019 13:41:36 GMT 51 | recorded_with: VCR 3.0.3 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | pull_request: 5 | types: [ labeled ] 6 | branches: 7 | - master 8 | 9 | jobs: 10 | prepare-release: 11 | name: Prepare release 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Set major release 16 | if: ${{ github.event.label.name == 'release-major' }} 17 | run: echo "RELEASE=major" >> $GITHUB_ENV 18 | - name: Set minor release 19 | if: ${{ github.event.label.name == 'release-minor' }} 20 | run: echo "RELEASE=minor" >> $GITHUB_ENV 21 | - name: Set patch release 22 | if: ${{ github.event.label.name == 'release-patch' }} 23 | run: echo "RELEASE=patch" >> $GITHUB_ENV 24 | - name: Check release env 25 | run: | 26 | if [[ -z "${{ env.RELEASE }}" ]]; 27 | then 28 | echo "You need to set a release label on PRs to the main branch" 29 | exit 1 30 | else 31 | exit 0 32 | fi 33 | - name: Install semver-tool 34 | run: | 35 | export DIR=$(mktemp -d) 36 | cd $DIR 37 | curl https://github.com/fsaintjacques/semver-tool/archive/3.2.0.tar.gz -L -o semver.tar.gz 38 | tar -xvf semver.tar.gz 39 | sudo cp semver-tool-3.2.0/src/semver /usr/local/bin 40 | - name: Bump version 41 | run: | 42 | export CURRENT=$(gem info pusher-push-notifications --remote --exact | grep -o "pusher-push-notifications ([0-9]*\.[0-9]*\.[0-9]*)" | awk -F '[()]' '{print $2}') 43 | export NEW_VERSION=$(semver bump ${{ env.RELEASE }} $CURRENT) 44 | echo "VERSION=$NEW_VERSION" >> $GITHUB_ENV 45 | - name: Checkout code 46 | uses: actions/checkout@v2 47 | - name: Setup git 48 | run: | 49 | git config user.email "pusher-ci@pusher.com" 50 | git config user.name "Pusher CI" 51 | git fetch 52 | git checkout ${{ github.event.pull_request.head.ref }} 53 | - name: Prepare CHANGELOG 54 | run: | 55 | echo "${{ github.event.pull_request.body }}" | csplit -s - "/##/" 56 | echo "# Changelog 57 | 58 | ## ${{ env.VERSION }} 59 | " >> CHANGELOG.tmp 60 | grep "^*" xx01 >> CHANGELOG.tmp 61 | grep -v "^# " CHANGELOG.md >> CHANGELOG.tmp 62 | cp CHANGELOG.tmp CHANGELOG.md 63 | - name: Prepare version.rb 64 | run: | 65 | sed -i "s|VERSION = '[^']*'|VERSION = '${{ env.VERSION }}'|" lib/pusher/push_notifications/version.rb 66 | - name: Prepare Gemfile.lock 67 | run: | 68 | sed -i "s|pusher-push-notifications ([^)]*)|pusher-push-notifications (${{ env.VERSION }})|" Gemfile.lock 69 | - name: Commit changes 70 | run: | 71 | git add CHANGELOG.md lib/pusher/push_notifications/version.rb Gemfile.lock 72 | git commit -m "Bump to version ${{ env.VERSION }}" 73 | - name: Push 74 | run: git push 75 | -------------------------------------------------------------------------------- /spec/cassettes/publishes/interests/valid_interests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com/publish_api/v1/instances/1b880590-6301-4bb5-b34f-45db1c5f5644/publishes 6 | body: 7 | encoding: UTF-8 8 | string: '{"interests":["interest-49","interest-92","interest-5","interest-99","interest-91","interest-86","interest-52","interest-33","interest-71","interest-85","interest-24","interest-21","interest-32","interest-38","interest-69","interest-58","interest-25","interest-64","interest-41","interest-16","interest-4","interest-94","interest-13","interest-93","interest-39","interest-76","interest-67","interest-11","interest-73","interest-56","interest-18","interest-12","interest-77","interest-15","interest-84","interest-100","interest-65","interest-6","interest-42","interest-95","interest-61","interest-14","interest-40","interest-43","interest-98","interest-63","interest-80","interest-53","interest-23","interest-37","interest-48","interest-88","interest-87","interest-20","interest-55","interest-79","interest-82","interest-1","interest-59","interest-89","interest-47","interest-72","interest-62","interest-44","interest-10","interest-2","interest-74","interest-8","interest-60","interest-26","interest-57","interest-81","interest-96","interest-19","interest-97","interest-66","interest-17","interest-50","interest-45","interest-3","interest-31","interest-78","interest-46","interest-9","interest-83","interest-30","interest-22","interest-29","interest-68","interest-54","interest-75","interest-27","interest-36","interest-70","interest-51","interest-90","interest-34","interest-28","interest-7","interest-35"],"apns":{"aps":{"alert":{"title":"Hello","body":"Hello, 9 | world!"}}},"fcm":{"notification":{"title":"Hello","body":"Hello, world!"}}}' 10 | headers: 11 | Accept: 12 | - application/json 13 | Accept-Encoding: 14 | - gzip, deflate 15 | User-Agent: 16 | - rest-client/2.0.2 (darwin17.5.0 x86_64) ruby/2.3.0p0 17 | Content-Type: 18 | - application/json 19 | Authorization: 20 | - Bearer F8AC0B756E50DF235F642D6F0DC2CDE0328CD9184B3874C5E91AB2189BB722FE 21 | X-Pusher-Library: 22 | - pusher-push-notifications-ruby 1.0.0 23 | Content-Length: 24 | - '1537' 25 | Host: 26 | - 1b880590-6301-4bb5-b34f-45db1c5f5644.pushnotifications.pusher.com 27 | response: 28 | status: 29 | code: 200 30 | message: OK 31 | headers: 32 | Server: 33 | - Cowboy 34 | Connection: 35 | - keep-alive 36 | Date: 37 | - Thu, 21 Feb 2019 13:41:35 GMT 38 | Content-Length: 39 | - '59' 40 | Content-Type: 41 | - text/plain; charset=utf-8 42 | Via: 43 | - 1.1 vegur 44 | body: 45 | encoding: UTF-8 46 | string: '{"publishId":"pubid-b31f2fcf-92dc-4f0f-b97d-6fdd82b7ac0f"} 47 | 48 | ' 49 | http_version: 50 | recorded_at: Thu, 21 Feb 2019 13:41:35 GMT 51 | recorded_with: VCR 3.0.3 52 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | pusher-push-notifications (2.0.2) 5 | jwt (~> 2.1, >= 2.1.0) 6 | rest-client (~> 2.0, >= 2.0.2) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | addressable (2.7.0) 12 | public_suffix (>= 2.0.2, < 5.0) 13 | ast (2.4.2) 14 | byebug (11.1.3) 15 | codecov (0.5.1) 16 | simplecov (>= 0.15, < 0.22) 17 | coderay (1.1.3) 18 | coveralls (0.8.23) 19 | json (>= 1.8, < 3) 20 | simplecov (~> 0.16.1) 21 | term-ansicolor (~> 1.3) 22 | thor (>= 0.19.4, < 2.0) 23 | tins (~> 1.6) 24 | crack (0.4.5) 25 | rexml 26 | diff-lcs (1.4.4) 27 | docile (1.3.5) 28 | domain_name (0.5.20190701) 29 | unf (>= 0.0.5, < 1.0.0) 30 | dotenv (2.7.6) 31 | hashdiff (1.0.1) 32 | http-accept (1.7.0) 33 | http-cookie (1.0.3) 34 | domain_name (~> 0.5) 35 | json (2.5.1) 36 | jwt (2.2.2) 37 | method_source (1.0.0) 38 | mime-types (3.3.1) 39 | mime-types-data (~> 3.2015) 40 | mime-types-data (3.2021.0225) 41 | netrc (0.11.0) 42 | parallel (1.20.1) 43 | parser (3.0.0.0) 44 | ast (~> 2.4.1) 45 | pry (0.13.1) 46 | coderay (~> 1.1) 47 | method_source (~> 1.0) 48 | pry-byebug (3.9.0) 49 | byebug (~> 11.0) 50 | pry (~> 0.13.0) 51 | public_suffix (4.0.6) 52 | rainbow (3.0.0) 53 | rake (13.0.3) 54 | rb-readline (0.5.5) 55 | regexp_parser (2.1.1) 56 | rest-client (2.1.0) 57 | http-accept (>= 1.7.0, < 2.0) 58 | http-cookie (>= 1.0.2, < 2.0) 59 | mime-types (>= 1.16, < 4.0) 60 | netrc (~> 0.8) 61 | rexml (3.2.4) 62 | rspec (3.10.0) 63 | rspec-core (~> 3.10.0) 64 | rspec-expectations (~> 3.10.0) 65 | rspec-mocks (~> 3.10.0) 66 | rspec-core (3.10.1) 67 | rspec-support (~> 3.10.0) 68 | rspec-expectations (3.10.1) 69 | diff-lcs (>= 1.2.0, < 2.0) 70 | rspec-support (~> 3.10.0) 71 | rspec-mocks (3.10.2) 72 | diff-lcs (>= 1.2.0, < 2.0) 73 | rspec-support (~> 3.10.0) 74 | rspec-support (3.10.2) 75 | rubocop (1.11.0) 76 | parallel (~> 1.10) 77 | parser (>= 3.0.0.0) 78 | rainbow (>= 2.2.2, < 4.0) 79 | regexp_parser (>= 1.8, < 3.0) 80 | rexml 81 | rubocop-ast (>= 1.2.0, < 2.0) 82 | ruby-progressbar (~> 1.7) 83 | unicode-display_width (>= 1.4.0, < 3.0) 84 | rubocop-ast (1.4.1) 85 | parser (>= 2.7.1.5) 86 | ruby-progressbar (1.11.0) 87 | simplecov (0.16.1) 88 | docile (~> 1.1) 89 | json (>= 1.8, < 3) 90 | simplecov-html (~> 0.10.0) 91 | simplecov-html (0.10.2) 92 | sync (0.5.0) 93 | term-ansicolor (1.7.1) 94 | tins (~> 1.0) 95 | thor (1.1.0) 96 | tins (1.28.0) 97 | sync 98 | unf (0.1.4) 99 | unf_ext 100 | unf_ext (0.0.7.7) 101 | unicode-display_width (2.0.0) 102 | vcr (3.0.3) 103 | webmock (3.12.0) 104 | addressable (>= 2.3.6) 105 | crack (>= 0.3.2) 106 | hashdiff (>= 0.4.0, < 2.0.0) 107 | 108 | PLATFORMS 109 | ruby 110 | 111 | DEPENDENCIES 112 | bundler (~> 2.2) 113 | codecov (~> 0) 114 | coveralls (~> 0.8.21) 115 | dotenv (~> 2.2, >= 2.2.1) 116 | pry-byebug (~> 3.6, >= 3.6.0) 117 | pusher-push-notifications! 118 | rake (~> 13.0) 119 | rb-readline (~> 0) 120 | rspec (~> 3.0) 121 | rubocop (>= 0.49.0) 122 | vcr (~> 3.0, >= 3.0.3) 123 | webmock (~> 3.0, >= 3.0.1) 124 | 125 | BUNDLED WITH 126 | 2.2.13 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pusher Beams Ruby Server SDK 2 | 3 | [![Test](https://github.com/pusher/push-notifications-ruby/actions/workflows/test.yml/badge.svg)](https://github.com/pusher/push-notifications-ruby/actions/workflows/test.yml) [![Coverage Status](https://coveralls.io/repos/github/pusher/push-notifications-ruby/badge.svg)](https://coveralls.io/github/pusher/push-notifications-ruby) [![Gem](https://img.shields.io/gem/v/pusher-push-notifications)](https://rubygems.org/gems/pusher-push-notifications) 4 | 5 | Pusher Beams using the Pusher system. 6 | 7 | ## Installation 8 | 9 | ```bash 10 | gem install pusher-push-notifications 11 | ``` 12 | 13 | Or add this line to your application's Gemfile: 14 | 15 | ```ruby 16 | gem 'pusher-push-notifications' 17 | ``` 18 | 19 | ## Configuration 20 | 21 | This configuration can be done anywhere you want, but if you are using rails the better place to put it is inside an initializer 22 | 23 | ```ruby 24 | require 'pusher/push_notifications' 25 | 26 | Pusher::PushNotifications.configure do |config| 27 |  config.instance_id = ENV['PUSHER_INSTANCE_ID'] # or the value directly 28 |  config.secret_key = ENV['PUSHER_SECRET_KEY'] 29 | end 30 | ``` 31 | 32 | Where `instance_id` and `secret_key` are the values of the instance you created in the Pusher Beams dashboard. 33 | 34 | If multiple clients are needed, store the reference that is returned from the `configure` method. 35 | 36 | ## Usage 37 | 38 | After the configuration is done you can push notifications like this: 39 | 40 | ```ruby 41 | require 'pusher/push_notifications' 42 | 43 | data = { 44 | apns: { 45 | aps: { 46 | alert: { 47 | title: 'Hello', 48 | body: 'Hello, world!' 49 | } 50 | } 51 | }, 52 | fcm: { 53 | notification: { 54 | title: 'Hello', 55 | body: 'Hello, world!' 56 | } 57 | } 58 | } 59 | 60 | # Publish the given 'data' to the specified interests. 61 | Pusher::PushNotifications.publish_to_interests(interests: ['hello'], payload: data) 62 | 63 | # Publish the given 'data' to the specified users. 64 | Pusher::PushNotifications.publish_to_users(users: ['jonathan', 'jordan', 'luis', 'luka', 'mina'], payload: data) 65 | 66 | # Authenticate User 67 | Pusher::PushNotifications.generate_token(user: 'Elmo') 68 | 69 | # Delete User 70 | Pusher::PushNotifications.delete_user(user: 'Elmo') 71 | ``` 72 | 73 | The return of this call is a ruby struct containing the http status code (`status`) the response body (`content`) and an `ok?` attribute saying if the notification was successful or not. 74 | 75 | **NOTE**: It's optional but you can insert a `data` key at the same level of the `aps` and `notification` keys with a custom value (A json for example), but keep in mind that you have the limitation of 10kb per message. 76 | 77 | ## Errors 78 | 79 | All available error responses can be be found [here](https://docs.pusher.com/beams/reference/publish-api#error-responses). 80 | 81 | ## Development 82 | 83 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 84 | 85 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 86 | 87 | ## Contributing 88 | 89 | - Found a bug? Please open an [issue](https://github.com/pusher/push-notifications-ruby/issues). 90 | - Have a feature request. Please open an [issue](https://github.com/pusher/push-notifications-ruby/issues). 91 | - If you want to contribute, please submit a [pull request](https://github.com/pusher/push-notifications-ruby/pulls) (preferrably with some tests). 92 | 93 | ## License 94 | 95 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 96 | -------------------------------------------------------------------------------- /spec/pusher/push_notifications/publish_to_users_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pusher::PushNotifications, '.publish_to_users' do 6 | def publish_to_users 7 | described_class.publish_to_users(users: users, payload: payload) 8 | end 9 | 10 | let(:users) { %w[jonathan jordan luis luka mina] } 11 | let(:payload) do 12 | { 13 | apns: { 14 | aps: { 15 | alert: { 16 | title: 'Hello', 17 | body: 'Hello, world!' 18 | } 19 | } 20 | }, 21 | fcm: { 22 | notification: { 23 | title: 'Hello', 24 | body: 'Hello, world!' 25 | } 26 | } 27 | } 28 | end 29 | 30 | context 'when payload is malformed' do 31 | let(:payload) do 32 | { invalid: 'payload' } 33 | end 34 | 35 | it 'does not send the notification' do 36 | VCR.use_cassette('publishes/users/invalid_payload') do 37 | response = publish_to_users 38 | expect(response).not_to be_ok 39 | end 40 | end 41 | end 42 | 43 | context 'when payload is correct' do 44 | it 'sends the notification' do 45 | VCR.use_cassette('publishes/users/valid_payload') do 46 | response = publish_to_users 47 | 48 | expect(response).to be_ok 49 | end 50 | end 51 | end 52 | 53 | context 'when no user ids are supplied' do 54 | let(:users) { [] } 55 | 56 | it 'warns an user id array should not be empty' do 57 | expect { publish_to_users }.to raise_error( 58 | Pusher::PushNotifications::UseCases:: 59 | PublishToUsers::UsersPublishError 60 | ).with_message( 61 | 'Must supply at least one user id.' 62 | ) 63 | end 64 | end 65 | 66 | context 'when user id is an empty string' do 67 | let(:users) { [''] } 68 | 69 | it 'warns an user id is invalid' do 70 | expect { publish_to_users }.to raise_error( 71 | Pusher::PushNotifications::UseCases:: 72 | PublishToUsers::UsersPublishError 73 | ).with_message( 74 | 'User Id cannot be empty.' 75 | ) 76 | end 77 | end 78 | 79 | context 'when user id length is too long' do 80 | max_user_id_length = Pusher::PushNotifications::UserId::MAX_USER_ID_LENGTH 81 | user_id = 'a' * (max_user_id_length + 1) 82 | let(:users) { [user_id] } 83 | 84 | it 'warns an user id is invalid' do 85 | expect { publish_to_users }.to raise_error( 86 | Pusher::PushNotifications::UseCases:: 87 | PublishToUsers::UsersPublishError 88 | ).with_message( 89 | 'User id length too long (expected fewer than ' \ 90 | "#{max_user_id_length + 1} characters)" 91 | ) 92 | end 93 | end 94 | 95 | context 'when 100 user ids are provided' do 96 | int_array = (1..100).to_a.shuffle 97 | test_users = int_array.map do |num| 98 | "user-#{num}" 99 | end 100 | 101 | let(:users) { test_users } 102 | 103 | it 'sends the notification' do 104 | VCR.use_cassette('publishes/users/valid_users') do 105 | response = publish_to_users 106 | 107 | expect(response).to be_ok 108 | end 109 | end 110 | end 111 | 112 | context 'when too many user ids are provided' do 113 | max_num_user_ids = Pusher::PushNotifications::UserId::MAX_NUM_USER_IDS 114 | int_array = (1..max_num_user_ids + 1).to_a.shuffle 115 | test_users = int_array.map do |num| 116 | "user-#{num}" 117 | end 118 | 119 | let(:users) { test_users } 120 | 121 | it 'raises an error' do 122 | VCR.use_cassette('publishes/users/valid_users') do 123 | expect { publish_to_users }.to raise_error( 124 | Pusher::PushNotifications::UseCases:: 125 | PublishToUsers::UsersPublishError 126 | ).with_message("Number of user ids #{test_users.length} \ 127 | exceeds maximum of 1000.") 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /spec/pusher/push_notifications/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pusher::PushNotifications do 6 | describe '.configure' do 7 | subject(:configuration) do 8 | described_class.configure do |config| 9 | config.instance_id = instance_id 10 | config.secret_key = secret_key 11 | config.endpoint = endpoint 12 | end 13 | end 14 | 15 | let(:instance_id) { ENV['PUSHER_INSTANCE_ID'] } 16 | let(:secret_key) { ENV['PUSHER_SECRET_KEY'] } 17 | let(:endpoint) { nil } 18 | 19 | around do |ex| 20 | original_instance_id = described_class.instance_id 21 | original_secret_key = described_class.secret_key 22 | original_endpoint = described_class.endpoint 23 | 24 | ex.run 25 | 26 | described_class.configure do |c| 27 | c.instance_id = original_instance_id 28 | c.secret_key = original_secret_key 29 | c.endpoint = original_endpoint 30 | end 31 | end 32 | 33 | context 'when instance id is not valid' do 34 | context 'when instance_id is nil' do 35 | let(:instance_id) { nil } 36 | 37 | it 'warns instance_id is invalid' do 38 | expect { configuration }.to raise_error( 39 | Pusher::PushNotifications::PushError 40 | ).with_message('Invalid instance id') 41 | end 42 | end 43 | 44 | context 'when instance_id is empty' do 45 | let(:instance_id) { ' ' } 46 | 47 | it 'warns instance_id is invalid' do 48 | expect { configuration }.to raise_error( 49 | Pusher::PushNotifications::PushError 50 | ).with_message('Invalid instance id') 51 | end 52 | end 53 | end 54 | 55 | context 'when secret key is not valid' do 56 | context 'when secret_key is nil' do 57 | let(:secret_key) { nil } 58 | 59 | it 'warns secret_key is invalid' do 60 | expect { configuration }.to raise_error( 61 | Pusher::PushNotifications::PushError 62 | ).with_message('Invalid secret key') 63 | end 64 | end 65 | 66 | context 'when secret_key is empty' do 67 | let(:secret_key) { ' ' } 68 | 69 | it 'warns secret_key is invalid' do 70 | expect { configuration }.to raise_error( 71 | Pusher::PushNotifications::PushError 72 | ).with_message('Invalid secret key') 73 | end 74 | end 75 | end 76 | 77 | context 'when endpoint is not valid' do 78 | context 'when endpoint is empty' do 79 | let(:endpoint) { ' ' } 80 | 81 | it 'warns endpoint is invalid' do 82 | expect { configuration }.to raise_error( 83 | Pusher::PushNotifications::PushError 84 | ).with_message('Invalid endpoint override') 85 | end 86 | end 87 | end 88 | 89 | context 'when endpoint is valid' do 90 | let(:endpoint) { 'https://testcluster.pusher.com' } 91 | 92 | it 'overrides the default endpoint' do 93 | configuration 94 | 95 | expect(configuration.endpoint).to eq('https://testcluster.pusher.com') 96 | end 97 | end 98 | 99 | context 'when instance id and secret key are valid' do 100 | it 'has everything set up' do 101 | configuration 102 | 103 | expect(configuration.instance_id).not_to be_nil 104 | expect(configuration.instance_id).not_to be_empty 105 | 106 | expect(configuration.secret_key).not_to be_nil 107 | expect(configuration.secret_key).not_to be_empty 108 | 109 | expect(configuration.endpoint).not_to be_nil 110 | expect(configuration.endpoint).not_to be_empty 111 | expect(configuration.endpoint).to eq( 112 | "https://#{configuration.instance_id}.pushnotifications.pusher.com" 113 | ) 114 | end 115 | end 116 | 117 | context 'when multiple instances are needed' do 118 | it 'returns unique clients' do 119 | client1 = described_class.configure do |config| 120 | config.instance_id = 'acd22e93-d8d6-43ba-9023-20ec05b1d08e' 121 | config.secret_key = '123' 122 | config.endpoint = 'https://testcluster.pusher.com' 123 | end 124 | client2 = described_class.configure do |config| 125 | config.instance_id = instance_id 126 | config.secret_key = secret_key 127 | config.endpoint = endpoint 128 | end 129 | 130 | expect(client1).not_to eq(client2) 131 | expect(client1.instance_id).not_to eq(client2.instance_id) 132 | expect(client1.secret_key).not_to eq(client2.secret_key) 133 | expect(client1.endpoint).not_to eq(client2.endpoint) 134 | end 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/pusher/push_notifications/publish_to_interests_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe Pusher::PushNotifications, '.publish_to_interests' do 6 | def publish_to_interests 7 | described_class.publish_to_interests(interests: interests, payload: payload) 8 | end 9 | 10 | let(:interests) { ['hello'] } 11 | let(:payload) do 12 | { 13 | apns: { 14 | aps: { 15 | alert: { 16 | title: 'Hello', 17 | body: 'Hello, world!' 18 | } 19 | } 20 | }, 21 | fcm: { 22 | notification: { 23 | title: 'Hello', 24 | body: 'Hello, world!' 25 | } 26 | } 27 | } 28 | end 29 | 30 | describe '#publish_to_interests' do 31 | context 'when payload is malformed' do 32 | let(:payload) do 33 | { invalid: 'payload' } 34 | end 35 | 36 | it 'does not send the notification' do 37 | VCR.use_cassette('publishes/interests/invalid_payload') do 38 | response = publish_to_interests 39 | 40 | expect(response).not_to be_ok 41 | end 42 | end 43 | end 44 | 45 | context 'when payload is correct' do 46 | it 'sends the notification' do 47 | VCR.use_cassette('publishes/interests/valid_payload') do 48 | response = publish_to_interests 49 | 50 | expect(response).to be_ok 51 | end 52 | end 53 | end 54 | 55 | context 'when interest name is invalid' do 56 | let(:interests) { ['lovely-valid-interest', 'hey €€ ***'] } 57 | max_user_id_length = Pusher::PushNotifications::UserId::MAX_USER_ID_LENGTH 58 | 59 | it 'warns an interest name is invalid' do 60 | expect { publish_to_interests }.to raise_error( 61 | Pusher::PushNotifications::UseCases::Publish::PublishError 62 | ).with_message("Invalid interest name \nMax #{max_user_id_length} \ 63 | characters and can only contain ASCII upper/lower-case letters, numbers \ 64 | or one of _-=@,.:") 65 | end 66 | end 67 | 68 | context 'when no interests provided' do 69 | let(:interests) { [] } 70 | 71 | it 'warns to provide at least one interest' do 72 | expect { publish_to_interests }.to raise_error( 73 | Pusher::PushNotifications::UseCases::Publish::PublishError 74 | ).with_message('Must provide at least one interest') 75 | end 76 | end 77 | 78 | context 'when 100 interests provided' do 79 | int_array = (1..100).to_a.shuffle 80 | test_interests = int_array.map do |num| 81 | "interest-#{num}" 82 | end 83 | 84 | let(:interests) { test_interests } 85 | 86 | it 'sends the notification' do 87 | VCR.use_cassette('publishes/interests/valid_interests') do 88 | response = publish_to_interests 89 | 90 | expect(response).to be_ok 91 | end 92 | end 93 | end 94 | 95 | context 'when too many interests are provided' do 96 | int_array = (1..101).to_a.shuffle 97 | test_interests = int_array.map do |num| 98 | "interest-#{num}" 99 | end 100 | 101 | let(:interests) { test_interests } 102 | 103 | it 'raises an error' do 104 | VCR.use_cassette('publishes/interests/valid_interests') do 105 | expect { publish_to_interests }.to raise_error( 106 | Pusher::PushNotifications::UseCases::Publish::PublishError 107 | ).with_message("Number of interests #{interests.length} \ 108 | exceeds maximum of 100") 109 | end 110 | end 111 | end 112 | 113 | context 'when there are two instances' do 114 | around do |ex| 115 | original_instance_id = described_class.instance_id 116 | original_secret_key = described_class.secret_key 117 | original_endpoint = described_class.endpoint 118 | 119 | ex.run 120 | 121 | described_class.configure do |c| 122 | c.instance_id = original_instance_id 123 | c.secret_key = original_secret_key 124 | c.endpoint = original_endpoint 125 | end 126 | end 127 | 128 | it 'uses the correct client' do 129 | client_a = Pusher::PushNotifications.configure do |config| 130 | config.instance_id = '123' 131 | config.secret_key = 'abc' 132 | end 133 | 134 | client_b = Pusher::PushNotifications.configure do |config| 135 | config.instance_id = '456' 136 | config.secret_key = 'def' 137 | end 138 | 139 | expect(Pusher::PushNotifications::UseCases::Publish) 140 | .to receive(:publish_to_interests) 141 | .with(client_a.send(:client), interests: interests, payload: payload) 142 | .and_return(nil) 143 | .once 144 | 145 | expect(Pusher::PushNotifications::UseCases::Publish) 146 | .to receive(:publish_to_interests) 147 | .with(client_b.send(:client), interests: interests, payload: payload) 148 | .and_return(nil) 149 | .once 150 | 151 | client_a.publish_to_interests(interests: interests, payload: payload) 152 | client_b.publish_to_interests(interests: interests, payload: payload) 153 | end 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /spec/pusher/push_notifications/client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Pusher::PushNotifications::Client do 4 | subject(:client) { described_class.new(config: config) } 5 | 6 | let(:config) do 7 | double( 8 | :config, 9 | instance_id: instance_id, 10 | secret_key: secret_key, 11 | endpoint: "https://#{instance_id}.pushnotifications.pusher.com" 12 | ) 13 | end 14 | 15 | describe '#post' do 16 | subject(:send_post) { client.post(resource, body) } 17 | let(:resource) { 'publishes' } 18 | 19 | let(:instance_id) { ENV['PUSHER_INSTANCE_ID'] } 20 | let(:secret_key) { ENV['PUSHER_SECRET_KEY'] } 21 | let(:body) do 22 | { 23 | interests: [ 24 | 'hello' 25 | ], 26 | apns: { 27 | aps: { 28 | alert: { 29 | title: 'Hello', 30 | body: 'Hello, world!' 31 | } 32 | } 33 | }, 34 | fcm: { 35 | notification: { 36 | title: 'Hello', 37 | body: 'Hello, world!' 38 | } 39 | } 40 | } 41 | end 42 | 43 | context 'when instance not found' do 44 | let(:instance_id) { 'abe1212381f60' } 45 | 46 | it 'returns 404' do 47 | VCR.use_cassette('publishes/invalid_instance_id') do 48 | response = send_post 49 | 50 | expect(response.status).to eq 404 51 | end 52 | end 53 | end 54 | 55 | context 'when secret key is incorrect' do 56 | let(:secret_key) { 'wrong-secret-key' } 57 | 58 | it 'returns 401' do 59 | VCR.use_cassette('publishes/invalid_secret_key') do 60 | response = send_post 61 | 62 | expect(response.status).to eq 401 63 | end 64 | end 65 | end 66 | end 67 | describe '#post_interests' do 68 | subject(:send_post) { client.post(resource, body) } 69 | let(:resource) { 'publishes' } 70 | 71 | let(:instance_id) { ENV['PUSHER_INSTANCE_ID'] } 72 | let(:secret_key) { ENV['PUSHER_SECRET_KEY'] } 73 | let(:body) do 74 | { 75 | interests: [ 76 | 'hello' 77 | ], 78 | apns: { 79 | aps: { 80 | alert: { 81 | title: 'Hello', 82 | body: 'Hello, world!' 83 | } 84 | } 85 | }, 86 | fcm: { 87 | notification: { 88 | title: 'Hello', 89 | body: 'Hello, world!' 90 | } 91 | } 92 | } 93 | end 94 | 95 | context 'when publish to interests payload is invalid' do 96 | let(:body) do 97 | { 98 | invalid: 'payload' 99 | } 100 | end 101 | 102 | it 'return 422' do 103 | VCR.use_cassette('publishes/interests/invalid_payload') do 104 | response = send_post 105 | 106 | expect(response.status).to eq(422) 107 | end 108 | end 109 | end 110 | 111 | context 'when publish to interests payload is valid' do 112 | it 'returns 200' do 113 | VCR.use_cassette('publishes/interests/valid_payload') do 114 | response = send_post 115 | 116 | expect(response.status).to eq(200) 117 | end 118 | end 119 | end 120 | end 121 | describe '#post_users' do 122 | subject(:send_post) { client.post(resource, body) } 123 | let(:resource) { 'publishes/users' } 124 | 125 | let(:instance_id) { ENV['PUSHER_INSTANCE_ID'] } 126 | let(:secret_key) { ENV['PUSHER_SECRET_KEY'] } 127 | let(:body) do 128 | { 129 | users: %w[ 130 | jonathan jordan luis luka mina 131 | ], 132 | apns: { 133 | aps: { 134 | alert: { 135 | title: 'Hello', 136 | body: 'Hello, world!' 137 | } 138 | } 139 | }, 140 | fcm: { 141 | notification: { 142 | title: 'Hello', 143 | body: 'Hello, world!' 144 | } 145 | } 146 | } 147 | end 148 | 149 | context 'when publish to users payload is invalid' do 150 | let(:body) do 151 | { 152 | invalid: 'payload' 153 | } 154 | end 155 | 156 | it 'return 422' do 157 | VCR.use_cassette('publishes/users/invalid_payload') do 158 | response = send_post 159 | 160 | expect(response.status).to eq(422) 161 | end 162 | end 163 | end 164 | 165 | context 'when publish to users payload is valid' do 166 | it 'returns 200' do 167 | VCR.use_cassette('publishes/users/valid_payload') do 168 | response = send_post 169 | 170 | expect(response.status).to eq(200) 171 | end 172 | end 173 | end 174 | end 175 | describe '#delete_user' do 176 | let(:user) { "Stop!' said Fred user" } 177 | let(:instance_id) { ENV['PUSHER_INSTANCE_ID'] } 178 | let(:secret_key) { ENV['PUSHER_SECRET_KEY'] } 179 | 180 | subject(:delete_user) { client.delete(user) } 181 | 182 | context 'when user id is empty' do 183 | let(:user) { '' } 184 | 185 | it 'return 404' do 186 | VCR.use_cassette('delete/user/id_empty') do 187 | response = delete_user 188 | 189 | expect(response.status).to eq(404) 190 | end 191 | end 192 | end 193 | 194 | context 'when user id is too long' do 195 | max_user_id_length = Pusher::PushNotifications::UserId::MAX_USER_ID_LENGTH 196 | let(:user) { 'a' * (max_user_id_length + 1) } 197 | 198 | it 'return 400' do 199 | VCR.use_cassette('delete/user/id_too_long') do 200 | response = delete_user 201 | 202 | expect(response.status).to eq(400) 203 | end 204 | end 205 | end 206 | 207 | context 'when user id is valid' do 208 | it 'returns 200' do 209 | VCR.use_cassette('delete/user') do 210 | response = delete_user 211 | 212 | expect(response.status).to eq(200) 213 | end 214 | end 215 | end 216 | end 217 | end 218 | --------------------------------------------------------------------------------