├── .gitignore
├── lib
├── pusher-fake
│ ├── support
│ │ ├── cucumber.rb
│ │ ├── rspec.rb
│ │ └── base.rb
│ ├── server
│ │ ├── chain_trap_handlers.rb
│ │ └── application.rb
│ ├── webhook.rb
│ ├── channel
│ │ ├── private.rb
│ │ ├── presence.rb
│ │ └── public.rb
│ ├── channel.rb
│ ├── server.rb
│ ├── configuration.rb
│ └── connection.rb
└── pusher-fake.rb
├── spec
├── support
│ ├── capybara.rb
│ ├── helpers
│ │ ├── event.rb
│ │ ├── user.rb
│ │ ├── connect.rb
│ │ └── subscription.rb
│ ├── pusher_fake.rb
│ ├── matchers
│ │ └── have_configuration_option.rb
│ ├── webhooks.rb
│ ├── application.rb
│ └── application
│ │ └── views
│ │ └── index.erb
├── features
│ ├── client
│ │ ├── connect_spec.rb
│ │ ├── event_spec.rb
│ │ ├── presence_spec.rb
│ │ └── subscribe_spec.rb
│ ├── api
│ │ ├── users_spec.rb
│ │ └── channels_spec.rb
│ └── server
│ │ ├── webhooks_spec.rb
│ │ └── event_spec.rb
├── spec_helper.rb
└── lib
│ ├── pusher-fake
│ ├── webhook_spec.rb
│ ├── channel_spec.rb
│ ├── configuration_spec.rb
│ ├── server_spec.rb
│ ├── channel
│ │ ├── private_spec.rb
│ │ ├── public_spec.rb
│ │ └── presence_spec.rb
│ ├── connection_spec.rb
│ └── server
│ │ └── application_spec.rb
│ └── pusher_fake_spec.rb
├── Rakefile
├── .github
└── workflows
│ └── ci.yml
├── Gemfile
├── LICENSE
├── pusher-fake.gemspec
├── .rubocop.yml
├── bin
└── pusher-fake
├── README.markdown
└── CHANGELOG.markdown
/.gitignore:
--------------------------------------------------------------------------------
1 | Gemfile.lock
2 | coverage/
3 | pkg/
4 |
--------------------------------------------------------------------------------
/lib/pusher-fake/support/cucumber.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "pusher-fake/support/base"
4 |
5 | # Reset channels between scenarios.
6 | After do
7 | PusherFake::Channel.reset
8 | end
9 |
--------------------------------------------------------------------------------
/spec/support/capybara.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "capybara/rspec"
4 |
5 | Capybara.app = PusherFake::Testing::Application.new
6 | Capybara.server = :puma, { Silent: true }
7 | Capybara.default_driver = :selenium_chrome_headless
8 |
--------------------------------------------------------------------------------
/lib/pusher-fake/support/rspec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "pusher-fake/support/base"
4 |
5 | # Reset channels between examples
6 | RSpec.configure do |config|
7 | config.after(:each) do
8 | PusherFake::Channel.reset
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/spec/features/client/connect_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | feature "Client connecting to the server" do
6 | scenario "successfully connects" do
7 | visit "/"
8 |
9 | expect(page).to have_content("Client connected.")
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/support/helpers/event.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module EventHelpers
4 | def have_event(event, options = {})
5 | have_css("li", text: "Channel #{options[:on]} received #{event} event.")
6 | end
7 | end
8 |
9 | RSpec.configure do |config|
10 | config.include(EventHelpers)
11 | end
12 |
--------------------------------------------------------------------------------
/spec/support/helpers/user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module UserHelpers
4 | def user_id(name = nil)
5 | using_session(name) do
6 | return page.evaluate_script("Pusher.instance.connection.socket_id")
7 | end
8 | end
9 | end
10 |
11 | RSpec.configure do |config|
12 | config.include(UserHelpers)
13 | end
14 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/setup"
4 | require "rspec/core/rake_task"
5 | require "yard"
6 |
7 | Bundler::GemHelper.install_tasks
8 |
9 | RSpec::Core::RakeTask.new do |t|
10 | t.pattern = "spec/**/*_spec.rb"
11 | end
12 |
13 | YARD::Rake::YardocTask.new do |t|
14 | t.files = ["lib/**/*.rb"]
15 | t.options = ["--no-private"]
16 | end
17 |
18 | task default: [:spec]
19 |
--------------------------------------------------------------------------------
/spec/support/helpers/connect.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ConnectHelpers
4 | def connect(options = {})
5 | visit "/"
6 |
7 | expect(page).to have_content("Client connected.")
8 |
9 | subscribe_to(options[:channel]) if options[:channel]
10 | end
11 |
12 | def connect_as(name, options = {})
13 | using_session(name) do
14 | connect(options)
15 | end
16 | end
17 | end
18 |
19 | RSpec.configure do |config|
20 | config.include(ConnectHelpers)
21 | end
22 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | matrix:
11 | ruby:
12 | - "3.2"
13 | - "3.3"
14 | - "3.4"
15 |
16 | name: Ruby ${{ matrix.ruby }}
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: ruby/setup-ruby@v1
20 | with:
21 | ruby-version: ${{ matrix.ruby }}
22 | bundler-cache: true
23 | - name: Run the tests
24 | run: xvfb-run bundle exec rake spec
25 | - name: Lint the code
26 | run: bundle exec rubocop
27 |
--------------------------------------------------------------------------------
/spec/support/pusher_fake.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "thin/server"
4 |
5 | RSpec.configure do |config|
6 | config.before(:each, type: :feature) do
7 | PusherFake.configuration.reset!
8 | PusherFake.configuration.web_options.tap do |options|
9 | Pusher.url = "http://PUSHER_API_KEY:PUSHER_API_SECRET@" \
10 | "#{options[:host]}:#{options[:port]}/apps/PUSHER_APP_ID"
11 | end
12 |
13 | @thread = Thread.new { PusherFake::Server.start }
14 | end
15 |
16 | config.after(:each, type: :feature) do
17 | @thread.exit
18 |
19 | PusherFake::Channel.reset
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/pusher-fake/server/chain_trap_handlers.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # :nocov:
4 | module PusherFake
5 | module Server
6 | # Monkeypatch to ensure previous trap handlers are called when new handlers
7 | # are added.
8 | #
9 | # @see +PusherFake::Server.chain_trap_handlers+
10 | module ChainTrapHandlers
11 | # Ensure a previous trap is chained when a new trap is added.
12 | #
13 | # @see +Signal.trap+
14 | def trap(*arguments)
15 | previous_trap = super do
16 | yield
17 |
18 | previous_trap&.call
19 | end
20 | end
21 | end
22 | end
23 | end
24 | # :nocov:
25 |
--------------------------------------------------------------------------------
/spec/support/matchers/have_configuration_option.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec::Matchers.define :have_configuration_option do |option|
4 | match do |configuration|
5 | configuration.respond_to?(option) &&
6 | configuration.respond_to?(:"#{option}=") &&
7 | (@default.nil? || configuration.public_send(option) == @default)
8 | end
9 |
10 | chain :with_default do |default|
11 | @default = default
12 | end
13 |
14 | failure_message do |_configuration|
15 | description = "expected configuration to have #{option.inspect} option"
16 | description << " with a default of #{@default.inspect}" unless @default.nil?
17 | description
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | gemspec
6 |
7 | gem "capybara", "3.40.0"
8 | gem "mutex_m", "0.3.0"
9 | gem "ostruct", "0.6.3"
10 | gem "puma", "7.1.0"
11 | gem "pusher", "2.0.3"
12 | gem "rake", "13.3.1"
13 | gem "rspec", "3.13.2"
14 | gem "rubocop", "1.81.7"
15 | gem "rubocop-capybara", "2.22.1"
16 | gem "rubocop-performance", "1.26.1"
17 | gem "rubocop-rake", "0.7.1"
18 | gem "rubocop-rspec", "3.8.0"
19 | gem "selenium-webdriver", "4.38.0"
20 | gem "yard", "0.9.37"
21 |
22 | group :test do
23 | gem "simplecov-console", "0.9.4", require: false
24 | end
25 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "bundler/setup"
4 |
5 | Bundler.require(:default, :development)
6 |
7 | if ENV["CI"] || ENV["COVERAGE"]
8 | require "simplecov"
9 | require "simplecov-console"
10 |
11 | SimpleCov.formatter = SimpleCov::Formatter::Console
12 | SimpleCov.start do
13 | add_filter("lib/pusher-fake/support")
14 | add_filter("spec/support")
15 | enable_coverage :branch
16 | minimum_coverage line: 100, branch: 100
17 | end
18 | end
19 |
20 | Dir[File.expand_path("support/**/*.rb", __dir__)].each do |file|
21 | require file
22 | end
23 |
24 | RSpec.configure do |config|
25 | config.expect_with :rspec do |rspec|
26 | rspec.syntax = :expect
27 | end
28 |
29 | # Raise errors for any deprecations.
30 | config.raise_errors_for_deprecations!
31 | end
32 |
--------------------------------------------------------------------------------
/spec/support/helpers/subscription.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SubscriptionHelpers
4 | def subscribe_to(channel)
5 | page.execute_script("Helpers.subscribe(#{MultiJson.dump(channel)})")
6 |
7 | expect(page).to have_content("Subscribed to #{channel}.")
8 | end
9 |
10 | def subscribe_to_as(channel, name)
11 | using_session(name) do
12 | subscribe_to(channel)
13 | end
14 | end
15 |
16 | def unsubscribe_from(channel)
17 | page.execute_script("Helpers.unsubscribe(#{MultiJson.dump(channel)})")
18 |
19 | expect(page).to have_content("Unsubscribed from #{channel}.")
20 | end
21 |
22 | def unsubscribe_from_as(channel, name)
23 | using_session(name) do
24 | unsubscribe_from(channel)
25 | end
26 | end
27 | end
28 |
29 | RSpec.configure do |config|
30 | config.include(SubscriptionHelpers)
31 | end
32 |
--------------------------------------------------------------------------------
/spec/features/api/users_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | feature "Requesting user API endpoint" do
6 | let(:users) { Pusher.get("/channels/#{channel_name}/users")[:users] }
7 | let(:channel_name) { "public-1" }
8 |
9 | before do
10 | connect
11 | connect_as "Bob"
12 | end
13 |
14 | scenario "with no users subscribed" do
15 | expect(users).to be_empty
16 | end
17 |
18 | scenario "with a single user subscribed" do
19 | subscribe_to(channel_name)
20 |
21 | expect(users.size).to eq(1)
22 | end
23 |
24 | scenario "with a multiple users subscribed" do
25 | subscribe_to(channel_name)
26 | subscribe_to_as(channel_name, "Bob")
27 |
28 | ids = PusherFake::Channel.channels[channel_name].connections.map(&:id)
29 |
30 | expect(users.size).to eq(2)
31 |
32 | users.each do |user|
33 | expect(ids).to include(user["id"])
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/pusher-fake/support/base.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | %w(app_id key secret).each do |setting|
4 | next unless Pusher.public_send(setting).nil?
5 |
6 | warn("Warning: Pusher.#{setting} is not set. " \
7 | "Should be set before including PusherFake")
8 | end
9 |
10 | unless defined?(PusherFake)
11 | warn("Warning: PusherFake is not defined. " \
12 | "Should be required before requiring a support file.")
13 | end
14 |
15 | # Use the same API key and secret as the live version.
16 | PusherFake.configure do |configuration|
17 | configuration.app_id = Pusher.app_id
18 | configuration.key = Pusher.key
19 | configuration.secret = Pusher.secret
20 | end
21 |
22 | # Set the host and port to the fake web server.
23 | PusherFake.configuration.web_options.tap do |options|
24 | Pusher.host = options[:host]
25 | Pusher.port = options[:port]
26 | end
27 |
28 | # Start the fake socket and web servers.
29 | Thread.new { PusherFake::Server.start }.tap do |thread|
30 | at_exit { Thread.kill(thread) }
31 | end
32 |
--------------------------------------------------------------------------------
/lib/pusher-fake/webhook.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PusherFake
4 | # Webhook triggering.
5 | class Webhook
6 | class << self
7 | def trigger(name, data = {})
8 | payload = MultiJson.dump(
9 | events: [data.merge(name: name)],
10 | time_ms: Time.now.to_i
11 | )
12 |
13 | PusherFake.log("HOOK: #{payload}")
14 | PusherFake.configuration.webhooks.each do |url|
15 | http = EventMachine::HttpRequest.new(url)
16 | http.post(body: payload, head: headers_for(payload))
17 | end
18 | end
19 |
20 | private
21 |
22 | def headers_for(payload)
23 | {
24 | "Content-Type" => "application/json",
25 | "X-Pusher-Key" => PusherFake.configuration.key,
26 | "X-Pusher-Signature" => signature_for(payload)
27 | }
28 | end
29 |
30 | def signature_for(payload)
31 | digest = OpenSSL::Digest.new("SHA256")
32 | secret = PusherFake.configuration.secret
33 |
34 | OpenSSL::HMAC.hexdigest(digest, secret, payload)
35 | end
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2011-2017 Tristan Dunn
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 |
--------------------------------------------------------------------------------
/pusher-fake.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Gem::Specification.new do |s|
4 | s.name = "pusher-fake"
5 | s.version = "6.0.0"
6 | s.platform = Gem::Platform::RUBY
7 | s.authors = ["Tristan Dunn"]
8 | s.email = "hello@tristandunn.com"
9 | s.homepage = "https://github.com/tristandunn/pusher-fake"
10 | s.summary = "A fake Pusher server for development and testing."
11 | s.description = "A fake Pusher server for development and testing."
12 | s.license = "MIT"
13 | s.metadata = {
14 | "bug_tracker_uri" => "https://github.com/tristandunn/pusher-fake/issues",
15 | "changelog_uri" => "https://github.com/tristandunn/pusher-fake/blob/main/CHANGELOG.markdown",
16 | "rubygems_mfa_required" => "true"
17 | }
18 |
19 | s.files = Dir["lib/**/*"].to_a
20 | s.executables << "pusher-fake"
21 | s.require_path = "lib"
22 |
23 | s.required_ruby_version = ">= 3.2"
24 |
25 | s.add_dependency "em-http-request", "~> 1.1"
26 | s.add_dependency "em-websocket", "~> 0.5"
27 | s.add_dependency "multi_json", "~> 1.6"
28 | s.add_dependency "mutex_m", "~> 0.3.0"
29 | s.add_dependency "thin", ">= 1.8", "< 2"
30 | end
31 |
--------------------------------------------------------------------------------
/spec/support/webhooks.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class WebhookHelper
4 | def self.events
5 | @events ||= []
6 | end
7 |
8 | def self.mutex
9 | @mutex ||= Mutex.new
10 | end
11 | end
12 |
13 | class WebhookEndpoint
14 | def self.call(environment)
15 | request = Rack::Request.new(environment)
16 | webhook = Pusher::WebHook.new(request)
17 |
18 | if webhook.valid?
19 | WebhookHelper.mutex.synchronize do
20 | WebhookHelper.events.concat(webhook.events)
21 | end
22 | end
23 |
24 | Rack::Response.new.finish
25 | end
26 | end
27 |
28 | RSpec.configure do |config|
29 | config.before(:suite) do
30 | EventMachine::WebSocket.singleton_class.prepend(PusherFake::Server::ChainTrapHandlers)
31 | Thin::Server.prepend(PusherFake::Server::ChainTrapHandlers)
32 |
33 | thread = Thread.new do
34 | # Not explicitly requiring Thin::Server occasionally results in
35 | # Thin::Server.start not being defined.
36 | require "thin"
37 | require "thin/server"
38 |
39 | EventMachine.run do
40 | Thin::Logging.silent = true
41 | Thin::Server.start("0.0.0.0", 8082, WebhookEndpoint)
42 | end
43 | end
44 |
45 | at_exit { thread.exit }
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/spec/features/client/event_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | feature "Client triggers a client event" do
6 | let(:event) { "client-message" }
7 | let(:other_user) { "Bob" }
8 | let(:public_channel) { "chat" }
9 | let(:private_channel) { "private-chat" }
10 |
11 | before do
12 | connect
13 | connect_as(other_user)
14 | end
15 |
16 | scenario "on a subscribed private channel" do
17 | subscribe_to(private_channel)
18 | subscribe_to_as(private_channel, other_user)
19 |
20 | trigger(private_channel, event)
21 |
22 | expect(page).not_to have_event(event, on: private_channel)
23 |
24 | using_session(other_user) do
25 | expect(page).to have_event(event, on: private_channel)
26 | end
27 | end
28 |
29 | scenario "on a subscribed public channel" do
30 | subscribe_to(public_channel)
31 | subscribe_to_as(public_channel, other_user)
32 |
33 | trigger(public_channel, event)
34 |
35 | expect(page).not_to have_event(event, on: public_channel)
36 |
37 | using_session(other_user) do
38 | expect(page).not_to have_event(event, on: public_channel)
39 | end
40 | end
41 |
42 | protected
43 |
44 | def trigger(channel, event)
45 | page.execute_script(
46 | "Helpers.trigger(#{MultiJson.dump(channel)}, #{MultiJson.dump(event)})"
47 | )
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/pusher-fake.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "em-http-request"
4 | require "em-websocket"
5 | require "multi_json"
6 | require "openssl"
7 | require "thin"
8 |
9 | # A Pusher fake.
10 | module PusherFake
11 | # The current version string.
12 | VERSION = "6.0.0"
13 |
14 | autoload :Channel, "pusher-fake/channel"
15 | autoload :Configuration, "pusher-fake/configuration"
16 | autoload :Connection, "pusher-fake/connection"
17 | autoload :Server, "pusher-fake/server"
18 | autoload :Webhook, "pusher-fake/webhook"
19 |
20 | # Call this method to modify the defaults.
21 | #
22 | # @example
23 | # PusherFake.configure do |configuration|
24 | # configuration.port = 443
25 | # end
26 | #
27 | # @yield [Configuration] The current configuration.
28 | def self.configure
29 | yield configuration
30 | end
31 |
32 | # @return [Configuration] Current configuration.
33 | def self.configuration
34 | @configuration ||= Configuration.new
35 | end
36 |
37 | # Convenience method for the JS to override the Pusher client host and port.
38 | #
39 | # @param [Hash] options Custom options for Pusher client.
40 | # @return [String] JavaScript overriding the Pusher client host and port.
41 | def self.javascript(options = {})
42 | arguments = [
43 | configuration.key,
44 | configuration.to_options(options)
45 | ].map(&:to_json).join(",")
46 |
47 | "new Pusher(#{arguments})"
48 | end
49 |
50 | def self.log(message)
51 | configuration.logger << "#{message}\n" if configuration.verbose
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | - rubocop-capybara
3 | - rubocop-performance
4 | - rubocop-rake
5 | - rubocop-rspec
6 |
7 | AllCops:
8 | NewCops: enable
9 | TargetRubyVersion: 3.2
10 |
11 | Layout/HashAlignment:
12 | EnforcedColonStyle: table
13 | EnforcedHashRocketStyle: table
14 |
15 | Layout/LineLength:
16 | Exclude:
17 | - bin/pusher-fake
18 |
19 | Metrics/BlockLength:
20 | Exclude:
21 | - pusher-fake.gemspec
22 | - bin/pusher-fake
23 | - spec/**/*_spec.rb
24 |
25 | Naming/FileName:
26 | Exclude:
27 | - lib/pusher-fake.rb
28 |
29 | Naming/RescuedExceptionsVariableName:
30 | PreferredName: error
31 |
32 | Naming/VariableNumber:
33 | EnforcedStyle: snake_case
34 |
35 | RSpec/ExampleLength:
36 | Max: 10
37 |
38 | RSpec/IndexedLet:
39 | Max: 2
40 |
41 | RSpec/MultipleDescribes:
42 | Enabled: false
43 |
44 | RSpec/MultipleExpectations:
45 | Max: 4
46 |
47 | RSpec/MultipleMemoizedHelpers:
48 | Enabled: false
49 |
50 | RSpec/NamedSubject:
51 | Enabled: false
52 |
53 | RSpec/SpecFilePathFormat:
54 | Enabled: false
55 |
56 | RSpec/SubjectStub:
57 | Enabled: false
58 |
59 | Style/HashEachMethods:
60 | Enabled: true
61 |
62 | Style/HashSyntax:
63 | EnforcedShorthandSyntax: never
64 |
65 | Style/HashTransformKeys:
66 | Enabled: true
67 |
68 | Style/HashTransformValues:
69 | Enabled: true
70 |
71 | Style/IfUnlessModifier:
72 | Enabled: false
73 |
74 | Style/PercentLiteralDelimiters:
75 | PreferredDelimiters:
76 | "%w": "()"
77 |
78 | Style/RaiseArgs:
79 | EnforcedStyle: compact
80 |
81 | Style/StringLiterals:
82 | EnforcedStyle: double_quotes
83 |
--------------------------------------------------------------------------------
/spec/features/client/presence_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | feature "Client on a presence channel" do
6 | let(:other_user) { "Bob" }
7 |
8 | before do
9 | connect
10 | end
11 |
12 | scenario "subscribing to a presence channel" do
13 | subscribe_to("presence-game-1")
14 |
15 | expect(page).to have_clients(1, in: "presence-game-1")
16 | end
17 |
18 | scenario "subscribing to a presence channel, with existing users" do
19 | connect_as(other_user, channel: "presence-game-1")
20 |
21 | subscribe_to("presence-game-1")
22 |
23 | expect(page).to have_clients(2, in: "presence-game-1", named: "Alan Turing")
24 | end
25 |
26 | scenario "member entering notification" do
27 | subscribe_to("presence-game-1")
28 |
29 | connect_as(other_user, channel: "presence-game-1")
30 |
31 | expect(page).to have_clients(2, in: "presence-game-1")
32 | end
33 |
34 | scenario "member leaving notification" do
35 | connect_as(other_user, channel: "presence-game-1")
36 | subscribe_to("presence-game-1")
37 |
38 | expect(page).to have_clients(2, in: "presence-game-1")
39 |
40 | unsubscribe_from_as("presence-game-1", other_user)
41 |
42 | expect(page).to have_clients(1, in: "presence-game-1")
43 | end
44 |
45 | scenario "other client connecting" do
46 | subscribe_to("presence-game-1")
47 |
48 | connect_as(other_user)
49 |
50 | expect(page).to have_clients(1, in: "presence-game-1")
51 | end
52 |
53 | protected
54 |
55 | def have_clients(count, options = {})
56 | have_css("li.channel-#{options[:in]}", count: count, text: options[:named])
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/spec/support/application.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rack"
4 |
5 | module PusherFake
6 | module Testing
7 | class Application
8 | def call(env)
9 | request = Rack::Request.new(env)
10 |
11 | case request.path
12 | when "/" then index
13 | when "/pusher/auth" then authenticate(request.params)
14 | when %r{\A/javascripts} then asset(request.path)
15 | else
16 | [404, {}, []]
17 | end
18 | end
19 |
20 | private
21 |
22 | def asset(path)
23 | headers = { "content-type" => "text/javascript" }
24 | root = File.join(File.dirname(__FILE__), "application")
25 | body = File.read(File.join(root, "public", path))
26 |
27 | [200, headers, [body]]
28 | end
29 |
30 | def authenticate(params)
31 | channel = Pusher[params["channel_name"]]
32 | response = channel.authenticate(params["socket_id"], channel_data(params))
33 | headers = { "Content-Type" => "application/json" }
34 |
35 | [200, headers, [MultiJson.dump(response)]]
36 | end
37 |
38 | def channel_data(params)
39 | return unless /^presence-/.match?(params["channel_name"])
40 |
41 | {
42 | user_id: params["socket_id"],
43 | user_info: { name: "Alan Turing" }
44 | }
45 | end
46 |
47 | def index
48 | headers = { "content-type" => "text/html" }
49 | root = File.join(File.dirname(__FILE__), "application")
50 | template = File.read(File.join(root, "views", "index.erb"))
51 | erb = ERB.new(template)
52 | body = erb.result(binding)
53 |
54 | [200, headers, [body]]
55 | end
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/pusher-fake/channel/private.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PusherFake
4 | module Channel
5 | # A private channel.
6 | class Private < Public
7 | # Add the connection to the channel if they are authorized.
8 | #
9 | # @param [Connection] connection The connection to add.
10 | # @param [Hash] options The options for the channel.
11 | # @option options [String] :auth The authentication string.
12 | # @option options [Hash] :channel_data Information for subscribed client.
13 | def add(connection, options = {})
14 | if authorized?(connection, options)
15 | subscription_succeeded(connection, options)
16 | else
17 | connection.emit("pusher_internal:subscription_error", {}, name)
18 | end
19 | end
20 |
21 | # Determine if the connection is authorized for the channel.
22 | #
23 | # @param [Connection] connection The connection to authorize.
24 | # @param [Hash] options
25 | # @option options [String] :auth The authentication string.
26 | # @return [Boolean] +true+ if authorized, +false+ otherwise.
27 | def authorized?(connection, options)
28 | authentication_for(connection.id, options[:channel_data]) ==
29 | options[:auth]
30 | end
31 |
32 | # Generate an authentication string from the channel based on the
33 | # connection ID provided.
34 | #
35 | # @private
36 | # @param [String] id The connection ID.
37 | # @param [String] data Custom channel data.
38 | # @return [String] The authentication string.
39 | def authentication_for(id, data = nil)
40 | configuration = PusherFake.configuration
41 |
42 | data = [id, name, data].compact.map(&:to_s).join(":")
43 | digest = OpenSSL::Digest.new("SHA256")
44 | signature = OpenSSL::HMAC.hexdigest(digest, configuration.secret, data)
45 |
46 | "#{configuration.key}:#{signature}"
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/pusher-fake/channel/presence.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PusherFake
4 | module Channel
5 | # A presence channel.
6 | class Presence < Private
7 | # @return [Hash] Channel members hash.
8 | attr_reader :members
9 |
10 | # Create a new {Presence} object.
11 | #
12 | # @param [String] name The channel name.
13 | def initialize(name)
14 | super
15 |
16 | @members = {}
17 | end
18 |
19 | # Remove the +connection+ from the channel and notify the channel.
20 | #
21 | # Also trigger the member_removed webhook.
22 | #
23 | # @param [Connection] connection The connection to remove.
24 | def remove(connection)
25 | super
26 |
27 | return unless members.key?(connection)
28 |
29 | trigger("member_removed",
30 | channel: name, user_id: members[connection][:user_id])
31 |
32 | emit("pusher_internal:member_removed", members.delete(connection))
33 | end
34 |
35 | # Return a hash containing presence information for the channel.
36 | #
37 | # @return [Hash] Hash containing presence information.
38 | def subscription_data
39 | hash = members.to_h { |_, member| [member[:user_id], member[:user_info]] }
40 |
41 | { presence: { hash: hash, count: members.size } }
42 | end
43 |
44 | private
45 |
46 | # Store the member data for the connection and notify the channel a
47 | # member was added.
48 | #
49 | # Also trigger the member_added webhook.
50 | #
51 | # @param [Connection] connection Connection a subscription succeeded for.
52 | # @param [Hash] options The options for the channel.
53 | def subscription_succeeded(connection, options = {})
54 | member = members[connection] = MultiJson.load(
55 | options[:channel_data], symbolize_keys: true
56 | )
57 |
58 | emit("pusher_internal:member_added", member)
59 |
60 | trigger("member_added", channel: name, user_id: member[:user_id])
61 |
62 | super
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/pusher-fake/channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PusherFake
4 | # Channel creation and management.
5 | module Channel
6 | autoload :Public, "pusher-fake/channel/public"
7 | autoload :Private, "pusher-fake/channel/private"
8 | autoload :Presence, "pusher-fake/channel/presence"
9 |
10 | # Prefix for private channels.
11 | PRIVATE_CHANNEL_PREFIX = "private-"
12 |
13 | # Prefix for presence channels.
14 | PRESENCE_CHANNEL_PREFIX = "presence-"
15 |
16 | class << self
17 | # @return [Hash] Cache of existing channels.
18 | attr_writer :channels
19 |
20 | # @return [Hash] Cache of existing channels.
21 | def channels
22 | @channels ||= {}
23 | end
24 |
25 | # Create a channel, determining the type by the name.
26 | #
27 | # @param [String] name The channel name.
28 | # @return [Public|Private] The channel object.
29 | def factory(name)
30 | self.channels ||= {}
31 | self.channels[name] ||= class_for(name).new(name)
32 | end
33 |
34 | # Remove a connection from all channels.
35 | #
36 | # Also deletes the channel if it is empty.
37 | #
38 | # @param [Connection] connection The connection to remove.
39 | def remove(connection)
40 | return if channels.nil?
41 |
42 | channels.each do |name, channel|
43 | channel.remove(connection)
44 |
45 | if channels[name].connections.empty?
46 | channels.delete(name)
47 | end
48 | end
49 | end
50 |
51 | # Reset the channel cache.
52 | def reset
53 | self.channels = {}
54 | end
55 |
56 | private
57 |
58 | # Determine the channel class to use based on the channel name.
59 | #
60 | # @param [String] name The name of the channel.
61 | # @return [Class] The class to use for the channel.
62 | def class_for(name)
63 | if name.start_with?(PRIVATE_CHANNEL_PREFIX)
64 | Private
65 | elsif name.start_with?(PRESENCE_CHANNEL_PREFIX)
66 | Presence
67 | else
68 | Public
69 | end
70 | end
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/pusher-fake/server.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PusherFake
4 | # Socket and web server manager.
5 | module Server
6 | autoload :Application, "pusher-fake/server/application"
7 | autoload :ChainTrapHandlers, "pusher-fake/server/chain_trap_handlers"
8 |
9 | class << self
10 | # Start the servers.
11 | #
12 | # @see start_socket_server
13 | # @see start_web_server
14 | def start
15 | chain_trap_handlers
16 |
17 | EventMachine.run do
18 | start_web_server
19 | start_socket_server
20 | end
21 | end
22 |
23 | # Start the WebSocket server.
24 | def start_socket_server
25 | EventMachine::WebSocket.start(configuration.socket_options) do |socket|
26 | socket.onopen do
27 | connection = Connection.new(socket)
28 | connection.establish
29 |
30 | socket.onmessage { |data| connection.process(data) }
31 | socket.onclose { Channel.remove(connection) }
32 | end
33 | end
34 | end
35 |
36 | # Start the web server.
37 | def start_web_server
38 | options = configuration.web_options.dup
39 | host = options.delete(:host)
40 | port = options.delete(:port)
41 |
42 | Thin::Logging.silent = true
43 | Thin::Server.new(host, port, Application).tap do |server|
44 | options.each do |key, value|
45 | server.__send__(:"#{key}=", value)
46 | end
47 |
48 | server.start!
49 | end
50 | end
51 |
52 | private
53 |
54 | # Force +Thin::Server+ and +EventMachine::WebSocket+ to call the chain of
55 | # trap handlers to ensure other handles, such as +RSpec+, can interrupt.
56 | def chain_trap_handlers
57 | EventMachine::WebSocket.singleton_class.prepend(ChainTrapHandlers)
58 | Thin::Server.prepend(ChainTrapHandlers)
59 | end
60 |
61 | # Convenience method for access the configuration object.
62 | #
63 | # @return [Configuration] The configuration object.
64 | def configuration
65 | PusherFake.configuration
66 | end
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/spec/lib/pusher-fake/webhook_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe PusherFake::Webhook, ".trigger" do
6 | subject { described_class }
7 |
8 | let(:data) { { channel: "name" } }
9 | let(:http) { instance_double(EventMachine::HttpConnection, post: true) }
10 | let(:name) { "channel_occupied" }
11 | let(:digest) { instance_double(OpenSSL::Digest::SHA256) }
12 | let(:webhooks) { ["url"] }
13 | let(:signature) { "signature" }
14 |
15 | let(:configuration) do
16 | instance_double(PusherFake::Configuration,
17 | key: "key",
18 | secret: "secret",
19 | webhooks: webhooks)
20 | end
21 |
22 | let(:headers) do
23 | {
24 | "Content-Type" => "application/json",
25 | "X-Pusher-Key" => configuration.key,
26 | "X-Pusher-Signature" => signature
27 | }
28 | end
29 |
30 | let(:payload) do
31 | MultiJson.dump(events: [data.merge(name: name)], time_ms: Time.now.to_i)
32 | end
33 |
34 | before do
35 | allow(OpenSSL::HMAC).to receive(:hexdigest).and_return(signature)
36 | allow(OpenSSL::Digest).to receive(:new).with("SHA256").and_return(digest)
37 | allow(EventMachine::HttpRequest).to receive(:new).and_return(http)
38 | allow(PusherFake).to receive(:log)
39 | allow(PusherFake).to receive(:configuration).and_return(configuration)
40 | end
41 |
42 | it "generates a signature" do
43 | subject.trigger(name, data)
44 |
45 | expect(OpenSSL::HMAC).to have_received(:hexdigest)
46 | .with(digest, configuration.secret, payload)
47 | end
48 |
49 | it "creates a HTTP request for each webhook URL" do
50 | subject.trigger(name, data)
51 |
52 | expect(EventMachine::HttpRequest).to have_received(:new)
53 | .with(webhooks.first)
54 | end
55 |
56 | it "posts the payload to the webhook URL" do
57 | subject.trigger(name, data)
58 |
59 | expect(http).to have_received(:post).with(body: payload, head: headers)
60 | end
61 |
62 | it "logs sending the hook" do
63 | subject.trigger(name, data)
64 |
65 | expect(PusherFake).to have_received(:log).with("HOOK: #{payload}")
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/bin/pusher-fake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # frozen_string_literal: true
4 |
5 | require "optparse"
6 | require "pusher"
7 | require "pusher-fake"
8 |
9 | PusherFake.configure do |configuration|
10 | OptionParser.new do |options|
11 | options.on("-iID", "--id ID", String, "Use ID as the application ID for Pusher") do |application_id|
12 | Pusher.app_id = application_id
13 | configuration.app_id = application_id
14 | end
15 |
16 | options.on("-kKEY", "--key KEY", String, "Use KEY as the key for Pusher") do |key|
17 | Pusher.key = key
18 | configuration.key = key
19 | end
20 |
21 | options.on("-sSECRET", "--secret SECRET", String, "Use SECRET as the secret token for Pusher") do |secret|
22 | Pusher.secret = secret
23 | configuration.secret = secret
24 | end
25 |
26 | options.on("--socket-host HOST", String, "Use HOST for the web socket server") do |host|
27 | configuration.socket_options[:host] = host
28 | end
29 |
30 | options.on("--socket-port PORT", Integer, "Use PORT for the web socket server") do |port|
31 | configuration.socket_options[:port] = port
32 | end
33 |
34 | options.on("-v", "--[no-]verbose", "Run verbosely") do |verbose|
35 | configuration.verbose = verbose
36 | end
37 |
38 | options.on("--web-host HOST", String, "Use HOST for the web server") do |host|
39 | configuration.web_options[:host] = host
40 | end
41 |
42 | options.on("--web-port PORT", Integer, "Use PORT for the web server") do |port|
43 | configuration.web_options[:port] = port
44 | end
45 |
46 | options.on("--webhooks URLS", Array, "Use URLS for the webhooks") do |hooks|
47 | configuration.webhooks = hooks
48 | end
49 | end.parse!
50 |
51 | # Optionally enable TLS for the em-websocket server.
52 | # configuration.socket_options = {
53 | # secure: true,
54 | # tls_options: { }
55 | # }
56 | #
57 | # Optionally enable SSL for the Thin web server.
58 | # configuration.web_options = {
59 | # ssl: true,
60 | # ssl_options: { }
61 | # }
62 | end
63 |
64 | raise OptionParser::MissingArgument.new("--id") if Pusher.app_id.nil?
65 | raise OptionParser::MissingArgument.new("--key") if Pusher.key.nil?
66 | raise OptionParser::MissingArgument.new("--secret") if Pusher.secret.nil?
67 |
68 | PusherFake::Server.start
69 |
--------------------------------------------------------------------------------
/spec/features/api/channels_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | feature "Requesting channel API endpoint" do
6 | let(:channel_name) { "public-1" }
7 | let(:presence_name) { "presence-example" }
8 |
9 | before do
10 | connect
11 | end
12 |
13 | scenario "all channels, with none created" do
14 | expect(channels).to be_empty
15 | end
16 |
17 | scenario "all channels, with one created" do
18 | subscribe_to(channel_name)
19 |
20 | expect(channels).to have_key(channel_name)
21 | end
22 |
23 | scenario "all channels, with a filter" do
24 | subscribe_to("other")
25 | subscribe_to(channel_name)
26 |
27 | result = channels(filter_by_prefix: "pu")
28 |
29 | expect(result.size).to eq(1)
30 | expect(result).to have_key(channel_name)
31 | end
32 |
33 | scenario "all channels, with info attributes" do
34 | subscribe_to(presence_name)
35 |
36 | result = channels(filter_by_prefix: "presence-", info: "user_count")
37 |
38 | expect(result.size).to eq(1)
39 | expect(result).to have_key(presence_name)
40 | expect(result[presence_name]).to have_key("user_count")
41 | expect(result[presence_name]["user_count"]).to eq(1)
42 | end
43 |
44 | scenario "all channels, with invalid info attributes" do
45 | expect do
46 | channels(info: "user_count")
47 | end.to raise_error(/user_count may only be requested for presence channels/)
48 | end
49 |
50 | scenario "channel, with no occupants" do
51 | expect(channel[:occupied]).to be(false)
52 | end
53 |
54 | scenario "channel, with an occupant" do
55 | subscribe_to(channel_name)
56 |
57 | expect(channel[:occupied]).to be(true)
58 | end
59 |
60 | scenario "channel, with info attributes" do
61 | subscribe_to(presence_name)
62 |
63 | result = Pusher.get("/channels/#{presence_name}", info: "user_count")
64 |
65 | expect(result[:occupied]).to be(true)
66 | expect(result[:user_count]).to eq(1)
67 | end
68 |
69 | scenario "channel, with invalid info attributes" do
70 | expect do
71 | channel(info: "user_count")
72 | end.to raise_error(
73 | /Cannot retrieve the user count unless the channel is a presence channel/
74 | )
75 | end
76 |
77 | protected
78 |
79 | def channel(options = {})
80 | Pusher.get("/channels/#{channel_name}", options)
81 | end
82 |
83 | def channels(options = {})
84 | Pusher.get("/channels", options)[:channels]
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/spec/features/client/subscribe_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | feature "Client subscribing to a channel" do
6 | let(:cache_event) { "command" }
7 | let(:cache_channel) { "cache-last-command" }
8 | let(:other_user) { "Bob" }
9 |
10 | before do
11 | connect
12 | end
13 |
14 | scenario "successfully subscribes to a channel" do
15 | subscribe_to("chat-message")
16 |
17 | expect(page).to have_content("Subscribed to chat-message.")
18 | end
19 |
20 | scenario "successfully subscribes to multiple channel" do
21 | subscribe_to("chat-enter")
22 | subscribe_to("chat-exit")
23 |
24 | expect(page).to have_content("Subscribed to chat-enter.")
25 | expect(page).to have_content("Subscribed to chat-exit.")
26 | end
27 |
28 | scenario "successfully subscribes to a private channel" do
29 | subscribe_to("private-message-bob")
30 |
31 | expect(page).to have_content("Subscribed to private-message-bob.")
32 | end
33 |
34 | scenario "successfully subscribes to a presence channel" do
35 | subscribe_to("presence-game-1")
36 |
37 | expect(page).to have_content("Subscribed to presence-game-1.")
38 | end
39 |
40 | scenario "unsuccessfully subscribes to a private channel" do
41 | override_socket_id("13.37")
42 |
43 | attempt_to_subscribe_to("private-message-bob")
44 |
45 | expect(page).to have_no_content("Subscribed to private-message-bob.")
46 | end
47 |
48 | scenario "unsuccessfully subscribes to a presence channel" do
49 | override_socket_id("13.37")
50 |
51 | attempt_to_subscribe_to("presence-game-1")
52 |
53 | expect(page).to have_no_content("Subscribed to presence-game-1.")
54 | end
55 |
56 | scenario "successfully subscribes to a cache channel, with no cached event" do
57 | subscribe_to(cache_channel)
58 |
59 | expect(page).to have_content("No cached event for cache-last-command.")
60 | end
61 |
62 | scenario "successfully subscribes to a cache channel, with cached event" do
63 | subscribe_to(cache_channel)
64 | Pusher.trigger(cache_channel, cache_event, {}, {})
65 |
66 | connect_as(other_user, channel: cache_channel)
67 |
68 | using_session(other_user) do
69 | expect(page).to have_content("Channel #{cache_channel} received #{cache_event} event.")
70 | end
71 | end
72 |
73 | protected
74 |
75 | def attempt_to_subscribe_to(channel)
76 | page.execute_script("Helpers.subscribe(#{MultiJson.dump(channel)})")
77 | end
78 |
79 | def override_socket_id(value)
80 | page.execute_script(
81 | "Pusher.instance.connection.socket_id = #{MultiJson.dump(value)};"
82 | )
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/spec/support/application/views/index.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | PusherFake Test Application
5 |
6 |
7 |
8 |
9 |
10 | Client disconnected.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/lib/pusher-fake/configuration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PusherFake
4 | # Configuration class.
5 | class Configuration
6 | # @return [String] The Pusher Applicaiton ID. (Defaults to +PUSHER_APP_ID+.)
7 | attr_reader :app_id
8 |
9 | # @return [Boolean] Disable the client statistics. (Defaults to +true+.)
10 | attr_accessor :disable_stats
11 |
12 | # @return [String] The Pusher API key. (Defaults to +PUSHER_API_KEY+.)
13 | attr_accessor :key
14 |
15 | # @return [IO] An IO instance for verbose logging.
16 | attr_accessor :logger
17 |
18 | # @return [String] The Pusher API token. (Defaults to +PUSHER_API_SECRET+.)
19 | attr_accessor :secret
20 |
21 | # Options for the socket server. See +EventMachine::WebSocket.start+.
22 | #
23 | # @return [Hash] Options for the socket server.
24 | attr_accessor :socket_options
25 |
26 | # @return [Boolean] Enable verbose logging.
27 | attr_accessor :verbose
28 |
29 | # Options for the web server. See +Thin::Server+ for options.
30 | #
31 | # @return [Hash] Options for the web server.
32 | attr_accessor :web_options
33 |
34 | # @return [Array] An array of webhook URLs. (Defaults to +[]+.)
35 | attr_accessor :webhooks
36 |
37 | # Instantiated from {PusherFake.configuration}. Sets the defaults.
38 | def initialize
39 | reset!
40 | end
41 |
42 | # Assign the application ID, ensuring it's a string.
43 | #
44 | # @params [Integer|String] id The application ID.
45 | def app_id=(id)
46 | @app_id = id.to_s
47 | end
48 |
49 | def reset!
50 | self.app_id = "PUSHER_APP_ID"
51 | self.key = "PUSHER_API_KEY"
52 | self.logger = standard_out_io
53 | self.secret = "PUSHER_API_SECRET"
54 | self.verbose = false
55 | self.webhooks = []
56 |
57 | self.disable_stats = true
58 | self.socket_options = { host: "127.0.0.1", port: available_port }
59 | self.web_options = { host: "127.0.0.1", port: available_port }
60 | end
61 |
62 | # Convert the configuration to a hash sutiable for Pusher JS options.
63 | #
64 | # @param [Hash] options Custom options for Pusher client.
65 | def to_options(options = {})
66 | options.merge(
67 | wsHost: socket_options[:host],
68 | wsPort: socket_options[:port],
69 | cluster: "us-east-1",
70 | forceTLS: false,
71 | disableStats: disable_stats
72 | )
73 | end
74 |
75 | private
76 |
77 | def available_port
78 | socket = Socket.new(:INET, :STREAM, 0)
79 | socket.bind(Addrinfo.tcp("127.0.0.1", 0))
80 | socket.local_address.ip_port.tap do
81 | socket.close
82 | end
83 | end
84 |
85 | def standard_out_io
86 | if $stdout.respond_to?(:to_io)
87 | $stdout.to_io
88 | else
89 | $stdout
90 | end
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/spec/lib/pusher_fake_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe PusherFake, ".configure" do
6 | subject { described_class }
7 |
8 | it "yields the configuration" do
9 | expect do |block|
10 | subject.configure(&block)
11 | end.to yield_with_args(subject.configuration)
12 | end
13 | end
14 |
15 | describe PusherFake, ".configuration" do
16 | subject { described_class }
17 |
18 | let(:configuration) { double }
19 |
20 | before do
21 | described_class.instance_variable_set(:@configuration, nil)
22 |
23 | allow(PusherFake::Configuration).to receive(:new).and_return(configuration)
24 | end
25 |
26 | after do
27 | described_class.instance_variable_set(:@configuration, nil)
28 | end
29 |
30 | it "initializes a configuration object" do
31 | subject.configuration
32 |
33 | expect(PusherFake::Configuration).to have_received(:new)
34 | end
35 |
36 | it "memoizes the configuration" do
37 | 2.times { subject.configuration }
38 |
39 | expect(PusherFake::Configuration).to have_received(:new).once
40 | end
41 |
42 | it "returns the configuration" do
43 | expect(subject.configuration).to eq(configuration)
44 | end
45 | end
46 |
47 | describe PusherFake, ".javascript" do
48 | subject { described_class }
49 |
50 | let(:configuration) { subject.configuration }
51 |
52 | it "returns JavaScript setting the host and port to the configured options" do
53 | javascript = subject.javascript
54 | arguments = [configuration.key, configuration.to_options]
55 | .map(&:to_json).join(",")
56 |
57 | expect(javascript).to eq("new Pusher(#{arguments})")
58 | end
59 |
60 | it "supports passing custom options" do
61 | options = { custom: "option" }
62 | javascript = subject.javascript(options)
63 | arguments = [configuration.key, configuration.to_options(options)]
64 | .map(&:to_json).join(",")
65 |
66 | expect(javascript).to eq("new Pusher(#{arguments})")
67 | end
68 | end
69 |
70 | describe PusherFake, ".log" do
71 | subject { described_class }
72 |
73 | let(:logger) { instance_double(Logger, :<< => "") }
74 | let(:message) { "Hello world." }
75 | let(:configuration) { subject.configuration }
76 |
77 | before do
78 | configuration.logger = logger
79 | end
80 |
81 | after do
82 | configuration.logger = StringIO.new("")
83 | end
84 |
85 | it "forwards message to logger when verbose" do
86 | configuration.verbose = true
87 |
88 | subject.log(message)
89 |
90 | expect(logger).to have_received(:<<).with("#{message}\n").once
91 | end
92 |
93 | it "does not forward message when not verbose" do
94 | configuration.verbose = false
95 |
96 | subject.log(message)
97 |
98 | expect(logger).not_to have_received(:<<)
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/spec/features/server/webhooks_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | feature "Receiving event webhooks" do
6 | let(:channel) { "room-1" }
7 | let(:other_user) { "Bob" }
8 | let(:cache_channel) { "cache-last-command" }
9 | let(:presence_channel) { "presence-room-1" }
10 |
11 | before do
12 | events.clear
13 |
14 | PusherFake.configuration.webhooks = ["http://127.0.0.1:8082"]
15 |
16 | connect
17 | connect_as(other_user)
18 | end
19 |
20 | scenario "occupying a channel" do
21 | subscribe_to(channel)
22 |
23 | expect(events).to include_event("channel_occupied", "channel" => channel)
24 |
25 | subscribe_to_as(channel, other_user)
26 |
27 | expect(events.size).to eq(1)
28 | end
29 |
30 | scenario "vacating a channel" do
31 | subscribe_to(channel)
32 | subscribe_to_as(channel, other_user)
33 |
34 | unsubscribe_from(channel)
35 |
36 | expect(events.size).to eq(1)
37 |
38 | unsubscribe_from_as(channel, other_user)
39 |
40 | expect(events).to include_event("channel_vacated", "channel" => channel)
41 | end
42 |
43 | scenario "subscribing to a presence channel" do
44 | subscribe_to(presence_channel)
45 |
46 | expect(events).to include_event(
47 | "member_added",
48 | "channel" => presence_channel, "user_id" => user_id
49 | )
50 |
51 | subscribe_to_as(presence_channel, other_user)
52 |
53 | expect(events).to include_event(
54 | "member_added",
55 | "channel" => presence_channel, "user_id" => user_id(other_user)
56 | )
57 | end
58 |
59 | scenario "unsubscribing from a presence channel" do
60 | subscribe_to(presence_channel)
61 | subscribe_to_as(presence_channel, other_user)
62 |
63 | unsubscribe_from(presence_channel)
64 |
65 | expect(events).to include_event("member_added",
66 | "channel" => presence_channel,
67 | "user_id" => user_id)
68 |
69 | unsubscribe_from_as(presence_channel, other_user)
70 |
71 | expect(events).to include_event("member_added",
72 | "channel" => presence_channel,
73 | "user_id" => user_id(other_user))
74 | end
75 |
76 | scenario "subscribing to a cache channel with no event" do
77 | subscribe_to(cache_channel)
78 |
79 | expect(events).to include_event("cache_miss",
80 | "channel" => cache_channel)
81 | end
82 |
83 | scenario "subscribing to a cache channel with an event" do
84 | subscribe_to(cache_channel)
85 | events.clear
86 | Pusher.trigger(cache_channel, "an event", {}, {})
87 |
88 | subscribe_to_as(cache_channel, other_user)
89 |
90 | expect(events).not_to include_event("cache_miss",
91 | "channel" => cache_channel)
92 | end
93 |
94 | protected
95 |
96 | def events
97 | sleep(1)
98 |
99 | WebhookHelper.mutex.synchronize do
100 | WebhookHelper.events
101 | end
102 | end
103 |
104 | def include_event(event, options = {})
105 | include(options.merge("name" => event))
106 | end
107 | end
108 |
--------------------------------------------------------------------------------
/lib/pusher-fake/connection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PusherFake
4 | # A client connection.
5 | class Connection
6 | # Prefix for client events.
7 | CLIENT_EVENT_PREFIX = "client-"
8 |
9 | # @return [EventMachine::WebSocket::Connection] Socket for the connection.
10 | attr_reader :socket
11 |
12 | # Create a new {Connection} object.
13 | #
14 | # @param [EventMachine::WebSocket::Connection] socket Connection object.
15 | def initialize(socket)
16 | @socket = socket
17 | end
18 |
19 | # The ID of the connection.
20 | #
21 | # @return [Integer] The object ID of the socket.
22 | def id
23 | parts = socket.object_id.to_s.chars
24 | parts = parts.each_slice((parts.length / 2.0).ceil).to_a
25 |
26 | [parts.first.join, parts.last.join].join(".")
27 | end
28 |
29 | # Emit an event to the connection.
30 | #
31 | # @param [String] event The event name.
32 | # @param [Hash] data The event data.
33 | # @param [String] channel The channel name.
34 | def emit(event, data = {}, channel = nil)
35 | message = { event: event, data: MultiJson.dump(data) }
36 | message[:channel] = channel if channel
37 |
38 | PusherFake.log("SEND #{id}: #{message}")
39 |
40 | socket.send(MultiJson.dump(message))
41 | end
42 |
43 | # Notify the Pusher client that a connection has been established.
44 | def establish
45 | emit("pusher:connection_established",
46 | socket_id: id, activity_timeout: 120)
47 | end
48 |
49 | # Process an event.
50 | #
51 | # @param [String] data The event data as JSON.
52 | def process(data)
53 | message = MultiJson.load(data, symbolize_keys: true)
54 | event = message[:event]
55 |
56 | PusherFake.log("RECV #{id}: #{message}")
57 |
58 | if event.start_with?(CLIENT_EVENT_PREFIX)
59 | process_trigger(event, message)
60 | else
61 | process_event(event, message)
62 | end
63 | end
64 |
65 | private
66 |
67 | def channel_for(message)
68 | Channel.factory(message[:channel] || message[:data][:channel])
69 | end
70 |
71 | def process_event(event, message)
72 | case event
73 | when "pusher:subscribe"
74 | channel_for(message).add(self, message[:data])
75 | when "pusher:unsubscribe"
76 | channel_for(message).remove(self)
77 | when "pusher:ping"
78 | emit("pusher:pong")
79 | end
80 | end
81 |
82 | def process_trigger(event, message)
83 | channel = channel_for(message)
84 |
85 | return unless channel.is_a?(Channel::Private) && channel.includes?(self)
86 |
87 | channel.emit(event, message[:data], socket_id: id)
88 |
89 | trigger(channel, id, event, message[:data])
90 | end
91 |
92 | def trigger(channel, id, event, data)
93 | Thread.new do
94 | hook = { event: event, channel: channel.name, socket_id: id }
95 | hook[:data] = MultiJson.dump(data) if data
96 |
97 | if channel.is_a?(Channel::Presence)
98 | hook[:user_id] = channel.members[self][:user_id]
99 | end
100 |
101 | channel.trigger("client_event", hook)
102 | end
103 | end
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/spec/lib/pusher-fake/channel_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe PusherFake::Channel, ".factory" do
6 | shared_examples_for "a channel factory" do
7 | subject { described_class }
8 |
9 | let(:channel) { double }
10 |
11 | before do
12 | allow(channel_class).to receive(:new).and_return(channel)
13 | end
14 |
15 | after do
16 | subject.reset
17 | end
18 |
19 | it "caches the channel" do
20 | allow(channel_class).to receive(:new).and_call_original
21 |
22 | factory_one = subject.factory(name)
23 | factory_two = subject.factory(name)
24 |
25 | expect(factory_one).to eq(factory_two)
26 | end
27 |
28 | it "creates the channel by name" do
29 | subject.factory(name)
30 |
31 | expect(channel_class).to have_received(:new).with(name)
32 | end
33 |
34 | it "returns the channel instance" do
35 | factory = subject.factory(name)
36 |
37 | expect(factory).to eq(channel)
38 | end
39 | end
40 |
41 | context "with a public channel" do
42 | let(:name) { "channel" }
43 | let(:channel_class) { PusherFake::Channel::Public }
44 |
45 | it_behaves_like "a channel factory"
46 | end
47 |
48 | context "with a private channel" do
49 | let(:name) { "private-channel" }
50 | let(:channel_class) { PusherFake::Channel::Private }
51 |
52 | it_behaves_like "a channel factory"
53 | end
54 |
55 | context "with a presence channel" do
56 | let(:name) { "presence-channel" }
57 | let(:channel_class) { PusherFake::Channel::Presence }
58 |
59 | it_behaves_like "a channel factory"
60 | end
61 | end
62 |
63 | describe PusherFake::Channel, ".remove" do
64 | subject { described_class }
65 |
66 | let(:channels) { { channel_1: channel_1, channel_2: channel_2 } }
67 | let(:connection) { double }
68 |
69 | let(:channel_1) do
70 | instance_double(PusherFake::Channel::Public,
71 | remove: nil,
72 | connections: instance_double(Array, empty?: true))
73 | end
74 |
75 | let(:channel_2) do
76 | instance_double(PusherFake::Channel::Public,
77 | remove: nil,
78 | connections: instance_double(Array, empty?: false))
79 | end
80 |
81 | before do
82 | allow(subject).to receive(:channels).and_return(channels)
83 | end
84 |
85 | it "removes the connection from all channels" do
86 | subject.remove(connection)
87 |
88 | expect(channel_1).to have_received(:remove).with(connection)
89 | expect(channel_2).to have_received(:remove).with(connection)
90 | end
91 |
92 | it "deletes a channel with no connections remaining" do
93 | subject.remove(connection)
94 |
95 | expect(channels).not_to have_key(:channel_1)
96 | end
97 |
98 | it "does not delete a channel with connections remaining" do
99 | subject.remove(connection)
100 |
101 | expect(channels).to have_key(:channel_2)
102 | end
103 |
104 | it "handles channels not being defined" do
105 | allow(subject).to receive(:channels).and_return(nil)
106 |
107 | expect do
108 | subject.remove(connection)
109 | end.not_to raise_error
110 | end
111 | end
112 |
113 | describe PusherFake::Channel, ".reset" do
114 | subject { described_class }
115 |
116 | it "empties the channel cache" do
117 | subject.factory("example")
118 | subject.reset
119 |
120 | expect(subject.channels).to eq({})
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/spec/lib/pusher-fake/configuration_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe PusherFake::Configuration do
6 | it do
7 | expect(subject).to have_configuration_option(:disable_stats)
8 | .with_default(true)
9 | end
10 |
11 | it do
12 | expect(subject).to have_configuration_option(:key)
13 | .with_default("PUSHER_API_KEY")
14 | end
15 |
16 | it do
17 | expect(subject).to have_configuration_option(:logger)
18 | .with_default($stdout.to_io)
19 | end
20 |
21 | it do
22 | expect(subject).to have_configuration_option(:verbose)
23 | .with_default(false)
24 | end
25 |
26 | it do
27 | expect(subject).to have_configuration_option(:webhooks)
28 | .with_default([])
29 | end
30 |
31 | it do
32 | expect(subject).to have_configuration_option(:app_id)
33 | .with_default("PUSHER_APP_ID")
34 | end
35 |
36 | it do
37 | expect(subject).to have_configuration_option(:secret)
38 | .with_default("PUSHER_API_SECRET")
39 | end
40 |
41 | it "has configuration option :socket_options" do
42 | expect(subject.socket_options).to be_a(Hash)
43 | expect(subject.socket_options[:host]).to eq("127.0.0.1")
44 | expect(subject.socket_options[:port]).to be_a(Integer)
45 | end
46 |
47 | it "has configuration option :web_options" do
48 | expect(subject.web_options).to be_a(Hash)
49 | expect(subject.web_options[:host]).to eq("127.0.0.1")
50 | expect(subject.web_options[:port]).to be_a(Integer)
51 | end
52 |
53 | it "defaults socket and web ports to different values" do
54 | expect(subject.socket_options[:port]).not_to eq(subject.web_options[:port])
55 | end
56 |
57 | context "with StringIO as standard out" do
58 | let(:io) { StringIO.new }
59 |
60 | around do |example|
61 | original = $stdout
62 | $stdout = io # rubocop:disable RSpec/ExpectOutput
63 |
64 | example.run
65 |
66 | $stdout = original # rubocop:disable RSpec/ExpectOutput
67 | end
68 |
69 | it "assigns standard out to the logger" do
70 | subject.reset!
71 |
72 | expect(subject.logger).to eq(io)
73 | end
74 | end
75 | end
76 |
77 | describe PusherFake::Configuration, "#app_id=" do
78 | subject { described_class.new }
79 |
80 | it "converts value to a string" do
81 | subject.app_id = 1_337
82 |
83 | expect(subject.app_id).to eq("1337")
84 | end
85 | end
86 |
87 | describe PusherFake::Configuration, "#to_options" do
88 | it "includes disable_stats as disableStats" do
89 | options = subject.to_options
90 |
91 | expect(options).to include(disableStats: subject.disable_stats)
92 | end
93 |
94 | it "includes the socket host as wsHost" do
95 | options = subject.to_options
96 |
97 | expect(options).to include(wsHost: subject.socket_options[:host])
98 | end
99 |
100 | it "includes the socket port as wsPort" do
101 | options = subject.to_options
102 |
103 | expect(options).to include(wsPort: subject.socket_options[:port])
104 | end
105 |
106 | it "includes the cluster by default" do
107 | options = subject.to_options
108 |
109 | expect(options).to include(cluster: "us-east-1")
110 | end
111 |
112 | it "disables the forceTLS option" do
113 | options = subject.to_options
114 |
115 | expect(options).to include(forceTLS: false)
116 | end
117 |
118 | it "supports passing custom options" do
119 | options = subject.to_options(custom: "option")
120 |
121 | expect(options).to include(custom: "option")
122 | end
123 | end
124 |
--------------------------------------------------------------------------------
/spec/features/server/event_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | feature "Server triggers event" do
6 | let(:event) { "message" }
7 | let(:other_user) { "Bob" }
8 | let(:public_channel) { "chat" }
9 | let(:private_channel) { "private-chat" }
10 |
11 | before do
12 | connect
13 | connect_as(other_user)
14 | end
15 |
16 | scenario "on a subscribed public channel" do
17 | subscribe_to(public_channel)
18 | subscribe_to_as(public_channel, other_user)
19 |
20 | trigger(public_channel, event)
21 |
22 | expect(page).to have_event(event, on: public_channel)
23 |
24 | using_session(other_user) do
25 | expect(page).to have_event(event, on: public_channel)
26 | end
27 | end
28 |
29 | scenario "on a previously subscribed public channel" do
30 | subscribe_to(public_channel)
31 | subscribe_to_as(public_channel, other_user)
32 | unsubscribe_from(public_channel)
33 |
34 | trigger(public_channel, event)
35 |
36 | expect(page).not_to have_event(event, on: public_channel)
37 |
38 | using_session(other_user) do
39 | expect(page).to have_event(event, on: public_channel)
40 | end
41 | end
42 |
43 | scenario "on an unsubscribed public channel" do
44 | trigger(public_channel, event)
45 |
46 | expect(page).not_to have_event(event, on: public_channel)
47 |
48 | using_session(other_user) do
49 | expect(page).not_to have_event(event, on: public_channel)
50 | end
51 | end
52 |
53 | scenario "on a subscribed private channel" do
54 | subscribe_to(private_channel)
55 | subscribe_to_as(private_channel, other_user)
56 |
57 | trigger(private_channel, event)
58 |
59 | expect(page).to have_event(event, on: private_channel)
60 |
61 | using_session(other_user) do
62 | expect(page).to have_event(event, on: private_channel)
63 | end
64 | end
65 |
66 | scenario "on a previously subscribed private channel" do
67 | subscribe_to(private_channel)
68 | subscribe_to_as(private_channel, other_user)
69 | unsubscribe_from(private_channel)
70 |
71 | trigger(private_channel, event)
72 |
73 | expect(page).not_to have_event(event, on: private_channel)
74 |
75 | using_session(other_user) do
76 | expect(page).to have_event(event, on: private_channel)
77 | end
78 | end
79 |
80 | scenario "on an unsubscribed private channel" do
81 | trigger(private_channel, event)
82 |
83 | expect(page).not_to have_event(event, on: private_channel)
84 |
85 | using_session(other_user) do
86 | expect(page).not_to have_event(event, on: private_channel)
87 | end
88 | end
89 |
90 | scenario "on multiple subscribed private channels" do
91 | subscribe_to("private-chat-1")
92 | subscribe_to_as("private-chat-2", other_user)
93 |
94 | trigger("private-chat-1", event)
95 | trigger("private-chat-2", event)
96 |
97 | expect(page).to have_event(event, on: "private-chat-1")
98 |
99 | using_session(other_user) do
100 | expect(page).to have_event(event, on: "private-chat-2")
101 | end
102 | end
103 |
104 | scenario "on a subscribed public channel, ignoring a user" do
105 | subscribe_to(public_channel)
106 | subscribe_to_as(public_channel, other_user)
107 |
108 | trigger(public_channel, event, socket_id: user_id(other_user))
109 |
110 | expect(page).to have_event(event, on: public_channel)
111 |
112 | using_session(other_user) do
113 | expect(page).not_to have_event(event, on: public_channel)
114 | end
115 | end
116 |
117 | protected
118 |
119 | def trigger(channel, event, options = {})
120 | Pusher.trigger(channel, event, {}, options)
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/lib/pusher-fake/channel/public.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PusherFake
4 | module Channel
5 | # A public channel.
6 | class Public
7 | CACHE_CHANNEL_PREFIX = /^(private-|presence-){0,1}cache-/
8 |
9 | # @return [Array] Connections in this channel.
10 | attr_reader :connections
11 |
12 | # @return [String] The channel name.
13 | attr_reader :name
14 |
15 | # Create a new {Public} object.
16 | #
17 | # @param [String] name The channel name.
18 | def initialize(name)
19 | @name = name
20 | @last_event = nil
21 | @connections = []
22 | end
23 |
24 | # Add the connection to the channel.
25 | #
26 | # @param [Connection] connection The connection to add.
27 | # @param [Hash] options The options for the channel.
28 | def add(connection, options = {})
29 | subscription_succeeded(connection, options)
30 | emit_last_event(connection)
31 | end
32 |
33 | # Emit an event to the channel.
34 | #
35 | # @param [String] event The event name.
36 | # @param [Hash] data The event data.
37 | def emit(event, data, options = {})
38 | if cache_channel?
39 | @last_event = [event, data]
40 | end
41 |
42 | connections.each do |connection|
43 | unless connection.id == options[:socket_id]
44 | connection.emit(event, data, name)
45 | end
46 | end
47 | end
48 |
49 | # Determine if the +connection+ is in the channel.
50 | #
51 | # @param [Connection] connection The connection.
52 | # @return [Boolean] If the connection is in the channel or not.
53 | def includes?(connection)
54 | connections.index(connection)
55 | end
56 |
57 | # Remove the +connection+ from the channel.
58 | #
59 | # If it is the last connection, trigger the channel_vacated webhook.
60 | #
61 | # @param [Connection] connection The connection to remove.
62 | def remove(connection)
63 | connections.delete(connection)
64 |
65 | trigger("channel_vacated", channel: name) if connections.empty?
66 | end
67 |
68 | # Return subscription data for the channel.
69 | #
70 | # @abstract
71 | # @return [Hash] Subscription data for the channel.
72 | def subscription_data
73 | {}
74 | end
75 |
76 | def trigger(name, data = {})
77 | PusherFake::Webhook.trigger(name, data)
78 | end
79 |
80 | private
81 |
82 | # @return [Array] Arguments for the last event emitted.
83 | attr_reader :last_event
84 |
85 | # Whether or not the channel is a cache channel.
86 | #
87 | # @return [Boolean]
88 | def cache_channel?
89 | @cache_channel ||= name.match?(CACHE_CHANNEL_PREFIX)
90 | end
91 |
92 | # Emit the last event if present and a cache channel.
93 | #
94 | # @param [Connection] connection The connection to emit to.
95 | def emit_last_event(connection)
96 | return unless cache_channel?
97 |
98 | if last_event
99 | connection.emit(*last_event, name)
100 | else
101 | connection.emit("pusher:cache_miss", nil, name)
102 | trigger("cache_miss", channel: name)
103 | end
104 | end
105 |
106 | # Notify the +connection+ of the successful subscription and add the
107 | # connection to the channel.
108 | #
109 | # If it is the first connection, trigger the channel_occupied webhook.
110 | #
111 | # @param [Connection] connection Connection a subscription succeeded for.
112 | # @param [Hash] options The options for the channel.
113 | def subscription_succeeded(connection, _options = {})
114 | connection.emit("pusher_internal:subscription_succeeded",
115 | subscription_data, name)
116 |
117 | connections.push(connection)
118 |
119 | trigger("channel_occupied", channel: name) if connections.length == 1
120 | end
121 | end
122 | end
123 | end
124 |
--------------------------------------------------------------------------------
/spec/lib/pusher-fake/server_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe PusherFake::Server, ".start" do
6 | subject { described_class }
7 |
8 | before do
9 | allow(subject).to receive_messages(start_web_server: nil, start_socket_server: nil)
10 | allow(EventMachine).to receive(:run).and_return(nil)
11 | end
12 |
13 | it "prepends the chain trap handlers module to the WebSocket server" do
14 | allow(EventMachine::WebSocket.singleton_class).to receive(:prepend)
15 |
16 | subject.start
17 |
18 | expect(EventMachine::WebSocket.singleton_class).to have_received(:prepend)
19 | .with(PusherFake::Server::ChainTrapHandlers)
20 | end
21 |
22 | it "prepends the chain trap handlers module to the web server" do
23 | allow(Thin::Server).to receive(:prepend)
24 |
25 | subject.start
26 |
27 | expect(Thin::Server).to have_received(:prepend).with(PusherFake::Server::ChainTrapHandlers)
28 | end
29 |
30 | it "runs the event loop" do
31 | subject.start
32 |
33 | expect(EventMachine).to have_received(:run).with(no_args)
34 | end
35 |
36 | it "starts the socket web server when run yields" do
37 | subject.start
38 |
39 | expect(subject).not_to have_received(:start_web_server)
40 |
41 | allow(EventMachine).to receive(:run).and_yield
42 |
43 | subject.start
44 |
45 | expect(subject).to have_received(:start_web_server).with(no_args)
46 | end
47 |
48 | it "starts the socket server when run yields" do
49 | subject.start
50 |
51 | expect(subject).not_to have_received(:start_socket_server)
52 |
53 | allow(EventMachine).to receive(:run).and_yield
54 |
55 | subject.start
56 |
57 | expect(subject).to have_received(:start_socket_server).with(no_args)
58 | end
59 | end
60 |
61 | describe PusherFake::Server, ".start_socket_server" do
62 | subject { described_class }
63 |
64 | let(:data) { double }
65 | let(:options) { configuration.socket_options }
66 |
67 | let(:configuration) do
68 | instance_double(PusherFake::Configuration,
69 | socket_options: { host: "192.168.0.1", port: 8080 })
70 | end
71 |
72 | let(:connection) do
73 | instance_double(PusherFake::Connection, establish: nil, process: nil)
74 | end
75 |
76 | let(:socket) do
77 | instance_double(EventMachine::WebSocket::Connection,
78 | onopen: nil, onmessage: nil, onclose: nil)
79 | end
80 |
81 | before do
82 | allow(PusherFake).to receive(:configuration).and_return(configuration)
83 | allow(PusherFake::Channel).to receive(:remove)
84 | allow(PusherFake::Connection).to receive(:new).and_return(connection)
85 | allow(EventMachine::WebSocket).to receive(:start).and_yield(socket)
86 | end
87 |
88 | it "creates a WebSocket server" do
89 | subject.start_socket_server
90 |
91 | expect(EventMachine::WebSocket).to have_received(:start).with(options)
92 | end
93 |
94 | it "defines an open callback on the socket" do
95 | subject.start_socket_server
96 |
97 | expect(socket).to have_received(:onopen).with(no_args)
98 | end
99 |
100 | it "creates a connection with the provided socket when onopen yields" do
101 | allow(socket).to receive(:onopen).and_yield
102 |
103 | subject.start_socket_server
104 |
105 | expect(PusherFake::Connection).to have_received(:new).with(socket)
106 | end
107 |
108 | it "establishes the connection when onopen yields" do
109 | allow(socket).to receive(:onopen).and_yield
110 |
111 | subject.start_socket_server
112 |
113 | expect(connection).to have_received(:establish).with(no_args)
114 | end
115 |
116 | it "defines a message callback on the socket when onopen yields" do
117 | allow(socket).to receive(:onopen).and_yield
118 |
119 | subject.start_socket_server
120 |
121 | expect(socket).to have_received(:onmessage).with(no_args)
122 | end
123 |
124 | it "triggers process on the connection when onmessage yields" do
125 | allow(socket).to receive(:onopen).and_yield
126 | allow(socket).to receive(:onmessage).and_yield(data)
127 |
128 | subject.start_socket_server
129 |
130 | expect(connection).to have_received(:process).with(data)
131 | end
132 |
133 | it "defines a close callback on the socket when onopen yields" do
134 | allow(socket).to receive(:onopen).and_yield
135 |
136 | subject.start_socket_server
137 |
138 | expect(socket).to have_received(:onclose).with(no_args)
139 | end
140 |
141 | it "removes the connection from all channels when onclose yields" do
142 | allow(socket).to receive(:onopen).and_yield
143 | allow(socket).to receive(:onclose).and_yield
144 |
145 | subject.start_socket_server
146 |
147 | expect(PusherFake::Channel).to have_received(:remove).with(connection)
148 | end
149 | end
150 |
151 | describe PusherFake::Server, ".start_web_server" do
152 | subject { described_class }
153 |
154 | let(:host) { "192.168.0.1" }
155 | let(:port) { 8081 }
156 | let(:server) { instance_double(Thin::Server, :start! => true, :ssl= => true) }
157 |
158 | let(:configuration) do
159 | instance_double(PusherFake::Configuration,
160 | web_options: { host: host, port: port, ssl: true })
161 | end
162 |
163 | before do
164 | allow(Thin::Server).to receive(:new).and_return(server)
165 | allow(Thin::Logging).to receive(:silent=)
166 | allow(PusherFake).to receive(:configuration).and_return(configuration)
167 | end
168 |
169 | it "silences the logging" do
170 | subject.start_web_server
171 |
172 | expect(Thin::Logging).to have_received(:silent=).with(true)
173 | end
174 |
175 | it "creates the web server" do
176 | subject.start_web_server
177 |
178 | expect(Thin::Server).to have_received(:new)
179 | .with(host, port, PusherFake::Server::Application)
180 | end
181 |
182 | it "assigns custom options to the server" do
183 | subject.start_web_server
184 |
185 | expect(server).to have_received(:ssl=).with(true)
186 | end
187 |
188 | it "starts the web server" do
189 | subject.start_web_server
190 |
191 | expect(server).to have_received(:start!).with(no_args)
192 | end
193 | end
194 |
--------------------------------------------------------------------------------
/lib/pusher-fake/server/application.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module PusherFake
4 | module Server
5 | # The fake web application.
6 | class Application
7 | CHANNEL_FILTER_ERROR = "user_count may only be requested for presence " \
8 | "channels - please supply filter_by_prefix " \
9 | "begining with presence-"
10 |
11 | CHANNEL_USER_COUNT_ERROR = "Cannot retrieve the user count unless the " \
12 | "channel is a presence channel"
13 |
14 | REQUEST_PATHS = {
15 | %r{\A/apps/:id/batch_events\z} => :batch_events,
16 | %r{\A/apps/:id/events\z} => :events,
17 | %r{\A/apps/:id/channels\z} => :channels,
18 | %r{\A/apps/:id/channels/([^/]+)\z} => :channel,
19 | %r{\A/apps/:id/channels/([^/]+)/users\z} => :users
20 | }.freeze
21 |
22 | # Process an API request.
23 | #
24 | # @param [Hash] environment The request environment.
25 | # @return [Rack::Response] A successful response.
26 | def self.call(environment)
27 | request = Rack::Request.new(environment)
28 | response = response_for(request)
29 |
30 | Rack::Response.new(MultiJson.dump(response)).finish
31 | rescue StandardError => error
32 | Rack::Response.new(error.message, 400).finish
33 | end
34 |
35 | # Emit batch events with data to the requested channel(s).
36 | #
37 | # @param [Rack::Request] request The HTTP request.
38 | # @return [Hash] An empty hash.
39 | def self.batch_events(request)
40 | batch = MultiJson.load(request.body.read)["batch"]
41 | batch.each do |event|
42 | send_event(event)
43 | end
44 |
45 | {}
46 | end
47 |
48 | # Emit an event with data to the requested channel(s).
49 | #
50 | # @param [Rack::Request] request The HTTP request.
51 | # @return [Hash] An empty hash.
52 | def self.events(request)
53 | event = MultiJson.load(request.body.read)
54 |
55 | send_event(event)
56 |
57 | {}
58 | end
59 |
60 | # Return a hash of channel information.
61 | #
62 | # Occupied status is always included. A user count may be requested for
63 | # presence channels.
64 | #
65 | # @param [String] name The channel name.
66 | # @param [Rack::Request] request The HTTP request.
67 | # @return [Hash] A hash of channel information.
68 | def self.channel(name, request)
69 | count = request.params["info"].to_s.split(",").include?("user_count")
70 |
71 | raise CHANNEL_USER_COUNT_ERROR if invalid_channel_to_count?(name, count)
72 |
73 | channel = Channel.channels[name]
74 | connections = channel ? channel.connections : []
75 |
76 | result = { occupied: connections.any? }
77 | result[:user_count] = connections.size if count
78 | result
79 | end
80 |
81 | # Returns a hash of occupied channels, optionally filtering with a
82 | # prefix. When filtering to presence chanenls, the user count maybe also
83 | # be requested.
84 | #
85 | # @param [Rack::Request] request The HTTP request.
86 | # @return [Hash] A hash of occupied channels.
87 | #
88 | # rubocop:disable Metrics/AbcSize
89 | def self.channels(request)
90 | count = request.params["info"].to_s.split(",").include?("user_count")
91 | prefix = request.params["filter_by_prefix"].to_s
92 |
93 | raise CHANNEL_FILTER_ERROR if invalid_channel_to_count?(prefix, count)
94 |
95 | PusherFake::Channel
96 | .channels
97 | .each_with_object(channels: {}) do |(name, channel), result|
98 | next unless name.start_with?(prefix)
99 |
100 | channels = result[:channels].merge!(name => {})
101 | channels[name][:user_count] = channel.connections.size if count
102 | end
103 | end
104 | # rubocop:enable Metrics/AbcSize
105 |
106 | # Attempt to provide a response for the provided request.
107 | #
108 | # @param [Rack::Request] request The HTTP request.
109 | # @return [Hash] A response hash.
110 | def self.response_for(request)
111 | id = PusherFake.configuration.app_id
112 |
113 | REQUEST_PATHS.each do |path, method|
114 | matcher = Regexp.new(path.to_s.sub(":id", id))
115 | matches = matcher.match(request.path)
116 |
117 | next if matches.nil?
118 |
119 | arguments = [matches[1], request].compact
120 |
121 | return public_send(method, *arguments)
122 | end
123 |
124 | raise "Unknown path: #{request.path}"
125 | end
126 |
127 | # Returns a hash of the IDs for the users in the channel.
128 | #
129 | # @param [String] name The channel name.
130 | # @return [Hash] A hash of user IDs.
131 | def self.users(name, _request = nil)
132 | channels = PusherFake::Channel.channels || {}
133 | channel = channels[name]
134 |
135 | if channel
136 | users = channel.connections.map do |connection|
137 | { id: connection.id }
138 | end
139 | end
140 |
141 | { users: users || [] }
142 | end
143 |
144 | # @return [Boolean]
145 | def self.invalid_channel_to_count?(name, includes_count)
146 | includes_count && !name.start_with?(Channel::PRESENCE_CHANNEL_PREFIX)
147 | end
148 | private_class_method :invalid_channel_to_count?
149 |
150 | # Emit an event with data to the requested channel(s).
151 | #
152 | # @param [Hash] event The raw event JSON.
153 | #
154 | # rubocop:disable Style/RescueModifier
155 | def self.send_event(event)
156 | data = MultiJson.load(event["data"]) rescue event["data"]
157 | channels = Array(event["channels"] || event["channel"])
158 | channels.each do |channel_name|
159 | channel = Channel.factory(channel_name)
160 | channel.emit(event["name"], data, socket_id: event["socket_id"])
161 | end
162 | end
163 | private_class_method :send_event
164 | # rubocop:enable Style/RescueModifier
165 | end
166 | end
167 | end
168 |
--------------------------------------------------------------------------------
/spec/lib/pusher-fake/channel/private_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe PusherFake::Channel::Private do
6 | subject { described_class }
7 |
8 | it "inherits from public channel" do
9 | expect(subject.ancestors).to include(PusherFake::Channel::Public)
10 | end
11 | end
12 |
13 | describe PusherFake::Channel::Private, "#add" do
14 | subject { described_class.new(name) }
15 |
16 | let(:data) { { auth: authentication } }
17 | let(:name) { "name" }
18 | let(:connection) { instance_double(PusherFake::Connection, emit: nil) }
19 | let(:connections) { instance_double(Array, push: nil, length: 0) }
20 | let(:authentication) { "auth" }
21 |
22 | before do
23 | allow(PusherFake::Webhook).to receive(:trigger)
24 | allow(subject).to receive(:connections).and_return(connections)
25 | end
26 |
27 | it "authorizes the connection" do
28 | allow(subject).to receive(:authorized?).and_return(nil)
29 |
30 | subject.add(connection, data)
31 |
32 | expect(subject).to have_received(:authorized?).with(connection, data)
33 | end
34 |
35 | it "adds the connection to the channel when authorized" do
36 | allow(subject).to receive(:authorized?).and_return(true)
37 |
38 | subject.add(connection, data)
39 |
40 | expect(connections).to have_received(:push).with(connection)
41 | end
42 |
43 | it "successfully subscribes the connection when authorized" do
44 | allow(subject).to receive(:authorized?).and_return(true)
45 |
46 | subject.add(connection, data)
47 |
48 | expect(connection).to have_received(:emit)
49 | .with("pusher_internal:subscription_succeeded", {}, subject.name)
50 | end
51 |
52 | it "triggers occupied webhook for first connection added when authorized" do
53 | allow(subject).to receive(:authorized?).and_return(true)
54 | allow(subject).to receive(:connections).and_call_original
55 |
56 | 2.times { subject.add(connection, data) }
57 |
58 | expect(PusherFake::Webhook).to have_received(:trigger)
59 | .with("channel_occupied", channel: name).once
60 | end
61 |
62 | it "unsuccessfully subscribes the connection when not authorized" do
63 | allow(subject).to receive(:authorized?).and_return(false)
64 |
65 | subject.add(connection, data)
66 |
67 | expect(connection).to have_received(:emit)
68 | .with("pusher_internal:subscription_error", {}, subject.name)
69 | end
70 |
71 | it "does not trigger channel occupied webhook when not authorized" do
72 | allow(subject).to receive(:authorized?).and_return(false)
73 | allow(subject).to receive(:connections).and_call_original
74 |
75 | 2.times { subject.add(connection, data) }
76 |
77 | expect(PusherFake::Webhook).not_to have_received(:trigger)
78 | end
79 | end
80 |
81 | describe PusherFake::Channel::Private, "#authentication_for" do
82 | subject { described_class.new(name) }
83 |
84 | let(:id) { "1234" }
85 | let(:name) { "private-channel" }
86 | let(:digest) { instance_double(OpenSSL::Digest::SHA256) }
87 | let(:string) { [id, name].join(":") }
88 | let(:signature) { "signature" }
89 |
90 | let(:configuration) do
91 | instance_double(PusherFake::Configuration, key: "key", secret: "secret")
92 | end
93 |
94 | before do
95 | allow(PusherFake).to receive(:configuration).and_return(configuration)
96 | allow(OpenSSL::HMAC).to receive(:hexdigest).and_return(signature)
97 | allow(OpenSSL::Digest).to receive(:new).with("SHA256").and_return(digest)
98 | end
99 |
100 | it "generates a signature" do
101 | subject.authentication_for(id)
102 |
103 | expect(OpenSSL::HMAC).to have_received(:hexdigest)
104 | .with(digest, configuration.secret, string)
105 | end
106 |
107 | it "returns the authentication string" do
108 | string = subject.authentication_for(id)
109 |
110 | expect(string).to eq("#{configuration.key}:#{signature}")
111 | end
112 | end
113 |
114 | describe PusherFake::Channel::Private,
115 | "#authentication_for, with channel data" do
116 | subject { described_class.new(name) }
117 |
118 | let(:id) { "1234" }
119 | let(:name) { "private-channel" }
120 | let(:digest) { instance_double(OpenSSL::Digest::SHA256) }
121 | let(:string) { [id, name, channel_data].join(":") }
122 | let(:signature) { "signature" }
123 | let(:channel_data) { "{}" }
124 |
125 | let(:configuration) do
126 | instance_double(PusherFake::Configuration, key: "key", secret: "secret")
127 | end
128 |
129 | before do
130 | allow(PusherFake).to receive(:configuration).and_return(configuration)
131 | allow(OpenSSL::HMAC).to receive(:hexdigest).and_return(signature)
132 | allow(OpenSSL::Digest).to receive(:new).with("SHA256").and_return(digest)
133 | end
134 |
135 | it "generates a signature" do
136 | subject.authentication_for(id, channel_data)
137 |
138 | expect(OpenSSL::HMAC).to have_received(:hexdigest)
139 | .with(digest, configuration.secret, string)
140 | end
141 |
142 | it "returns the authentication string" do
143 | string = subject.authentication_for(id, channel_data)
144 |
145 | expect(string).to eq("#{configuration.key}:#{signature}")
146 | end
147 | end
148 |
149 | describe PusherFake::Channel::Private, "#authorized?" do
150 | subject { described_class.new(name) }
151 |
152 | let(:data) { { auth: authentication, channel_data: channel_data } }
153 | let(:name) { "private-channel" }
154 | let(:connection) { instance_double(PusherFake::Connection, id: "1") }
155 | let(:channel_data) { "{}" }
156 | let(:authentication) { "authentication" }
157 |
158 | before do
159 | allow(subject).to receive(:authentication_for)
160 | end
161 |
162 | it "generates authentication for the connection ID" do
163 | subject.authorized?(connection, data)
164 |
165 | expect(subject).to have_received(:authentication_for)
166 | .with(connection.id, channel_data)
167 | end
168 |
169 | it "returns true if the authentication matches" do
170 | allow(subject).to receive(:authentication_for).and_return(authentication)
171 |
172 | expect(subject).to be_authorized(connection, data)
173 | end
174 |
175 | it "returns false if the authentication matches" do
176 | allow(subject).to receive(:authentication_for).and_return("")
177 |
178 | expect(subject).not_to be_authorized(connection, data)
179 | end
180 | end
181 |
--------------------------------------------------------------------------------
/spec/lib/pusher-fake/channel/public_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe PusherFake::Channel::Public do
6 | subject { described_class }
7 |
8 | let(:name) { "channel" }
9 |
10 | it "assigns the provided name" do
11 | channel = subject.new(name)
12 |
13 | expect(channel.name).to eq(name)
14 | end
15 |
16 | it "creates an empty connections array" do
17 | channel = subject.new(name)
18 |
19 | expect(channel.connections).to eq([])
20 | end
21 | end
22 |
23 | describe PusherFake::Channel, "#add" do
24 | subject { PusherFake::Channel::Public.new(name) }
25 |
26 | let(:name) { "name" }
27 | let(:connection) { instance_double(PusherFake::Connection, emit: nil) }
28 | let(:connections) { instance_double(Array, push: nil, length: 0) }
29 |
30 | before do
31 | allow(PusherFake::Webhook).to receive(:trigger)
32 | allow(subject).to receive(:connections).and_return(connections)
33 | end
34 |
35 | it "adds the connection" do
36 | subject.add(connection)
37 |
38 | expect(connections).to have_received(:push).with(connection)
39 | end
40 |
41 | it "successfully subscribes the connection" do
42 | subject.add(connection)
43 |
44 | expect(connection).to have_received(:emit)
45 | .with("pusher_internal:subscription_succeeded", {}, subject.name)
46 | end
47 |
48 | it "triggers channel occupied webhook for the first connection added" do
49 | allow(subject).to receive(:connections).and_call_original
50 |
51 | 2.times { subject.add(connection) }
52 |
53 | expect(PusherFake::Webhook).to have_received(:trigger)
54 | .with("channel_occupied", channel: name).once
55 | end
56 |
57 | context "when a cached channel" do
58 | let(:name) { "cache-name" }
59 |
60 | before do
61 | allow(subject).to receive(:connections).and_call_original
62 | end
63 |
64 | context "with a cached event" do
65 | before do
66 | subject.emit("example-cache-event", example: true)
67 | end
68 |
69 | it "triggers the cached event to the connection added" do
70 | subject.add(connection)
71 |
72 | expect(connection).to have_received(:emit)
73 | .with("example-cache-event", { example: true }, name).once
74 | end
75 |
76 | it "does not trigger cache miss event" do
77 | subject.add(connection)
78 |
79 | expect(connection).not_to have_received(:emit)
80 | .with("pusher:cache-miss", nil, name)
81 | end
82 |
83 | it "does not trigger cache miss webhook" do
84 | subject.add(connection)
85 |
86 | expect(PusherFake::Webhook).not_to have_received(:trigger)
87 | .with("cache_miss", channel: name)
88 | end
89 | end
90 |
91 | context "without a cached event" do
92 | it "triggers cache miss event" do
93 | subject.add(connection)
94 |
95 | expect(connection).to have_received(:emit)
96 | .with("pusher:cache_miss", nil, name).once
97 | end
98 |
99 | it "triggers cache miss webhook" do
100 | subject.add(connection)
101 |
102 | expect(PusherFake::Webhook).to have_received(:trigger)
103 | .with("cache_miss", channel: name).once
104 | end
105 |
106 | it "does not trigger event" do
107 | subject.add(connection)
108 |
109 | expect(connection).not_to have_received(:emit)
110 | .with("example-cache-event", { example: true }, name)
111 | end
112 | end
113 | end
114 | end
115 |
116 | describe PusherFake::Channel, "#emit" do
117 | subject { PusherFake::Channel::Public.new(name) }
118 |
119 | let(:data) { double }
120 | let(:name) { "name" }
121 | let(:event) { "event" }
122 | let(:connections) { [connection_1, connection_2] }
123 |
124 | let(:connection_1) do
125 | instance_double(PusherFake::Connection, emit: nil, id: "1")
126 | end
127 |
128 | let(:connection_2) do
129 | instance_double(PusherFake::Connection, emit: nil, id: "2")
130 | end
131 |
132 | before do
133 | allow(subject).to receive(:connections).and_return(connections)
134 | end
135 |
136 | it "emits the event for each connection in the channel" do
137 | subject.emit(event, data)
138 |
139 | expect(connection_1).to have_received(:emit).with(event, data, name)
140 | expect(connection_2).to have_received(:emit).with(event, data, name)
141 | end
142 |
143 | it "ignores connection if socket_id matches the connections ID" do
144 | subject.emit(event, data, socket_id: connection_2.id)
145 |
146 | expect(connection_1).to have_received(:emit).with(event, data, name)
147 | expect(connection_2).not_to have_received(:emit)
148 | end
149 | end
150 |
151 | describe PusherFake::Channel, "#includes?" do
152 | subject { PusherFake::Channel::Public.new("name") }
153 |
154 | let(:connection) { double }
155 |
156 | it "returns true if the connection is in the channel" do
157 | allow(subject).to receive(:connections).and_return([connection])
158 |
159 | expect(subject).to be_includes(connection)
160 | end
161 |
162 | it "returns false if the connection is not in the channel" do
163 | allow(subject).to receive(:connections).and_return([])
164 |
165 | expect(subject).not_to be_includes(connection)
166 | end
167 | end
168 |
169 | describe PusherFake::Channel, "#remove" do
170 | subject { PusherFake::Channel::Public.new(name) }
171 |
172 | let(:name) { "name" }
173 | let(:connection_1) { double }
174 | let(:connection_2) { double }
175 |
176 | before do
177 | allow(PusherFake::Webhook).to receive(:trigger)
178 | allow(subject).to receive(:connections)
179 | .and_return([connection_1, connection_2])
180 | end
181 |
182 | it "removes the connection from the channel" do
183 | subject.remove(connection_1)
184 |
185 | expect(subject.connections).not_to include(connection_1)
186 | end
187 |
188 | it "triggers channel vacated webhook when all connections are removed" do
189 | subject.remove(connection_1)
190 |
191 | expect(PusherFake::Webhook).not_to have_received(:trigger)
192 |
193 | subject.remove(connection_2)
194 |
195 | expect(PusherFake::Webhook).to have_received(:trigger)
196 | .with("channel_vacated", channel: name).once
197 | end
198 | end
199 |
200 | describe PusherFake::Channel::Public, "#subscription_data" do
201 | subject { described_class.new("name") }
202 |
203 | it "returns an empty hash" do
204 | expect(subject.subscription_data).to eq({})
205 | end
206 | end
207 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | # pusher-fake [](https://rubygems.org/gems/pusher-fake) [](https://github.com/tristandunn/pusher-fake/actions?query=workflow%3ACI) [](https://codeclimate.com/github/tristandunn/pusher-fake/maintainability)
2 |
3 | A fake [Pusher](https://pusher.com) server for development and testing.
4 |
5 | When run, an entire fake service starts on two random open ports. A Pusher account is not required to make connections to the fake service. If you need to know the host or port, you can find the values in the configuration.
6 |
7 | The project fully replaces the Pusher service with a local version for testing and development. Using the service as a replacement for production is not recommended.
8 |
9 | #### Why?
10 |
11 | 1. Working offline is not possible.
12 | 1. Using a remote API for testing is slow.
13 | 1. Wasting connections and messages in development is unreasonable.
14 | 1. Stubbing the JavaScript, such as with [pusher-js-test-stub](https://github.com/pusher-community/pusher-js-test-stub), is suboptimal and tedious for integration tests.
15 |
16 | ## Usage
17 |
18 | ### Test Environment
19 |
20 | #### 1. Use the PusherFake JS for the Pusher JS instance.
21 |
22 | ```erb
23 |
34 | ```
35 |
36 | #### 2. Start PusherFake in your environment.
37 |
38 | ##### RSpec
39 |
40 | ```ruby
41 | require "pusher-fake/support/rspec"
42 | ```
43 |
44 | ##### Cucumber
45 |
46 | ```ruby
47 | require "pusher-fake/support/cucumber"
48 | ```
49 |
50 | ##### Zeus
51 |
52 | Using Zeus requires a custom plan. See [an example plan](https://github.com/tristandunn/pusher-fake-example/commit/add6dedad3b6da12cdac818d2fff3696a5d44738) for the configuration necessary.
53 |
54 | ##### Other
55 |
56 | ```ruby
57 | require "pusher-fake/support/base"
58 |
59 | # Reset the channels after each test:
60 | PusherFake::Channel.reset
61 | ```
62 |
63 | ### Development Environment
64 |
65 | In a Rails initializer, or any file executed during loading:
66 |
67 | ```ruby
68 | # Avoid running outside of development, if it's a global file.
69 | if Rails.env.development?
70 | # Set the Pusher configuration, if it's not done elsewhere.
71 | Pusher.app_id = "MY_TEST_ID"
72 | Pusher.key = "MY_TEST_KEY"
73 | Pusher.secret = "MY_TEST_SECRET"
74 |
75 | # Require the base file, which starts the socket and web servers.
76 | #
77 | # If you're including this file in different processes, you may want to add
78 | # another check or even possibly hard code the socket and web ports.
79 | require "pusher-fake/support/base"
80 | end
81 | ```
82 |
83 | If you're using Foreman, or something similar, you'll want to limit the fake to a single process:
84 |
85 | ```ruby
86 | if ENV["PUSHER_FAKE"]
87 | require "pusher-fake/support/base"
88 | end
89 | ```
90 |
91 | ```
92 | web: PUSHER_FAKE=1 bundle exec unicorn ...
93 | worker: bundle exec ...
94 | ```
95 |
96 | ### Clients
97 |
98 | If you're creating a `Pusher::Client` instance and wish to use the fake, you need to provide the options.
99 |
100 | ```ruby
101 | Pusher::Client.new({
102 | key: Pusher.key,
103 | app_id: Pusher.app_id,
104 | secret: Pusher.secret
105 | }.merge(PusherFake.configuration.web_options))
106 | ```
107 |
108 | ### Binary
109 |
110 | If you need to run the fake as a standalone service, perhaps when using Docker, there is a `pusher-fake` binary available.
111 |
112 | ```
113 | $ pusher-fake --help
114 | Usage: pusher-fake [options]
115 | -i, --id ID Use ID as the application ID for Pusher
116 | -k, --key KEY Use KEY as the key for Pusher
117 | -s, --secret SECRET Use SECRET as the secret token for Pusher
118 | --socket-host HOST Use HOST for the web socket server
119 | --socket-port PORT Use PORT for the web socket server
120 | -v, --[no-]verbose Run verbosely
121 | --web-host HOST Use HOST for the web server
122 | --web-port PORT Use PORT for the web server
123 | --webhooks URLS Use URLS for the webhooks
124 | ```
125 |
126 | Note that the binary does not support SSL options since they're forwarded to the server libraries. If you need SSL support in the binary, it's recommended you copy [the included binary](bin/pusher-fake) into your own project and set [the appropriate configuration](README.markdown#ssl) there instead.
127 |
128 | ## Configuration
129 |
130 | Note that the application ID, API key, and token are automatically set to the `Pusher` values when using an included support file.
131 |
132 | ### Settings
133 |
134 | Setting | Description
135 | ----------|------------
136 | app_id | The Pusher application ID.
137 | key | The Pusher API key.
138 | logger | An IO instance for verbose logging.
139 | secret | The Pusher API token.
140 | socket_options | Socket server options. See `EventMachine::WebSocket.start` for options.
141 | verbose | Enable verbose logging.
142 | web_options | Web server options. See `Thin::Server` for options.
143 | webhooks | Array of webhook URLs.
144 |
145 | ### Usage
146 |
147 | ```ruby
148 | # Single setting.
149 | PusherFake.configuration.verbose = true
150 |
151 | # Multiple settings.
152 | PusherFake.configure do |configuration|
153 | configuration.logger = Rails.logger
154 | configuration.verbose = true
155 | end
156 | ```
157 |
158 | ### SSL
159 |
160 | The WebSocket server is provided all `socket_options`, allowing you to set the `secure` and `tls_options` options to [create a secure server](https://github.com/igrigorik/em-websocket#secure-server).
161 |
162 | The web server passes all `web_options`, besides `host` and `port`, to the Thin backend via attribute writers, allowing you to set the `ssl` and `ssl_options` options.
163 |
164 | If you would like to force TLS for the JavaScript client, you can provide a `forceTLS` option:
165 |
166 | ```erb
167 | var instance = <%= PusherFake.javascript(forceTLS: true) %>;
168 | ```
169 |
170 | ## Examples
171 |
172 | * [pusher-fake-example](https://github.com/tristandunn/pusher-fake-example) - An example of using pusher-fake with RSpec to test a Rails application.
173 |
174 | ## License
175 |
176 | pusher-fake uses the MIT license. See LICENSE for more details.
177 |
--------------------------------------------------------------------------------
/spec/lib/pusher-fake/channel/presence_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | describe PusherFake::Channel::Presence do
6 | subject { described_class }
7 |
8 | let(:name) { "channel" }
9 |
10 | it "inherits from private channel" do
11 | expect(subject.ancestors).to include(PusherFake::Channel::Private)
12 | end
13 |
14 | it "creates an empty members hash" do
15 | channel = subject.new(name)
16 |
17 | expect(channel.members).to eq({})
18 | end
19 | end
20 |
21 | describe PusherFake::Channel::Presence, "#add" do
22 | subject { described_class.new(name) }
23 |
24 | let(:name) { "name" }
25 | let(:user_id) { "1234" }
26 | let(:connection) { instance_double(PusherFake::Connection, emit: nil) }
27 | let(:connections) { instance_double(Array, push: nil, length: 0) }
28 | let(:channel_data) { { user_id: user_id } }
29 | let(:authentication) { "auth" }
30 | let(:subscription_data) { { presence: { hash: {}, count: 1 } } }
31 |
32 | let(:data) do
33 | { auth: authentication,
34 | channel_data: MultiJson.dump(channel_data) }
35 | end
36 |
37 | before do
38 | allow(PusherFake::Webhook).to receive(:trigger)
39 | allow(MultiJson).to receive(:load).and_return(channel_data)
40 | allow(subject).to receive_messages(
41 | connections: connections,
42 | emit: nil,
43 | subscription_data: subscription_data
44 | )
45 | end
46 |
47 | it "authorizes the connection" do
48 | allow(subject).to receive(:authorized?).and_return(nil)
49 |
50 | subject.add(connection, data)
51 |
52 | expect(subject).to have_received(:authorized?).with(connection, data)
53 | end
54 |
55 | it "parses the channel_data when authorized" do
56 | allow(subject).to receive(:authorized?).and_return(true)
57 |
58 | subject.add(connection, data)
59 |
60 | expect(MultiJson).to have_received(:load)
61 | .with(data[:channel_data], symbolize_keys: true)
62 | end
63 |
64 | it "assigns the channel_data to the members hash for current connection" do
65 | allow(subject).to receive(:authorized?).and_return(true)
66 |
67 | subject.add(connection, data)
68 |
69 | expect(subject.members[connection]).to eq(channel_data)
70 | end
71 |
72 | it "notifies the channel of the new member when authorized" do
73 | allow(subject).to receive(:authorized?).and_return(true)
74 |
75 | subject.add(connection, data)
76 |
77 | expect(subject).to have_received(:emit)
78 | .with("pusher_internal:member_added", channel_data)
79 | end
80 |
81 | it "successfully subscribes the connection when authorized" do
82 | allow(subject).to receive(:authorized?).and_return(true)
83 |
84 | subject.add(connection, data)
85 |
86 | expect(connection).to have_received(:emit)
87 | .with("pusher_internal:subscription_succeeded",
88 | subscription_data, subject.name)
89 | end
90 |
91 | it "adds the connection to the channel when authorized" do
92 | allow(subject).to receive(:authorized?).and_return(true)
93 |
94 | subject.add(connection, data)
95 |
96 | expect(connections).to have_received(:push).with(connection)
97 | end
98 |
99 | it "triggers occupied webhook for first connection added when authorized" do
100 | allow(subject).to receive(:authorized?).and_return(true)
101 | allow(subject).to receive(:connections).and_call_original
102 |
103 | 2.times { subject.add(connection, data) }
104 |
105 | expect(PusherFake::Webhook).to have_received(:trigger)
106 | .with("channel_occupied", channel: name).once
107 | end
108 |
109 | it "triggers the member added webhook when authorized" do
110 | allow(subject).to receive(:authorized?).and_return(true)
111 |
112 | subject.add(connection, data)
113 |
114 | expect(PusherFake::Webhook).to have_received(:trigger)
115 | .with("member_added", channel: name, user_id: user_id).once
116 | end
117 |
118 | it "unsuccessfully subscribes the connection when not authorized" do
119 | allow(subject).to receive(:authorized?).and_return(false)
120 |
121 | subject.add(connection, data)
122 |
123 | expect(connection).to have_received(:emit)
124 | .with("pusher_internal:subscription_error", {}, subject.name)
125 | end
126 |
127 | it "does not trigger channel occupied webhook when not authorized" do
128 | allow(subject).to receive(:authorized?).and_return(false)
129 | allow(subject).to receive(:connections).and_call_original
130 |
131 | 2.times { subject.add(connection, data) }
132 |
133 | expect(PusherFake::Webhook).not_to have_received(:trigger)
134 | end
135 |
136 | it "does not trigger the member added webhook when not authorized" do
137 | allow(subject).to receive(:authorized?).and_return(false)
138 |
139 | subject.add(connection, data)
140 |
141 | expect(PusherFake::Webhook).not_to have_received(:trigger)
142 | end
143 | end
144 |
145 | describe PusherFake::Channel::Presence, "#remove" do
146 | subject { described_class.new(name) }
147 |
148 | let(:name) { "name" }
149 | let(:user_id) { "1234" }
150 | let(:connection) { double }
151 | let(:channel_data) { { user_id: user_id } }
152 |
153 | before do
154 | allow(PusherFake::Webhook).to receive(:trigger)
155 | allow(subject).to receive_messages(connections: [connection], emit: nil)
156 |
157 | subject.members[connection] = channel_data
158 | end
159 |
160 | it "removes the connection from the channel" do
161 | subject.remove(connection)
162 |
163 | expect(subject.connections).to be_empty
164 | end
165 |
166 | it "removes the connection from the members hash" do
167 | subject.remove(connection)
168 |
169 | expect(subject.members).not_to have_key(connection)
170 | end
171 |
172 | it "triggers the member removed webhook" do
173 | subject.remove(connection)
174 |
175 | expect(PusherFake::Webhook).to have_received(:trigger)
176 | .with("member_removed", channel: name, user_id: user_id).once
177 | end
178 |
179 | it "notifies the channel of the removed member" do
180 | subject.remove(connection)
181 |
182 | expect(subject).to have_received(:emit)
183 | .with("pusher_internal:member_removed", channel_data)
184 | end
185 | end
186 |
187 | describe PusherFake::Channel::Presence,
188 | "#remove, for an unsubscribed connection" do
189 | subject { described_class.new(name) }
190 |
191 | let(:name) { "name" }
192 | let(:user_id) { "1234" }
193 | let(:connection) { double }
194 | let(:channel_data) { { user_id: user_id } }
195 |
196 | before do
197 | allow(subject).to receive_messages(connections: [], emit: nil, trigger: nil)
198 | end
199 |
200 | it "does not raise an error" do
201 | expect do
202 | subject.remove(connection)
203 | end.not_to raise_error
204 | end
205 |
206 | it "does not trigger an event" do
207 | subject.remove(connection)
208 |
209 | expect(subject).not_to have_received(:trigger)
210 | .with("member_removed", channel: name, user_id: user_id)
211 | end
212 |
213 | it "does not emit an event" do
214 | subject.remove(connection)
215 |
216 | expect(subject).not_to have_received(:emit)
217 | .with("pusher_internal:member_removed", channel_data)
218 | end
219 | end
220 |
221 | describe PusherFake::Channel::Presence, "#subscription_data" do
222 | subject { described_class.new("name") }
223 |
224 | let(:one) { { user_id: 1, name: "Bob" } }
225 | let(:two) { { user_id: 2, name: "Beau" } }
226 | let(:data) { subject.subscription_data }
227 | let(:members) { { double => one } }
228 |
229 | before do
230 | allow(subject).to receive(:members).and_return(members)
231 | end
232 |
233 | it "returns hash with presence information" do
234 | expect(data).to eq(presence: {
235 | hash: { one[:user_id] => one[:user_info] },
236 | count: 1
237 | })
238 | end
239 |
240 | it "handles multiple members" do
241 | members[double] = two
242 |
243 | expect(data[:presence][:count]).to eq(2)
244 | expect(data[:presence][:hash]).to eq(
245 | one[:user_id] => one[:user_info], two[:user_id] => two[:user_info]
246 | )
247 | end
248 | end
249 |
--------------------------------------------------------------------------------
/CHANGELOG.markdown:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## Unreleased
4 |
5 | * Add support for Ruby 3.4. (Tristan Dunn)
6 | * Drop support for Ruby 3.1. (Tristan Dunn)
7 | * Update development dependencies. (Tristan Dunn)
8 | * Remove the sinatra dependency. (Tristan Dunn)
9 |
10 | ## 6.0.0 — March 21st, 2024
11 |
12 | * Add support for Ruby 3.3. (Tristan Dunn)
13 | * Drop support for Ruby 3.0. (Tristan Dunn)
14 | * Update development dependencies. (Tristan Dunn)
15 |
16 | ## 5.0.0 — March 27th, 2023
17 |
18 | * Add support for Ruby 3.2. (Tristan Dunn)
19 | * Drop support for Ruby 2.7. (Tristan Dunn)
20 | * Update development dependencies. (Tristan Dunn)
21 |
22 | ## 4.2.0 — August 1st, 2022
23 |
24 | * Correctly handle signal trap chains. (Christian Prescott, Tristan Dunn)
25 | * Loosen the thin version restriction. (Tristan Dunn)
26 | * Update development dependencies. (Tristan Dunn)
27 |
28 | ## 4.1.0 — April 26th, 2022
29 |
30 | * Add support for cache channels. (Tristan Dunn)
31 |
32 | ## 4.0.0 — April 5th, 2022
33 |
34 | * Add support for Ruby 3.1. (Tristan Dunn)
35 | * Drop support for Ruby 2.6. (Tristan Dunn)
36 | * Update the Pusher JS client to version 7.0.6. (Tristan Dunn)
37 | * Require MFA for privileged operations on RubyGems. (Tristan Dunn)
38 | * Update development and test dependencies. (Tristan Dunn)
39 | * Replace coveralls with simplecov. (Tristan Dunn)
40 |
41 | ## 3.0.1 — September 10th, 2021
42 |
43 | * Handle `StringIO` being assigned to `$stdout`. (Tristan Dunn)
44 | * Update development dependencies. (Tristan Dunn)
45 |
46 | ## 3.0.0 — August 10th, 2021
47 |
48 | * Add support for Ruby 3.0. (Tristan Dunn)
49 | * Drop support for Ruby 2.4 and 2.5. (Tristan Dunn)
50 | * Update development dependencies. (Tristan Dunn)
51 |
52 | ## 2.2.0 — September 16th, 2020
53 |
54 | * Enable support for Windows by using a thread instead of fork. (Ian Clarkson)
55 | * Update development dependencies. (Tristan Dunn)
56 |
57 | ## 2.1.0 — September 5th, 2020
58 |
59 | * Remove deprecated Cucumber file. (Tristan Dunn)
60 | * Update the Pusher JS client to version 7.0.0. (Tristan Dunn)
61 | * Update development and test dependencies. (Tristan Dunn)
62 |
63 | ## 2.0.0 — March 31st, 2020
64 |
65 | * Add support for Ruby 2.7. (Tristan Dunn)
66 | * Fix connection ID for Ruby 2.7. (Mark Thorn)
67 | * Drop support for Ruby 2.2 and 2.3. (Tristan Dunn)
68 | * Update development and test dependencies. (Tristan Dunn)
69 |
70 | ## 1.12.0 — April 2nd, 2019
71 |
72 | * Add webhooks options to binary. (nytai)
73 | * Update development and test dependencies. (Tristan Dunn)
74 |
75 | ## 1.11.0 — November 17th, 2018
76 |
77 | * Add statistics configuration and disable by default. (Tristan Dunn)
78 | * Update development and test dependencies. (Tristan Dunn)
79 |
80 | ## 1.10.0 — September 5th, 2018
81 |
82 | * Ensure the application ID is a string. (Craig McNamara)
83 | * Fix a typo in the README. (Jouke Waleson)
84 | * Update development and test dependencies. (Tristan Dunn)
85 |
86 | ## 1.9.0 — November 1st, 2017
87 |
88 | * Warn when the library is not required before a support file. (Tristan Dunn)
89 | * Update the Pusher JS client to version 4.2.1. (Tristan Dunn)
90 | * Update development and test dependencies. (Tristan Dunn)
91 |
92 | ## 1.8.0 — March 13th, 2017
93 |
94 | * Add a `pusher-fake` binary to run the servers. (Tristan Dunn)
95 | * Update development and test dependencies. (Tristan Dunn)
96 |
97 | ## 1.7.0 — November 4th, 2016
98 |
99 | * Add support for batch events. (Tyler Hogan)
100 | * Update the Pusher JS client to version 3.2.1. (Tristan Dunn)
101 | * Update development and test dependencies. (Tristan Dunn)
102 |
103 | ## 1.6.0 — July 31st, 2016
104 |
105 | * Update development and test dependencies. (Tristan Dunn)
106 | * Update the Pusher JS client to version 3.1.0. (Tristan Dunn)
107 |
108 | ## 1.5.0 — February 12th, 2016
109 |
110 | * Warn when Pusher configuration is not set. (Tristan Dunn)
111 | * Update the Pusher JS client to version 3.0.0. (Tristan Dunn)
112 | * Update development and test dependencies. (Tristan Dunn)
113 |
114 | ## 1.4.0 — May 20th, 2015
115 |
116 | * Update development and test dependencies. (Tristan Dunn)
117 |
118 | ## 1.3.0 — February 16th, 2015
119 |
120 | * Only enable the WebSocket transport. (Tristan Dunn)
121 | * Update the Pusher JS client to version 2.2.4. (Tristan Dunn)
122 | * Update development and test dependencies. (Tristan Dunn)
123 |
124 | ## 1.2.0 — August 9th, 2014
125 |
126 | * Default socket and web ports to available ports. (Tristan Dunn)
127 | * Update development dependencies. (Tristan Dunn)
128 |
129 | ## 1.1.0 — July 22nd, 2014
130 |
131 | * Add support for frameworks besides Cucumber. (Tristan Dunn)
132 | * Update development dependencies. (Tristan Dunn)
133 |
134 | ## 1.0.1 — June 9th, 2014
135 |
136 | * Update the Pusher JS client to version 2.2.2. (Tristan Dunn)
137 |
138 | ## 1.0.0 — June 7th, 2014
139 |
140 | * Double encode JSON data to match Pusher. (Tristan Dunn, Adrien Jarthon)
141 | * Treat socket_id as a string to match Pusher. (Tristan Dunn, Adrien Jarthon)
142 | * Trigger client_event webhooks. (Tristan Dunn).
143 | * Add verbose logging. (Tristan Dunn)
144 | * Miscellaneous clean up. (Tristan Dunn)
145 | * Update the Pusher JS client to version 2.2.1. (Tristan Dunn)
146 | * Update dependencies. (Tristan Dunn)
147 |
148 | ## 0.14.0 — February 19th, 2014
149 |
150 | * Handle pusher:ping events from client. (Mark Thorn)
151 | * Avoid issue when removing unsubscribed connection from presence channel. (Mark Thorn)
152 | * Add initial support for verbose logging. (Tristan Dunn)
153 | * Change coveralls to be a test dependency. (Tristan Dunn)
154 | * Miscellaneous clean up. (Tristan Dunn)
155 | * Update dependencies. (Tristan Dunn)
156 |
157 | ## 0.13.0 — January 15th, 2014
158 |
159 | * Remove deprecated configuration options. (Tristan Dunn)
160 | * Update the Pusher JS client to version 2.1.6. (Tristan Dunn)
161 | * Miscellaneous clean up. (Tristan Dunn)
162 | * Update dependencies. (Tristan Dunn)
163 |
164 | ## 0.12.0 — December 21st, 2013
165 |
166 | * Update the Pusher JS client to version 2.1.5. (Tristan Dunn)
167 | * Update dependencies. (Tristan Dunn, Matthieu Aussaguel)
168 |
169 | ## 0.11.0 — October 30th, 2013
170 |
171 | * Support setting custom options on the socket and web server. (Tristan Dunn)
172 | * Update the Pusher JS client to version 2.1.3. (Tristan Dunn)
173 | * Update dependencies. (Tristan Dunn)
174 |
175 | ## 0.10.0 — August 26th, 2013
176 |
177 | * Resolve dependency issue. (Tristan Dunn)
178 | * Update the Pusher JS client to version 2.1.2. (Tristan Dunn)
179 | * Update dependencies. (Tristan Dunn)
180 |
181 | ## 0.9.0 — June 6th, 2013
182 |
183 | * Use fuzzy version requirement for runtime dependencies. (Patrick Van Stee)
184 | * Update dependencies. (Tristan Dunn)
185 |
186 | ## 0.8.0 — May 11th, 2013
187 |
188 | * Update dependencies. (Tristan Dunn)
189 |
190 | ## 0.7.0 — February 25th, 2013
191 |
192 | * Raise and log on unknown server paths. (Tristan Dunn)
193 | * Update dependencies. (Tristan Dunn)
194 |
195 | ## 0.6.0 — January 23rd, 2013
196 |
197 | * Add a file for easily starting the fake server in Cucumber. (Tristan Dunn)
198 | * Add convenience method for the JS to override Pusher client configuration. (Tristan Dunn)
199 | * Update dependencies. (Tristan Dunn)
200 |
201 | ## 0.5.0 — January 21st, 2013
202 |
203 | * Support channel, channels, and user REST API endpoints. (Tristan Dunn)
204 | * Update dependencies. (Tristan Dunn)
205 |
206 | ## 0.4.0 — December 14th, 2012
207 |
208 | * Support excluding recipients. (Tristan Dunn)
209 | * Don't deliver client events to the originator of the event. (Thomas Walpole)
210 | * Update dependencies. (Tristan Dunn)
211 |
212 | ## 0.3.0 — December 12th, 2012
213 |
214 | * Support triggering webhooks. (Tristan Dunn)
215 | * Update dependencies. (Tristan Dunn)
216 |
217 | ## 0.2.0 — November 28th, 2012
218 |
219 | * Replace ruby-hmac with openssl. (Sergey Nartimov)
220 | * Use multi_json instead of yajl-ruby. (Sergey Nartimov)
221 | * Update dependencies. (Tristan Dunn)
222 |
223 | ## 0.1.5 — November 12th, 2012
224 |
225 | * Use the new Pusher event format. (Tristan Dunn)
226 | * Upgraded the Pusher JS client to version 1.12.5. (Tristan Dunn)
227 | * Update dependencies. (Tristan Dunn)
228 |
229 | ## 0.1.4 — July 15th, 2012
230 |
231 | * Upgraded the Pusher JS client to version 1.12.1. (Tristan Dunn)
232 | * Improve documentation. (Tristan Dunn)
233 | * Update dependencies. (Tristan Dunn)
234 |
235 | ## 0.1.3 — July 9th, 2012
236 |
237 | * Ensure the server returns a valid JSON response. (Marko Anastasov)
238 | * Handle channels not being defined when attempting to remove a connection. (Tristan Dunn)
239 | * Update dependencies. (Tristan Dunn)
240 |
241 | ## 0.1.2 — April 19th, 2012
242 |
243 | * Make subscription_data match Pusher v1.11 format. (Thomas Walpole)
244 | * Miscellaneous clean up. (Tristan Dunn)
245 |
246 | ## 0.1.1 — March 29th, 2012
247 |
248 | * Added support for parametric app_id in configuration and application server. (Alessandro Morandi)
249 | * Upgraded the Pusher JS client to version 1.11.2. (Tristan Dunn)
250 | * Added Rake as a development dependency. (Tristan Dunn)
251 | * Miscellaneous clean up. (Tristan Dunn)
252 |
253 | ## 0.1.0 — March 14th, 2012
254 |
255 | * Initial release.
256 |
--------------------------------------------------------------------------------
/spec/lib/pusher-fake/connection_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | shared_examples_for "#process" do
6 | subject { PusherFake::Connection.new(double) }
7 |
8 | let(:json) { double }
9 |
10 | before do
11 | allow(PusherFake).to receive(:log)
12 | allow(MultiJson).to receive(:load).and_return(message)
13 | end
14 |
15 | it "parses the JSON data" do
16 | subject.process(json)
17 |
18 | expect(MultiJson).to have_received(:load).with(json, symbolize_keys: true)
19 | end
20 |
21 | it "logs receiving the event" do
22 | subject.process(json)
23 |
24 | expect(PusherFake).to have_received(:log)
25 | .with("RECV #{subject.id}: #{message}")
26 | end
27 | end
28 |
29 | describe PusherFake::Connection do
30 | subject { described_class }
31 |
32 | let(:socket) { double }
33 |
34 | it "assigns the provided socket" do
35 | connection = subject.new(socket)
36 |
37 | expect(connection.socket).to eq(socket)
38 | end
39 | end
40 |
41 | describe PusherFake::Connection, "#emit" do
42 | subject { described_class.new(socket) }
43 |
44 | let(:data) { { some: "data", good: true } }
45 | let(:json) { MultiJson.dump(message) }
46 | let(:event) { "name" }
47 | let(:channel) { "channel" }
48 | let(:message) { { event: event, data: MultiJson.dump(data) } }
49 | let(:channel_json) { MultiJson.dump(message.merge(channel: channel)) }
50 |
51 | let(:socket) do
52 | instance_double(EventMachine::WebSocket::Connection, send: nil)
53 | end
54 |
55 | before do
56 | allow(PusherFake).to receive(:log)
57 | end
58 |
59 | it "sends the event to the socket as JSON" do
60 | subject.emit(event, data)
61 |
62 | expect(socket).to have_received(:send).with(json)
63 | end
64 |
65 | it "sets a channel when provided" do
66 | subject.emit(event, data, channel)
67 |
68 | expect(socket).to have_received(:send).with(channel_json)
69 | end
70 |
71 | it "logs sending the event" do
72 | subject.emit(event, data)
73 |
74 | expect(PusherFake).to have_received(:log)
75 | .with("SEND #{subject.id}: #{message}")
76 | end
77 | end
78 |
79 | describe PusherFake::Connection, "#establish" do
80 | subject { described_class.new(socket) }
81 |
82 | let(:socket) { double }
83 |
84 | before do
85 | allow(subject).to receive(:emit)
86 | end
87 |
88 | it "emits the connection established event with the connection ID" do
89 | subject.establish
90 |
91 | expect(subject).to have_received(:emit)
92 | .with("pusher:connection_established",
93 | socket_id: subject.id, activity_timeout: 120)
94 | end
95 | end
96 |
97 | describe PusherFake::Connection, "#id" do
98 | subject { described_class.new(socket) }
99 |
100 | let(:id) { "1234.567" }
101 | let(:socket) { instance_double(Object, object_id: 1_234_567) }
102 |
103 | it "returns the object ID of the socket" do
104 | expect(subject.id).to eq(id)
105 | end
106 | end
107 |
108 | describe PusherFake::Connection, "#process, with a subscribe event" do
109 | it_behaves_like "#process" do
110 | let(:data) { { channel: name, auth: "auth" } }
111 | let(:name) { "channel" }
112 | let(:message) { { event: "pusher:subscribe", data: data } }
113 |
114 | let(:channel) do
115 | instance_double(PusherFake::Channel::Presence, add: nil)
116 | end
117 |
118 | before do
119 | allow(PusherFake::Channel).to receive(:factory).and_return(channel)
120 | end
121 |
122 | it "creates a channel from the event data" do
123 | subject.process(json)
124 |
125 | expect(PusherFake::Channel).to have_received(:factory).with(name)
126 | end
127 |
128 | it "attempts to add the connection to the channel" do
129 | subject.process(json)
130 |
131 | expect(channel).to have_received(:add).with(subject, data)
132 | end
133 | end
134 | end
135 |
136 | describe PusherFake::Connection, "#process, with an unsubscribe event" do
137 | it_behaves_like "#process" do
138 | let(:name) { "channel" }
139 | let(:message) { { event: "pusher:unsubscribe", channel: name } }
140 |
141 | let(:channel) do
142 | instance_double(PusherFake::Channel::Presence, remove: nil)
143 | end
144 |
145 | before do
146 | allow(PusherFake::Channel).to receive(:factory).and_return(channel)
147 | end
148 |
149 | it "creates a channel from the event data" do
150 | subject.process(json)
151 |
152 | expect(PusherFake::Channel).to have_received(:factory).with(name)
153 | end
154 |
155 | it "removes the connection from the channel" do
156 | subject.process(json)
157 |
158 | expect(channel).to have_received(:remove).with(subject)
159 | end
160 | end
161 | end
162 |
163 | describe PusherFake::Connection, "#process, with a ping event" do
164 | it_behaves_like "#process" do
165 | let(:message) { { event: "pusher:ping", data: {} } }
166 |
167 | before do
168 | allow(subject).to receive(:emit)
169 | allow(PusherFake::Channel).to receive(:factory)
170 | end
171 |
172 | it "does not create a channel" do
173 | subject.process(json)
174 |
175 | expect(PusherFake::Channel).not_to have_received(:factory)
176 | end
177 |
178 | it "emits a pong event" do
179 | subject.process(json)
180 |
181 | expect(subject).to have_received(:emit).with("pusher:pong")
182 | end
183 | end
184 | end
185 |
186 | describe PusherFake::Connection, "#process, with a client event" do
187 | it_behaves_like "#process" do
188 | let(:data) { {} }
189 | let(:name) { "channel" }
190 | let(:event) { "client-hello-world" }
191 | let(:message) { { event: event, data: data, channel: name } }
192 |
193 | let(:channel) do
194 | instance_double(PusherFake::Channel::Private,
195 | emit: nil, includes?: nil, is_a?: true)
196 | end
197 |
198 | before do
199 | allow(subject).to receive(:trigger)
200 | allow(PusherFake::Channel).to receive(:factory).and_return(channel)
201 | end
202 |
203 | it "creates a channel from the event data" do
204 | subject.process(json)
205 |
206 | expect(PusherFake::Channel).to have_received(:factory).with(name)
207 | end
208 |
209 | it "ensures the channel is private" do
210 | subject.process(json)
211 |
212 | expect(channel).to have_received(:is_a?)
213 | .with(PusherFake::Channel::Private)
214 | end
215 |
216 | it "checks if the connection is in the channel" do
217 | subject.process(json)
218 |
219 | expect(channel).to have_received(:includes?).with(subject)
220 | end
221 |
222 | it "emits the event when the connection is in the channel" do
223 | allow(channel).to receive(:includes?).and_return(true)
224 |
225 | subject.process(json)
226 |
227 | expect(channel).to have_received(:emit)
228 | .with(event, data, socket_id: subject.id)
229 | end
230 |
231 | it "does not emit the event when the channel is not private" do
232 | allow(channel).to receive_messages(is_a?: false, includes?: true)
233 |
234 | subject.process(json)
235 |
236 | expect(channel).not_to have_received(:emit)
237 | end
238 |
239 | it "does not emit the event when the connection is not in the channel" do
240 | allow(channel).to receive(:includes?).and_return(false)
241 |
242 | subject.process(json)
243 |
244 | expect(channel).not_to have_received(:emit)
245 | end
246 | end
247 | end
248 |
249 | describe PusherFake::Connection,
250 | "#process, with a client event trigger a webhook" do
251 | it_behaves_like "#process" do
252 | let(:data) { { example: "data" } }
253 | let(:name) { "channel" }
254 | let(:event) { "client-hello-world" }
255 | let(:user_id) { 1 }
256 | let(:members) { { subject => { user_id: user_id } } }
257 | let(:message) { { event: event, channel: name } }
258 | let(:options) { { channel: name, event: event, socket_id: subject.id } }
259 |
260 | let(:channel) do
261 | instance_double(PusherFake::Channel::Presence,
262 | name: name, emit: nil, includes?: nil)
263 | end
264 |
265 | before do
266 | allow(channel).to receive(:trigger)
267 | allow(channel).to receive(:includes?).with(subject).and_return(true)
268 | allow(channel).to receive(:is_a?)
269 | .with(PusherFake::Channel::Private).and_return(true)
270 | allow(channel).to receive(:is_a?)
271 | .with(PusherFake::Channel::Presence).and_return(false)
272 |
273 | # NOTE: Hack to avoid race condition in unit tests.
274 | allow(Thread).to receive(:new).and_yield
275 |
276 | allow(PusherFake::Channel).to receive(:factory).and_return(channel)
277 | end
278 |
279 | it "triggers the client event webhook" do
280 | subject.process(json)
281 |
282 | expect(channel).to have_received(:trigger)
283 | .with("client_event", options).once
284 | end
285 |
286 | it "includes data in event when present" do
287 | message[:data] = data
288 |
289 | subject.process(json)
290 |
291 | expect(channel).to have_received(:trigger)
292 | .with("client_event", options.merge(data: MultiJson.dump(data))).once
293 | end
294 |
295 | it "includes user ID in event when on a presence channel" do
296 | allow(channel).to receive_messages(is_a?: true, members: members)
297 |
298 | subject.process(json)
299 |
300 | expect(channel).to have_received(:trigger)
301 | .with("client_event", options.merge(user_id: user_id)).once
302 | end
303 | end
304 | end
305 |
306 | describe PusherFake::Connection, "#process, with an unknown event" do
307 | it_behaves_like "#process" do
308 | let(:data) { {} }
309 | let(:name) { "channel" }
310 | let(:event) { "hello-world" }
311 | let(:channel) { instance_double(PusherFake::Channel::Public, emit: nil) }
312 | let(:message) { { event: event, data: data, channel: name } }
313 |
314 | before do
315 | allow(PusherFake::Channel).to receive(:factory).and_return(channel)
316 | end
317 |
318 | it "does not create a channel" do
319 | subject.process(json)
320 |
321 | expect(PusherFake::Channel).not_to have_received(:factory)
322 | end
323 |
324 | it "does not emit the event" do
325 | subject.process(json)
326 |
327 | expect(channel).not_to have_received(:emit)
328 | end
329 | end
330 | end
331 |
--------------------------------------------------------------------------------
/spec/lib/pusher-fake/server/application_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "spec_helper"
4 |
5 | shared_examples_for "an API request" do
6 | subject { PusherFake::Server::Application }
7 |
8 | let(:hash) { double }
9 | let(:string) { double }
10 | let(:request) { instance_double(Rack::Request, path: path) }
11 | let(:response) { double }
12 | let(:environment) { double }
13 |
14 | before do
15 | allow(response).to receive(:finish).and_return(response)
16 |
17 | allow(MultiJson).to receive(:dump).and_return(string)
18 | allow(Rack::Request).to receive(:new).and_return(request)
19 | allow(Rack::Response).to receive(:new).and_return(response)
20 | end
21 |
22 | it "creates a request" do
23 | subject.call(environment)
24 |
25 | expect(Rack::Request).to have_received(:new).with(environment)
26 | end
27 |
28 | it "dumps the response hash to JSON" do
29 | subject.call(environment)
30 |
31 | expect(MultiJson).to have_received(:dump).with(hash)
32 | end
33 |
34 | it "creates a Rack response with the response JSON" do
35 | subject.call(environment)
36 |
37 | expect(Rack::Response).to have_received(:new).with(string)
38 | end
39 |
40 | it "finishes the response" do
41 | subject.call(environment)
42 |
43 | expect(response).to have_received(:finish).with(no_args)
44 | end
45 |
46 | it "returns the response" do
47 | result = subject.call(environment)
48 |
49 | expect(result).to eq(response)
50 | end
51 | end
52 |
53 | describe PusherFake::Server::Application, ".call, with a numeric ID" do
54 | it_behaves_like "an API request" do
55 | let(:id) { Time.now.to_i }
56 | let(:path) { "/apps/#{id}/events" }
57 |
58 | before do
59 | PusherFake.configuration.app_id = id
60 |
61 | allow(subject).to receive(:events).and_return(hash)
62 | end
63 |
64 | after do
65 | PusherFake.configuration.app_id = "PUSHER_APP_ID"
66 | end
67 | end
68 | end
69 |
70 | describe PusherFake::Server::Application, ".call, for triggering events" do
71 | it_behaves_like "an API request" do
72 | let(:id) { PusherFake.configuration.app_id }
73 | let(:path) { "/apps/#{id}/events" }
74 |
75 | before do
76 | allow(subject).to receive(:events).and_return(hash)
77 | end
78 |
79 | it "emits events" do
80 | subject.call(environment)
81 |
82 | expect(subject).to have_received(:events).with(request)
83 | end
84 | end
85 | end
86 |
87 | describe PusherFake::Server::Application,
88 | ".call, for triggering batch events" do
89 | it_behaves_like "an API request" do
90 | let(:id) { PusherFake.configuration.app_id }
91 | let(:path) { "/apps/#{id}/batch_events" }
92 |
93 | before do
94 | allow(subject).to receive(:batch_events).and_return(hash)
95 | end
96 |
97 | it "emits batch events" do
98 | subject.call(environment)
99 |
100 | expect(subject).to have_received(:batch_events).with(request)
101 | end
102 | end
103 | end
104 |
105 | describe PusherFake::Server::Application,
106 | ".call, retrieving occupied channels" do
107 | it_behaves_like "an API request" do
108 | let(:id) { PusherFake.configuration.app_id }
109 | let(:path) { "/apps/#{id}/channels" }
110 |
111 | before do
112 | allow(subject).to receive(:channels).and_return(hash)
113 | end
114 |
115 | it "filters the occupied channels" do
116 | subject.call(environment)
117 |
118 | expect(subject).to have_received(:channels).with(request)
119 | end
120 | end
121 | end
122 |
123 | describe PusherFake::Server::Application, ".call, with unknown path" do
124 | subject { described_class }
125 |
126 | let(:path) { "/apps/fake/events" }
127 | let(:request) { instance_double(Rack::Request, path: path) }
128 | let(:message) { "Unknown path: #{path}" }
129 | let(:response) { double }
130 | let(:environment) { double }
131 |
132 | before do
133 | allow(response).to receive(:finish).and_return(response)
134 |
135 | allow(Rack::Request).to receive(:new).and_return(request)
136 | allow(Rack::Response).to receive(:new).and_return(response)
137 | end
138 |
139 | it "creates a request" do
140 | subject.call(environment)
141 |
142 | expect(Rack::Request).to have_received(:new).with(environment)
143 | end
144 |
145 | it "creates a Rack response with the error message" do
146 | subject.call(environment)
147 |
148 | expect(Rack::Response).to have_received(:new).with(message, 400)
149 | end
150 |
151 | it "finishes the response" do
152 | subject.call(environment)
153 |
154 | expect(response).to have_received(:finish).with(no_args)
155 | end
156 |
157 | it "returns the response" do
158 | result = subject.call(environment)
159 |
160 | expect(result).to eq(response)
161 | end
162 | end
163 |
164 | describe PusherFake::Server::Application, ".call, raising an error" do
165 | subject { described_class }
166 |
167 | let(:id) { PusherFake.configuration.app_id }
168 | let(:path) { "/apps/#{id}/channels" }
169 | let(:message) { "Example error message." }
170 | let(:request) { instance_double(Rack::Request, path: path) }
171 | let(:response) { double }
172 | let(:environment) { double }
173 |
174 | before do
175 | allow(subject).to receive(:channels).and_raise(message)
176 |
177 | allow(response).to receive(:finish).and_return(response)
178 |
179 | allow(Rack::Request).to receive(:new).and_return(request)
180 | allow(Rack::Response).to receive(:new).and_return(response)
181 | end
182 |
183 | it "creates a request" do
184 | subject.call(environment)
185 |
186 | expect(Rack::Request).to have_received(:new).with(environment)
187 | end
188 |
189 | it "creates a Rack response with the error message" do
190 | subject.call(environment)
191 |
192 | expect(Rack::Response).to have_received(:new).with(message, 400)
193 | end
194 |
195 | it "finishes the response" do
196 | subject.call(environment)
197 |
198 | expect(response).to have_received(:finish).with(no_args)
199 | end
200 |
201 | it "returns the response" do
202 | result = subject.call(environment)
203 |
204 | expect(result).to eq(response)
205 | end
206 | end
207 |
208 | describe PusherFake::Server::Application, ".events" do
209 | subject { described_class }
210 |
211 | let(:body) { instance_double(StringIO, read: event_json) }
212 | let(:data) { { "example" => "data" } }
213 | let(:name) { "event-name" }
214 | let(:request) { instance_double(Rack::Request, body: body) }
215 | let(:channels) { %w(channel-1 channel-2) }
216 | let(:channel_1) { instance_double(PusherFake::Channel::Public, emit: true) }
217 | let(:channel_2) { instance_double(PusherFake::Channel::Public, emit: true) }
218 | let(:data_json) { data.to_json }
219 | let(:socket_id) { double }
220 | let(:event_json) { double }
221 |
222 | let(:event) do
223 | {
224 | "channels" => channels,
225 | "name" => name,
226 | "data" => data_json,
227 | "socket_id" => socket_id
228 | }
229 | end
230 |
231 | before do
232 | allow(MultiJson).to receive(:load).with(event_json).and_return(event)
233 | allow(MultiJson).to receive(:load).with(data_json).and_return(data)
234 | allow(PusherFake::Channel).to receive(:factory)
235 | .with(channels[0]).and_return(channel_1)
236 | allow(PusherFake::Channel).to receive(:factory)
237 | .with(channels[1]).and_return(channel_2)
238 | end
239 |
240 | it "parses the request body as JSON" do
241 | subject.events(request)
242 |
243 | expect(MultiJson).to have_received(:load).with(event_json)
244 | end
245 |
246 | it "parses the event data as JSON" do
247 | subject.events(request)
248 |
249 | expect(MultiJson).to have_received(:load).with(data_json)
250 | end
251 |
252 | it "handles invalid JSON for event data" do
253 | event["data"] = data = "fake"
254 |
255 | allow(MultiJson).to receive(:load)
256 | .with(data).and_raise(MultiJson::LoadError)
257 |
258 | expect { subject.events(request) }.not_to raise_error
259 | end
260 |
261 | it "creates channels by name" do
262 | subject.events(request)
263 |
264 | channels.each do |channel|
265 | expect(PusherFake::Channel).to have_received(:factory).with(channel)
266 | end
267 | end
268 |
269 | it "emits the event to the channels" do
270 | subject.events(request)
271 |
272 | expect(channel_1).to have_received(:emit)
273 | .with(name, data, socket_id: socket_id)
274 | expect(channel_2).to have_received(:emit)
275 | .with(name, data, socket_id: socket_id)
276 | end
277 | end
278 |
279 | describe PusherFake::Server::Application, ".batch_events" do
280 | subject { described_class }
281 |
282 | let(:body) { instance_double(StringIO, read: event_json) }
283 | let(:data) { { "example" => "data" } }
284 | let(:name) { "event-name" }
285 | let(:request) { instance_double(Rack::Request, body: body) }
286 | let(:channels) { %w(channel-1 channel-2) }
287 | let(:channel_1) { instance_double(PusherFake::Channel::Public, emit: true) }
288 | let(:channel_2) { instance_double(PusherFake::Channel::Public, emit: true) }
289 | let(:data_json) { data.to_json }
290 | let(:socket_id) { double }
291 | let(:event_json) { double }
292 |
293 | let(:batch) do
294 | {
295 | "batch" => [{
296 | "channels" => channels,
297 | "name" => name,
298 | "data" => data_json,
299 | "socket_id" => socket_id
300 | }]
301 | }
302 | end
303 |
304 | before do
305 | allow(MultiJson).to receive(:load).with(event_json).and_return(batch)
306 | allow(MultiJson).to receive(:load).with(data_json).and_return(data)
307 | allow(PusherFake::Channel).to receive(:factory)
308 | .with(channels[0]).and_return(channel_1)
309 | allow(PusherFake::Channel).to receive(:factory)
310 | .with(channels[1]).and_return(channel_2)
311 | end
312 |
313 | it "parses the request body as JSON" do
314 | subject.batch_events(request)
315 |
316 | expect(MultiJson).to have_received(:load).with(event_json)
317 | end
318 |
319 | it "parses the event data as JSON" do
320 | subject.batch_events(request)
321 |
322 | expect(MultiJson).to have_received(:load).with(data_json)
323 | end
324 |
325 | it "handles invalid JSON for event data" do
326 | batch["batch"].first["data"] = data = "fake"
327 |
328 | allow(MultiJson).to receive(:load)
329 | .with(data).and_raise(MultiJson::LoadError)
330 |
331 | expect { subject.batch_events(request) }.not_to raise_error
332 | end
333 |
334 | it "creates channels by name" do
335 | subject.batch_events(request)
336 |
337 | channels.each do |channel|
338 | expect(PusherFake::Channel).to have_received(:factory).with(channel)
339 | end
340 | end
341 |
342 | it "emits the event to the channels" do
343 | subject.batch_events(request)
344 |
345 | expect(channel_1).to have_received(:emit)
346 | .with(name, data, socket_id: socket_id)
347 | expect(channel_2).to have_received(:emit)
348 | .with(name, data, socket_id: socket_id)
349 | end
350 | end
351 |
352 | describe PusherFake::Server::Application, ".channels, requesting all" do
353 | subject { described_class }
354 |
355 | let(:request) { instance_double(Rack::Request, params: {}) }
356 | let(:channels) { { "channel-1" => double, "channel-2" => double } }
357 |
358 | before do
359 | allow(PusherFake::Channel).to receive(:channels).and_return(channels)
360 | end
361 |
362 | it "returns a hash of all the channels" do
363 | hash = subject.channels(request)
364 |
365 | expect(hash).to eq(channels: { "channel-1" => {}, "channel-2" => {} })
366 | end
367 | end
368 |
369 | describe PusherFake::Server::Application,
370 | ".channels, requesting channels with a filter" do
371 | subject { described_class }
372 |
373 | let(:params) { { "filter_by_prefix" => "public-" } }
374 | let(:request) { instance_double(Rack::Request, params: params) }
375 | let(:channels) { { "public-1" => double, "presence-1" => double } }
376 |
377 | before do
378 | allow(PusherFake::Channel).to receive(:channels).and_return(channels)
379 | end
380 |
381 | it "returns a hash of the channels matching the filter" do
382 | hash = subject.channels(request)
383 |
384 | expect(hash).to eq(channels: { "public-1" => {} })
385 | end
386 | end
387 |
388 | describe PusherFake::Server::Application,
389 | ".channels, requesting user count for channels with a filter" do
390 | subject { described_class }
391 |
392 | let(:request) { instance_double(Rack::Request, params: params) }
393 | let(:channels) { { "public-1" => double, "presence-1" => channel } }
394 |
395 | let(:channel) do
396 | instance_double(PusherFake::Channel::Presence,
397 | connections: [double, double])
398 | end
399 |
400 | let(:params) do
401 | { "filter_by_prefix" => "presence-", "info" => "user_count" }
402 | end
403 |
404 | before do
405 | allow(PusherFake::Channel).to receive(:channels).and_return(channels)
406 | end
407 |
408 | it "returns hash of channels matching the filter and includes user count" do
409 | hash = subject.channels(request)
410 |
411 | expect(hash).to eq(channels: { "presence-1" => { user_count: 2 } })
412 | end
413 | end
414 |
415 | describe PusherFake::Server::Application,
416 | ".channels, requesting all channels with no channels occupied" do
417 | subject { described_class }
418 |
419 | let(:request) { instance_double(Rack::Request, params: {}) }
420 |
421 | before do
422 | allow(PusherFake::Channel).to receive(:channels).and_return({})
423 | end
424 |
425 | it "returns a hash of no channels" do
426 | hash = subject.channels(request)
427 |
428 | expect(hash).to eq(channels: {})
429 | end
430 | end
431 |
432 | describe PusherFake::Server::Application,
433 | ".channels, requesting a user count on a non-presence channel" do
434 | subject { described_class }
435 |
436 | let(:params) { { "filter_by_prefix" => "public-", "info" => "user_count" } }
437 | let(:request) { instance_double(Rack::Request, params: params) }
438 |
439 | it "raises an error" do
440 | expect do
441 | subject.channels(request)
442 | end.to raise_error(subject::CHANNEL_FILTER_ERROR)
443 | end
444 | end
445 |
446 | describe PusherFake::Server::Application, ".channel, for an occupied channel" do
447 | subject { described_class }
448 |
449 | let(:name) { "public-1" }
450 | let(:request) { instance_double(Rack::Request, params: {}) }
451 | let(:channels) { { name => channel } }
452 |
453 | let(:channel) do
454 | instance_double(PusherFake::Channel::Presence, connections: [double])
455 | end
456 |
457 | before do
458 | allow(PusherFake::Channel).to receive(:channels).and_return(channels)
459 | end
460 |
461 | it "returns a hash with the occupied status" do
462 | hash = subject.channel(name, request)
463 |
464 | expect(hash).to eq(occupied: true)
465 | end
466 | end
467 |
468 | describe PusherFake::Server::Application, ".channel, for unoccupied channel" do
469 | subject { described_class }
470 |
471 | let(:name) { "public-1" }
472 | let(:request) { instance_double(Rack::Request, params: {}) }
473 | let(:channels) { { name => channel } }
474 |
475 | let(:channel) do
476 | instance_double(PusherFake::Channel::Presence, connections: [])
477 | end
478 |
479 | before do
480 | allow(PusherFake::Channel).to receive(:channels).and_return(channels)
481 | end
482 |
483 | it "returns a hash with the occupied status" do
484 | hash = subject.channel(name, request)
485 |
486 | expect(hash).to eq(occupied: false)
487 | end
488 | end
489 |
490 | describe PusherFake::Server::Application, ".channel, for an unknown channel" do
491 | subject { described_class }
492 |
493 | let(:request) { instance_double(Rack::Request, params: {}) }
494 | let(:channels) { {} }
495 |
496 | before do
497 | allow(PusherFake::Channel).to receive(:channels).and_return(channels)
498 | end
499 |
500 | it "returns a hash with the occupied status" do
501 | hash = subject.channel("fake", request)
502 |
503 | expect(hash).to eq(occupied: false)
504 | end
505 | end
506 |
507 | describe PusherFake::Server::Application,
508 | ".channel, request user count for a presence channel" do
509 | subject { described_class }
510 |
511 | let(:name) { "presence-1" }
512 | let(:params) { { "info" => "user_count" } }
513 | let(:request) { instance_double(Rack::Request, params: params) }
514 | let(:channels) { { name => channel } }
515 |
516 | let(:channel) do
517 | instance_double(PusherFake::Channel::Presence,
518 | connections: [double, double])
519 | end
520 |
521 | before do
522 | allow(PusherFake::Channel).to receive(:channels).and_return(channels)
523 | end
524 |
525 | it "returns a hash with the occupied status" do
526 | hash = subject.channel(name, request)
527 |
528 | expect(hash).to eq(occupied: true, user_count: 2)
529 | end
530 | end
531 |
532 | describe PusherFake::Server::Application,
533 | ".channel, requesting a user count on a non-presence channel" do
534 | subject { described_class }
535 |
536 | let(:params) { { "info" => "user_count" } }
537 | let(:request) { instance_double(Rack::Request, params: params) }
538 |
539 | it "raises an error" do
540 | expect do
541 | subject.channel("public-1", request)
542 | end.to raise_error(subject::CHANNEL_USER_COUNT_ERROR)
543 | end
544 | end
545 |
546 | describe PusherFake::Server::Application, ".users, for an occupied channel" do
547 | subject { described_class }
548 |
549 | let(:name) { "public-1" }
550 | let(:user_1) { instance_double(PusherFake::Connection, id: "1") }
551 | let(:user_2) { instance_double(PusherFake::Connection, id: "2") }
552 | let(:channels) { { name => channel } }
553 |
554 | let(:channel) do
555 | instance_double(PusherFake::Channel::Public, connections: [user_1, user_2])
556 | end
557 |
558 | before do
559 | allow(PusherFake::Channel).to receive(:channels).and_return(channels)
560 | end
561 |
562 | it "returns a hash with the occupied status" do
563 | hash = subject.users(name)
564 |
565 | expect(hash).to eq(users: [{ id: user_1.id }, { id: user_2.id }])
566 | end
567 | end
568 |
569 | describe PusherFake::Server::Application, ".users, for an empty channel" do
570 | subject { described_class }
571 |
572 | let(:name) { "public-1" }
573 | let(:channels) { { name => channel } }
574 |
575 | let(:channel) do
576 | instance_double(PusherFake::Channel::Public, connections: [])
577 | end
578 |
579 | before do
580 | allow(PusherFake::Channel).to receive(:channels).and_return(channels)
581 | end
582 |
583 | it "returns a hash with the occupied status" do
584 | hash = subject.users(name)
585 |
586 | expect(hash).to eq(users: [])
587 | end
588 | end
589 |
590 | describe PusherFake::Server::Application, ".users, for an unknown channel" do
591 | subject { described_class }
592 |
593 | let(:channels) { {} }
594 |
595 | before do
596 | allow(PusherFake::Channel).to receive(:channels).and_return(channels)
597 | end
598 |
599 | it "returns a hash with the occupied status" do
600 | hash = subject.users("fake")
601 |
602 | expect(hash).to eq(users: [])
603 | end
604 | end
605 |
--------------------------------------------------------------------------------