├── .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 [![Latest Version](https://img.shields.io/gem/v/pusher-fake.svg)](https://rubygems.org/gems/pusher-fake) [![Build Status](https://github.com/tristandunn/pusher-fake/workflows/CI/badge.svg)](https://github.com/tristandunn/pusher-fake/actions?query=workflow%3ACI) [![Maintainability](https://api.codeclimate.com/v1/badges/110c6ef7a313bf8baac3/maintainability)](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 | --------------------------------------------------------------------------------