pusher-fake 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/features/channel_presence.feature +21 -0
  2. data/features/channel_subscribe.feature +33 -0
  3. data/features/channel_trigger.feature +74 -0
  4. data/features/client_connect.feature +8 -0
  5. data/features/step_definitions/channel_steps.rb +82 -0
  6. data/features/step_definitions/client_steps.rb +28 -0
  7. data/features/step_definitions/event_steps.rb +41 -0
  8. data/features/step_definitions/navigation_steps.rb +3 -0
  9. data/features/step_definitions/presence_steps.rb +6 -0
  10. data/features/support/application.rb +29 -0
  11. data/features/support/application/public/javascripts/vendor/pusher-1.11.js +1155 -0
  12. data/features/support/application/views/index.erb +43 -0
  13. data/features/support/environment.rb +12 -0
  14. data/features/support/pusher-fake.rb +7 -0
  15. data/features/support/wait.rb +9 -0
  16. data/lib/pusher-fake.rb +35 -0
  17. data/lib/pusher-fake/channel.rb +51 -0
  18. data/lib/pusher-fake/channel/presence.rb +50 -0
  19. data/lib/pusher-fake/channel/private.rb +44 -0
  20. data/lib/pusher-fake/channel/public.rb +63 -0
  21. data/lib/pusher-fake/configuration.rb +31 -0
  22. data/lib/pusher-fake/connection.rb +54 -0
  23. data/lib/pusher-fake/server.rb +48 -0
  24. data/lib/pusher-fake/server/application.rb +60 -0
  25. data/spec/lib/pusher-fake/channel/presence_spec.rb +133 -0
  26. data/spec/lib/pusher-fake/channel/private_spec.rb +125 -0
  27. data/spec/lib/pusher-fake/channel/public_spec.rb +90 -0
  28. data/spec/lib/pusher-fake/channel_spec.rb +117 -0
  29. data/spec/lib/pusher-fake/configuration_spec.rb +10 -0
  30. data/spec/lib/pusher-fake/connection_spec.rb +193 -0
  31. data/spec/lib/pusher-fake/server/application_spec.rb +133 -0
  32. data/spec/lib/pusher-fake/server_spec.rb +150 -0
  33. data/spec/spec_helper.rb +12 -0
  34. data/spec/support/have_configuration_option_matcher.rb +19 -0
  35. metadata +265 -0
@@ -0,0 +1,43 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>PusherFake Test Application</title>
5
+ </head>
6
+ <body>
7
+
8
+ <section id="presence">
9
+ <header>
10
+ <h1><span>0</span> Clients</h1>
11
+ </header>
12
+
13
+ <ul>
14
+ </ul>
15
+ </section>
16
+
17
+ <script src="/javascripts/vendor/pusher-1.11.js"></script>
18
+ <script>
19
+ window.addEventListener("DOMContentLoaded", function() {
20
+ // Use the PusherFake server.
21
+ Pusher.host = <%= PusherFake.configuration.socket_host.to_json %>;
22
+ Pusher.ws_port = <%= PusherFake.configuration.socket_port.to_json %>;
23
+
24
+ // Create the client instance.
25
+ Pusher.instance = new Pusher(<%= PusherFake.configuration.key.to_json %>);
26
+ Pusher.instance.events = {};
27
+
28
+ // Force the connection to go unavailable after a single attempt.
29
+ Pusher.instance.connection.connectionAttempts = 4;
30
+
31
+ // Record all events.
32
+ Pusher.instance.connection.bind("message", function(message) {
33
+ var events = Pusher.instance.events,
34
+ namespace = [message.channel, message.event].join(":");
35
+
36
+ events[namespace] || (events[namespace] = []);
37
+ events[namespace].push(message.data);
38
+ });
39
+ }, false);
40
+ </script>
41
+
42
+ </body>
43
+ </html>
@@ -0,0 +1,12 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ require "capybara/cucumber"
4
+
5
+ Bundler.require(:default, :development)
6
+
7
+ Dir[File.expand_path("../support/**/*.rb", __FILE__)].each do |file|
8
+ require file
9
+ end
10
+
11
+ Capybara.app = Sinatra::Application
12
+ Capybara.javascript_driver = :webkit
@@ -0,0 +1,7 @@
1
+ Thread.new { PusherFake::Server.start }.tap do |thread|
2
+ at_exit { thread.exit }
3
+ end
4
+
5
+ After do
6
+ PusherFake::Channel.reset
7
+ end
@@ -0,0 +1,9 @@
1
+ module Capybara
2
+ module Wait
3
+ def wait(seconds = 0.25, &block)
4
+ sleep(seconds) && yield
5
+ end
6
+ end
7
+ end
8
+
9
+ World(Capybara::Wait)
@@ -0,0 +1,35 @@
1
+ require "em-websocket"
2
+ require "hmac-sha2"
3
+ require "thin"
4
+ require "yajl"
5
+
6
+ require "pusher-fake/channel"
7
+ require "pusher-fake/channel/public"
8
+ require "pusher-fake/channel/private"
9
+ require "pusher-fake/channel/presence"
10
+ require "pusher-fake/configuration"
11
+ require "pusher-fake/connection"
12
+ require "pusher-fake/server"
13
+ require "pusher-fake/server/application"
14
+
15
+ module PusherFake
16
+ # The current version string.
17
+ VERSION = "0.1.0"
18
+
19
+ # Call this method to modify the defaults.
20
+ #
21
+ # @example
22
+ # PusherFake.configure do |configuration|
23
+ # configuration.port = 443
24
+ # end
25
+ #
26
+ # @yield [Configuration] The current configuration.
27
+ def self.configure
28
+ yield(configuration)
29
+ end
30
+
31
+ # @return [Configuration] Current configuration.
32
+ def self.configuration
33
+ @@configuration ||= Configuration.new
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ module PusherFake
2
+ module Channel
3
+ class << self
4
+ PRIVATE_CHANNEL_MATCHER = /^private-/.freeze
5
+ PRESENCE_CHANNEL_MATCHER = /^presence-/.freeze
6
+
7
+ attr_accessor :channels
8
+
9
+ # Create a channel, determing the type by the name.
10
+ #
11
+ # @param [String] name The channel name.
12
+ # @return [Public|Private] The channel object.
13
+ def factory(name)
14
+ self.channels ||= {}
15
+ self.channels[name] ||= class_for(name).new(name)
16
+ end
17
+
18
+ # Remove a connection from all channels.
19
+ #
20
+ # Also deletes the channel if it is empty.
21
+ #
22
+ # @param [Connection] connection The connection to remove.
23
+ def remove(connection)
24
+ channels.each do |name, channel|
25
+ channel.remove(connection)
26
+
27
+ if channels[name].connections.empty?
28
+ channels.delete(name)
29
+ end
30
+ end
31
+ end
32
+
33
+ # Reset the channel cache.
34
+ def reset
35
+ self.channels = {}
36
+ end
37
+
38
+ private
39
+
40
+ def class_for(name)
41
+ if name =~ PRIVATE_CHANNEL_MATCHER
42
+ Private
43
+ elsif name =~ PRESENCE_CHANNEL_MATCHER
44
+ Presence
45
+ else
46
+ Public
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,50 @@
1
+ module PusherFake
2
+ module Channel
3
+ class Presence < Private
4
+ # @return [Hash] Channel members hash.
5
+ attr_reader :members
6
+
7
+ # Create a new {Presence} object.
8
+ #
9
+ # @param [String] name The channel name.
10
+ def initialize(name)
11
+ super(name)
12
+
13
+ @members = {}
14
+ end
15
+
16
+ # Removes the +connection+ from the channel and notifies the channel.
17
+ #
18
+ # @param [Connection] connection The connection to remove.
19
+ def remove(connection)
20
+ super
21
+
22
+ emit("pusher_internal:member_removed", members.delete(connection))
23
+ end
24
+
25
+ # Returns a subscription hash containing presence information for
26
+ # the channel.
27
+ #
28
+ # @return [Hash] Subscription hash contained presence information.
29
+ def subscription_data
30
+ hash = Hash[
31
+ members.map { |_, member|
32
+ [member[:user_id], member]
33
+ }
34
+ ]
35
+
36
+ { presence: { hash: hash, count: members.size } }
37
+ end
38
+
39
+ private
40
+
41
+ def subscription_succeeded(connection, options = {})
42
+ members[connection] = Yajl::Parser.parse(options[:channel_data], symbolize_keys: true)
43
+
44
+ emit("pusher_internal:member_added", members[connection])
45
+
46
+ super(connection, options)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ module PusherFake
2
+ module Channel
3
+ class Private < Public
4
+ # Add the connection to the channel if they are authorized.
5
+ #
6
+ # @param [Connection] connection The connection to add.
7
+ # @param [Hash] options The options for the channel.
8
+ # @option options [String] :auth The authentication string.
9
+ # @option options [Hash] :channel_data The ID and information for the subscribed client.
10
+ def add(connection, options = {})
11
+ if authorized?(connection, options)
12
+ subscription_succeeded(connection, options)
13
+ else
14
+ connection.emit("pusher_internal:subscription_error", {}, name)
15
+ end
16
+ end
17
+
18
+ # Determine if the connection is authorized for the channel.
19
+ #
20
+ # @param [Connection] connection The connection to authorize.
21
+ # @param [Hash] options
22
+ # @option options [String] :auth The authentication string.
23
+ # @return [Boolean] +true+ if authorized, +false+ otherwise.
24
+ def authorized?(connection, options)
25
+ authentication_for(connection.socket.object_id, options[:channel_data]) == options[:auth]
26
+ end
27
+
28
+ # Generate an authentication string from the channel based on the
29
+ # connection ID provided.
30
+ #
31
+ # @private
32
+ # @param [String] id The connection ID.
33
+ # @param [String] data Custom channel data.
34
+ # @return [String] The authentication string.
35
+ def authentication_for(id, data = nil)
36
+ configuration = PusherFake.configuration
37
+ string = [id, name, data].compact.map(&:to_s).join(":")
38
+ signature = HMAC::SHA256.hexdigest(configuration.secret, string)
39
+
40
+ "#{configuration.key}:#{signature}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ module PusherFake
2
+ module Channel
3
+ class Public
4
+ # @return [Array] Connections in this channel.
5
+ attr_reader :connections
6
+
7
+ # @return [String] The channel name.
8
+ attr_reader :name
9
+
10
+ # Create a new {Public} object.
11
+ #
12
+ # @param [String] name The channel name.
13
+ def initialize(name)
14
+ @name = name
15
+ @connections = []
16
+ end
17
+
18
+ # Add the connection to the channel.
19
+ #
20
+ # @param [Connection] connection The connection to add.
21
+ # @param [Hash] options The options for the channel.
22
+ def add(connection, options = {})
23
+ subscription_succeeded(connection, options)
24
+ end
25
+
26
+ # Emits an event to the channel.
27
+ #
28
+ # @param [String] event The event name.
29
+ # @param [Hash] data The event data.
30
+ def emit(event, data)
31
+ connections.each do |connection|
32
+ connection.emit(event, data, name)
33
+ end
34
+ end
35
+
36
+ # Determines if the +connection+ is in the channel.
37
+ #
38
+ # @param [Connection] connection The connection.
39
+ # @return [Boolean] +true+ if the connection is in the channel, +false+ otherwise.
40
+ def includes?(connection)
41
+ connections.index(connection)
42
+ end
43
+
44
+ # Removes the +connection+ from the channel.
45
+ #
46
+ # @param [Connection] connection The connection to remove.
47
+ def remove(connection)
48
+ connections.delete(connection)
49
+ end
50
+
51
+ private
52
+
53
+ def subscription_data
54
+ {}
55
+ end
56
+
57
+ def subscription_succeeded(connection, options = {})
58
+ connection.emit("pusher_internal:subscription_succeeded", subscription_data, name)
59
+ connections.push(connection)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,31 @@
1
+ module PusherFake
2
+ class Configuration
3
+ # @return [String] The Pusher API key. (Defaults to +PUSHER_API_KEY+.)
4
+ attr_accessor :key
5
+
6
+ # @return [String] The Pusher API token. (Defaults to +PUSHER_API_SECRET+.)
7
+ attr_accessor :secret
8
+
9
+ # @return [String] The host on which the socket server listens. (Defaults to +127.0.0.1+.)
10
+ attr_accessor :socket_host
11
+
12
+ # @return [Fixnum] The port on which the socket server listens. (Defaults to +8080+.)
13
+ attr_accessor :socket_port
14
+
15
+ # @return [String] The host on which the web server listens. (Defaults to +127.0.0.1+.)
16
+ attr_accessor :web_host
17
+
18
+ # @return [Fixnum] The port on which the web server listens. (Defaults to +8081+.)
19
+ attr_accessor :web_port
20
+
21
+ # Instantiated from {PusherFake.configuration}. Sets the defaults.
22
+ def initialize
23
+ self.key = "PUSHER_API_KEY"
24
+ self.secret = "PUSHER_API_SECRET"
25
+ self.socket_host = "127.0.0.1"
26
+ self.socket_port = 8080
27
+ self.web_host = "127.0.0.1"
28
+ self.web_port = 8081
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,54 @@
1
+ module PusherFake
2
+ class Connection
3
+ # @return [EventMachine::WebSocket::Connection] The socket object for this connection.
4
+ attr_reader :socket
5
+
6
+ # Create a new {Connection} object.
7
+ #
8
+ # @param [EventMachine::WebSocket::Connection] socket The socket object for the connection.
9
+ def initialize(socket)
10
+ @socket = socket
11
+ end
12
+
13
+ # Emit an event to the connection.
14
+ #
15
+ # @param [String] event The event name.
16
+ # @param [Hash] data The event data.
17
+ # @param [String] The channel name.
18
+ def emit(event, data = {}, channel = nil)
19
+ message = { event: event, data: data }
20
+ message[:channel] = channel if channel
21
+ message = Yajl::Encoder.encode(message)
22
+
23
+ socket.send(message)
24
+ end
25
+
26
+ # Notifies the Pusher client that a connection has been established.
27
+ def establish
28
+ emit("pusher:connection_established", socket_id: socket.object_id)
29
+ end
30
+
31
+ # Processes an event.
32
+ #
33
+ # @param [String] data The event data as JSON.
34
+ def process(data)
35
+ message = Yajl::Parser.parse(data, symbolize_keys: true)
36
+ data = message[:data]
37
+ event = message[:event]
38
+ channel_name = message[:channel] || data.delete(:channel)
39
+ channel = Channel.factory(channel_name)
40
+
41
+ case event
42
+ when "pusher:subscribe"
43
+ channel.add(self, data)
44
+ when "pusher:unsubscribe"
45
+ channel.remove(self)
46
+ when /^client-(.+)$/
47
+ return unless channel.is_a?(Channel::Private)
48
+ return unless channel.includes?(self)
49
+
50
+ channel.emit(event, data)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,48 @@
1
+ module PusherFake
2
+ module Server
3
+ # Start the servers.
4
+ #
5
+ # @see start_socket_server
6
+ # @see start_web_server
7
+ def self.start
8
+ EventMachine.run do
9
+ start_web_server
10
+ start_socket_server
11
+ end
12
+ end
13
+
14
+ # Start the WebSocket server.
15
+ def self.start_socket_server
16
+ EventMachine::WebSocket.start(socket_server_options) do |socket|
17
+ socket.onopen do
18
+ connection = Connection.new(socket)
19
+ connection.establish
20
+
21
+ socket.onmessage do |data|
22
+ connection.process(data)
23
+ end
24
+ socket.onclose do
25
+ Channel.remove(connection)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ # Start the web server.
32
+ def self.start_web_server
33
+ Thin::Logging.silent = true
34
+ Thin::Server.start(configuration.web_host, configuration.web_port, Application)
35
+ end
36
+
37
+ private
38
+
39
+ def self.configuration
40
+ PusherFake.configuration
41
+ end
42
+
43
+ def self.socket_server_options
44
+ { host: configuration.socket_host,
45
+ port: configuration.socket_port }
46
+ end
47
+ end
48
+ end