pusher-fake 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/features/channel_presence.feature +21 -0
- data/features/channel_subscribe.feature +33 -0
- data/features/channel_trigger.feature +74 -0
- data/features/client_connect.feature +8 -0
- data/features/step_definitions/channel_steps.rb +82 -0
- data/features/step_definitions/client_steps.rb +28 -0
- data/features/step_definitions/event_steps.rb +41 -0
- data/features/step_definitions/navigation_steps.rb +3 -0
- data/features/step_definitions/presence_steps.rb +6 -0
- data/features/support/application.rb +29 -0
- data/features/support/application/public/javascripts/vendor/pusher-1.11.js +1155 -0
- data/features/support/application/views/index.erb +43 -0
- data/features/support/environment.rb +12 -0
- data/features/support/pusher-fake.rb +7 -0
- data/features/support/wait.rb +9 -0
- data/lib/pusher-fake.rb +35 -0
- data/lib/pusher-fake/channel.rb +51 -0
- data/lib/pusher-fake/channel/presence.rb +50 -0
- data/lib/pusher-fake/channel/private.rb +44 -0
- data/lib/pusher-fake/channel/public.rb +63 -0
- data/lib/pusher-fake/configuration.rb +31 -0
- data/lib/pusher-fake/connection.rb +54 -0
- data/lib/pusher-fake/server.rb +48 -0
- data/lib/pusher-fake/server/application.rb +60 -0
- data/spec/lib/pusher-fake/channel/presence_spec.rb +133 -0
- data/spec/lib/pusher-fake/channel/private_spec.rb +125 -0
- data/spec/lib/pusher-fake/channel/public_spec.rb +90 -0
- data/spec/lib/pusher-fake/channel_spec.rb +117 -0
- data/spec/lib/pusher-fake/configuration_spec.rb +10 -0
- data/spec/lib/pusher-fake/connection_spec.rb +193 -0
- data/spec/lib/pusher-fake/server/application_spec.rb +133 -0
- data/spec/lib/pusher-fake/server_spec.rb +150 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/have_configuration_option_matcher.rb +19 -0
- 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
|
data/lib/pusher-fake.rb
ADDED
@@ -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
|