├── .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 | [](https://github.com/pusher/push-notifications-ruby/actions/workflows/test.yml) [](https://coveralls.io/github/pusher/push-notifications-ruby) [](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 |
--------------------------------------------------------------------------------