pusher-fake 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -72,3 +72,13 @@ Feature: Triggering events on a channel
72
72
  When I trigger the "client-message" event on the "chat" channel
73
73
  Then I should not receive a "client-message" event on the "chat" channel
74
74
  And Bob should not receive a "client-message" event on the "chat" channel
75
+
76
+ Scenario: Server triggers an event on multiple private channels
77
+ Given I am subscribed to the "private-chat-1" channel
78
+ And Bob is subscribed to the "private-chat-2" channel
79
+ When a "message" event is triggered on the following channels:
80
+ | name |
81
+ | private-chat-1 |
82
+ | private-chat-2 |
83
+ Then I should receive a "message" event on the "private-chat-1" channel
84
+ And Bob should receive a "message" event on the "private-chat-2" channel
@@ -0,0 +1,40 @@
1
+ @javascript
2
+ Feature: Triggering channel webhooks
3
+
4
+ Background:
5
+ Given I am connected
6
+ And Bob is connected
7
+
8
+ Scenario: Occupying and vacating a channel
9
+ When I subscribe to the "game-1" channel
10
+ Then the server should have received the following event:
11
+ | name | channel_occupied |
12
+ | channel | game-1 |
13
+ When Bob is subscribed to the "game-1" channel
14
+ Then the server should have received no events
15
+ When Bob unsubscribes from the "game-1" channel
16
+ Then the server should have received no events
17
+ When I unsubscribe from the "game-1" channel
18
+ Then the server should have received the following event:
19
+ | name | channel_vacated |
20
+ | channel | game-1 |
21
+
22
+ Scenario: Subscribing and unsubscribing from a presence channel
23
+ When I subscribe to the "presence-chat-1" channel
24
+ Then the server should have received the following user event:
25
+ | name | member_added |
26
+ | channel | presence-chat-1 |
27
+ When Bob is subscribed to the "presence-chat-1" channel
28
+ Then the server should have received the following user event:
29
+ | user | Bob |
30
+ | name | member_added |
31
+ | channel | presence-chat-1 |
32
+ When Bob unsubscribes from the "presence-chat-1" channel
33
+ Then the server should have received the following user event:
34
+ | user | Bob |
35
+ | name | member_removed |
36
+ | channel | presence-chat-1 |
37
+ When I unsubscribe from the "presence-chat-1" channel
38
+ Then the server should have received the following user event:
39
+ | name | member_removed |
40
+ | channel | presence-chat-1 |
@@ -10,51 +10,48 @@ Given %{$name is subscribed to the "$channel" channel} do |name, channel|
10
10
  end
11
11
 
12
12
  When %{I subscribe to the "$channel" channel} do |channel|
13
- page.execute_script("Pusher.instance.subscribe(#{channel.to_json})")
13
+ page.execute_script("Pusher.instance.subscribe(#{MultiJson.dump(channel)})")
14
14
  end
15
15
 
16
16
  When %{I subscribe to the "$channel" channel with presence events} do |channel|
17
17
  page.execute_script(%{
18
- var list = document.querySelector("#presence ul"),
19
- count = document.querySelector("#presence header h1 span"),
20
- channel = Pusher.instance.subscribe(#{channel.to_json});
18
+ var list = list || document.querySelector("section ul"),
19
+ count = count || document.querySelector("section header h1 span"),
20
+ addClient = addClient || function(client) {
21
+ var
22
+ element = list.appendChild(document.createElement("li"));
23
+ element.setAttribute("id", "client-" + client.id);
21
24
 
22
- channel.bind("pusher:subscription_succeeded", function(clients) {
23
- count.innerHTML = clients.count;
25
+ if (client.info) {
26
+ element.innerHTML = client.info.name;
27
+ }
28
+ },
29
+ changeCount = changeCount || function(delta) {
30
+ count.innerHTML = parseInt(count.innerHTML, 10) + delta;
31
+ };
24
32
 
25
- clients.each(function(client) {
26
- var
27
- element = list.appendChild(document.createElement("li"));
28
- element.setAttribute("id", "client-" + client.id);
33
+ Pusher.instance.subscribe(#{MultiJson.dump(channel)})
34
+ .bind("pusher:subscription_succeeded", function(clients) {
35
+ clients.each(addClient);
29
36
 
30
- if (client.info) {
31
- element.innerHTML = client.info.name;
32
- }
33
- });
34
- });
35
- channel.bind("pusher:member_added", function(client) {
36
- count.innerHTML = parseInt(count.innerHTML, 10) + 1;
37
-
38
- var
39
- element = list.appendChild(document.createElement("li"));
40
- element.setAttribute("id", "client-" + client.id);
37
+ count.innerHTML = clients.count;
38
+ })
39
+ .bind("pusher:member_added", function(client) {
40
+ addClient(client);
41
+ changeCount(1);
42
+ })
43
+ .bind("pusher:member_removed", function(client) {
44
+ var item = list.querySelector("li#client-" + client.id);
41
45
 
42
- if (client.info) {
43
- element.innerHTML = client.info.name;
44
- }
45
- });
46
- channel.bind("pusher:member_removed", function(client) {
47
- var item = list.querySelector("li#client-" + client.id);
46
+ list.removeChild(item);
48
47
 
49
- count.innerHTML = parseInt(count.innerHTML, 10) - 1;
50
-
51
- list.removeChild(item);
52
- });
48
+ changeCount(-1);
49
+ });
53
50
  })
54
51
  end
55
52
 
56
53
  When %{I unsubscribe from the "$channel" channel} do |channel|
57
- page.execute_script("Pusher.instance.unsubscribe(#{channel.to_json})")
54
+ page.execute_script("Pusher.instance.unsubscribe(#{MultiJson.dump(channel)})")
58
55
  end
59
56
 
60
57
  When %{$name unsubscribes from the "$channel" channel} do |name, channel|
@@ -64,10 +61,10 @@ When %{$name unsubscribes from the "$channel" channel} do |name, channel|
64
61
  end
65
62
 
66
63
  Then %{I should be subscribed to the "$channel" channel} do |channel|
67
- wait_until do
64
+ Capybara.timeout do
68
65
  subscribed = page.evaluate_script(%{
69
66
  var
70
- channel = Pusher.instance.channel(#{channel.to_json});
67
+ channel = Pusher.instance.channel(#{MultiJson.dump(channel)});
71
68
  channel && channel.subscribed;
72
69
  })
73
70
  subscribed == true
@@ -78,7 +75,7 @@ Then %{I should not be subscribed to the "$channel" channel} do |channel|
78
75
  wait do
79
76
  subscribed = page.evaluate_script(%{
80
77
  var
81
- channel = Pusher.instance.channel(#{channel.to_json});
78
+ channel = Pusher.instance.channel(#{MultiJson.dump(channel)});
82
79
  channel && channel.subscribed;
83
80
  })
84
81
  subscribed.should be_false
@@ -14,14 +14,14 @@ Given "I change my socket ID" do
14
14
  end
15
15
 
16
16
  Then "I should be connected" do
17
- wait_until do
17
+ Capybara.timeout do
18
18
  state = page.evaluate_script("Pusher.instance.connection.state")
19
19
  state == "connected"
20
20
  end
21
21
  end
22
22
 
23
23
  Then "I should not be connected" do
24
- wait_until do
24
+ Capybara.timeout do
25
25
  state = page.evaluate_script("Pusher.instance.connection.state")
26
26
  state == "unavailable"
27
27
  end
@@ -1,17 +1,23 @@
1
1
  When %{a "$event" event is triggered on the "$channel" channel} do |event, channel|
2
- Pusher.trigger([channel], event, {})
2
+ Pusher.trigger(channel, event, {})
3
+ end
4
+
5
+ When %{a "$event" event is triggered on the following channels:} do |event, table|
6
+ channels = table.hashes.collect { |hash| hash["name"] }
7
+
8
+ Pusher.trigger(channels, event, {})
3
9
  end
4
10
 
5
11
  When %{I trigger the "$event" event on the "$channel" channel} do |event, channel|
6
12
  page.execute_script(%{
7
13
  var
8
- channel = Pusher.instance.channel(#{channel.to_json});
9
- channel.trigger(#{event.to_json}, {});
14
+ channel = Pusher.instance.channel(#{MultiJson.dump(channel)});
15
+ channel.trigger(#{MultiJson.dump(event)}, {});
10
16
  })
11
17
  end
12
18
 
13
19
  When %{I manually trigger the "$event" event on the "$channel" channel} do |event, channel|
14
- page.execute_script(%{Pusher.instance.send_event(#{event.to_json}, {}, #{channel.to_json})})
20
+ page.execute_script(%{Pusher.instance.send_event(#{MultiJson.dump(event)}, {}, #{MultiJson.dump(channel)})})
15
21
  end
16
22
 
17
23
  Then /^([^ ]+) should receive a "([^"]+)" event on the "([^"]+)" channel$/ do |name, event, channel|
@@ -19,7 +25,7 @@ Then /^([^ ]+) should receive a "([^"]+)" event on the "([^"]+)" channel$/ do |n
19
25
 
20
26
  using_session(name) do
21
27
  wait do
22
- events = page.evaluate_script("Pusher.instance.events[#{[channel, event].join(":").to_json}]")
28
+ events = page.evaluate_script("Pusher.instance.events[#{MultiJson.dump([channel, event].join(":"))}]")
23
29
  events.length.should == 1
24
30
  end
25
31
  end
@@ -30,7 +36,7 @@ Then /^([^ ]+) should not receive a "([^"]+)" event on the "([^"]+)" channel$/ d
30
36
 
31
37
  using_session(name) do
32
38
  wait do
33
- events = page.evaluate_script("Pusher.instance.events[#{[channel, event].join(":").to_json}]")
39
+ events = page.evaluate_script("Pusher.instance.events[#{MultiJson.dump([channel, event].join(":"))}]")
34
40
  events.should be_nil
35
41
  end
36
42
  end
@@ -1,5 +1,5 @@
1
1
  Then /^I should see (\d+) clients?(?: with the name "([^"]+)")?$/ do |count, name|
2
- within("#presence") do
2
+ within("section") do
3
3
  should have_css("header h1 span", text: count)
4
4
  should have_css("ul li", count: count.to_i, text: name)
5
5
  end
@@ -0,0 +1,21 @@
1
+ Then /^the server should have received the following (user )*event:$/ do |user_event, table|
2
+ event = table.transpose.hashes.first
3
+
4
+ using_session(event.delete("user")) do
5
+ page.evaluate_script("Pusher.instance.connection.socket_id").tap do |socket_id|
6
+ event.merge!("user_id" => socket_id.to_s)
7
+ end
8
+ end if user_event
9
+
10
+ Capybara.timeout do
11
+ $events.include?(event)
12
+ end
13
+
14
+ $events.replace([])
15
+ end
16
+
17
+ Then /^the server should have received no events$/ do
18
+ wait do
19
+ $events.should be_empty
20
+ end
21
+ end
@@ -22,6 +22,6 @@ class Sinatra::Application
22
22
  data = { user_id: params[:socket_id], user_info: { name: "Alan Turing" } }
23
23
  end
24
24
 
25
- channel.authenticate(params[:socket_id], data).to_json
25
+ MultiJson.dump(channel.authenticate(params[:socket_id], data))
26
26
  end
27
27
  end
@@ -5,24 +5,23 @@
5
5
  </head>
6
6
  <body>
7
7
 
8
- <section id="presence">
8
+ <section>
9
9
  <header>
10
10
  <h1><span>0</span> Clients</h1>
11
11
  </header>
12
12
 
13
- <ul>
14
- </ul>
13
+ <ul></ul>
15
14
  </section>
16
15
 
17
16
  <script src="/javascripts/vendor/pusher-1.12.5.js"></script>
18
17
  <script>
19
18
  window.addEventListener("DOMContentLoaded", function() {
20
19
  // Use the PusherFake server.
21
- Pusher.host = <%= PusherFake.configuration.socket_host.to_json %>;
22
- Pusher.ws_port = <%= PusherFake.configuration.socket_port.to_json %>;
20
+ Pusher.host = <%= MultiJson.dump(PusherFake.configuration.socket_host) %>;
21
+ Pusher.ws_port = <%= MultiJson.dump(PusherFake.configuration.socket_port) %>;
23
22
 
24
23
  // Create the client instance.
25
- Pusher.instance = new Pusher(<%= PusherFake.configuration.key.to_json %>);
24
+ Pusher.instance = new Pusher(<%= MultiJson.dump(PusherFake.configuration.key) %>);
26
25
  Pusher.instance.events = {};
27
26
 
28
27
  // Force the connection to go unavailable after a single attempt.
@@ -1,4 +1,3 @@
1
- require "rubygems"
2
1
  require "bundler/setup"
3
2
  require "capybara/cucumber"
4
3
 
@@ -0,0 +1,36 @@
1
+ $events = []
2
+
3
+ Thread.new do
4
+ # Not explicitly requiring Thin::Server occasionally results in
5
+ # Thin::Server.start not being defined.
6
+ require "thin"
7
+ require "thin/server"
8
+
9
+ class WebhookEndpoint
10
+ def self.call(environment)
11
+ request = Rack::Request.new(environment)
12
+ webhook = Pusher::WebHook.new(request)
13
+
14
+ if webhook.valid?
15
+ $events.concat(webhook.events)
16
+ end
17
+
18
+ Rack::Response.new.finish
19
+ end
20
+ end
21
+
22
+ EventMachine.run do
23
+ Thin::Logging.silent = true
24
+ Thin::Server.start('0.0.0.0', 8082, WebhookEndpoint)
25
+ Thread.current[:ready] = true
26
+ end
27
+ end.tap do |thread|
28
+ at_exit { thread.exit }
29
+
30
+ # Wait for the webhook endpoint server to start.
31
+ Capybara.timeout do
32
+ thread[:ready]
33
+ end
34
+ end
35
+
36
+ PusherFake.configuration.webhooks = ["http://localhost:8082"]
data/lib/pusher-fake.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require "em-http-request"
1
2
  require "em-websocket"
2
3
  require "multi_json"
3
4
  require "openssl"
@@ -11,10 +12,11 @@ require "pusher-fake/configuration"
11
12
  require "pusher-fake/connection"
12
13
  require "pusher-fake/server"
13
14
  require "pusher-fake/server/application"
15
+ require "pusher-fake/webhook"
14
16
 
15
17
  module PusherFake
16
18
  # The current version string.
17
- VERSION = "0.2.0"
19
+ VERSION = "0.3.0"
18
20
 
19
21
  # Call this method to modify the defaults.
20
22
  #
@@ -19,6 +19,8 @@ module PusherFake
19
19
  def remove(connection)
20
20
  super
21
21
 
22
+ trigger("member_removed", channel: name, user_id: members[connection][:user_id])
23
+
22
24
  emit("pusher_internal:member_removed", members.delete(connection))
23
25
  end
24
26
 
@@ -43,9 +45,11 @@ module PusherFake
43
45
  # @param [Connection] connection The connection a subscription succeeded for.
44
46
  # @param [Hash] options The options for the channel.
45
47
  def subscription_succeeded(connection, options = {})
46
- members[connection] = MultiJson.load(options[:channel_data], symbolize_keys: true)
48
+ member = members[connection] = MultiJson.load(options[:channel_data], symbolize_keys: true)
49
+
50
+ emit("pusher_internal:member_added", member)
47
51
 
48
- emit("pusher_internal:member_added", members[connection])
52
+ trigger("member_added", channel: name, user_id: member[:user_id])
49
53
 
50
54
  super
51
55
  end
@@ -46,6 +46,10 @@ module PusherFake
46
46
  # @param [Connection] connection The connection to remove.
47
47
  def remove(connection)
48
48
  connections.delete(connection)
49
+
50
+ if connections.empty?
51
+ trigger("channel_vacated", channel: name)
52
+ end
49
53
  end
50
54
 
51
55
  # Return subscription data for the channel.
@@ -66,6 +70,14 @@ module PusherFake
66
70
  def subscription_succeeded(connection, options = {})
67
71
  connection.emit("pusher_internal:subscription_succeeded", subscription_data, name)
68
72
  connections.push(connection)
73
+
74
+ if connections.length == 1
75
+ trigger("channel_occupied", channel: name)
76
+ end
77
+ end
78
+
79
+ def trigger(name, data = {})
80
+ PusherFake::Webhook.trigger(name, data)
69
81
  end
70
82
  end
71
83
  end
@@ -21,6 +21,9 @@ module PusherFake
21
21
  # @return [Fixnum] The port on which the web server listens. (Defaults to +8081+.)
22
22
  attr_accessor :web_port
23
23
 
24
+ # @return [Array] An array of webhook URLs. (Defaults to +[]+.)
25
+ attr_accessor :webhooks
26
+
24
27
  # Instantiated from {PusherFake.configuration}. Sets the defaults.
25
28
  def initialize
26
29
  self.app_id = "PUSHER_APP_ID"
@@ -30,6 +33,7 @@ module PusherFake
30
33
  self.socket_port = 8080
31
34
  self.web_host = "127.0.0.1"
32
35
  self.web_port = 8081
36
+ self.webhooks = []
33
37
  end
34
38
  end
35
39
  end
@@ -0,0 +1,30 @@
1
+ module PusherFake
2
+ class Webhook
3
+ class << self
4
+ def trigger(name, data = {})
5
+ payload = MultiJson.dump({
6
+ events: [data.merge(name: name)],
7
+ time_ms: Time.now.to_i
8
+ })
9
+
10
+ PusherFake.configuration.webhooks.each do |url|
11
+ http = EventMachine::HttpRequest.new(url)
12
+ http.post(body: payload, head: headers_for(payload))
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def headers_for(payload)
19
+ { "Content-Type" => "application/json",
20
+ "X-Pusher-Key" => PusherFake.configuration.key,
21
+ "X-Pusher-Signature" => signature_for(payload)
22
+ }
23
+ end
24
+
25
+ def signature_for(payload)
26
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, PusherFake.configuration.secret, payload)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -16,16 +16,19 @@ describe PusherFake::Channel::Presence do
16
16
  end
17
17
 
18
18
  describe PusherFake::Channel::Presence, "#add" do
19
- let(:data) { { auth: authentication, channel_data: "{}" } }
19
+ let(:data) { { auth: authentication, channel_data: MultiJson.dump(channel_data) } }
20
+ let(:name) { "name" }
21
+ let(:user_id) { "1234" }
20
22
  let(:connection) { stub(emit: nil) }
21
- let(:connections) { stub(push: nil) }
22
- let(:channel_data) { {} }
23
+ let(:connections) { stub(push: nil, length: 0) }
24
+ let(:channel_data) { { user_id: user_id } }
23
25
  let(:authentication) { "auth" }
24
26
  let(:subscription_data) { { presence: { hash: {}, count: 1 } } }
25
27
 
26
- subject { PusherFake::Channel::Presence.new("name") }
28
+ subject { PusherFake::Channel::Presence.new(name) }
27
29
 
28
30
  before do
31
+ PusherFake::Webhook.stubs(:trigger)
29
32
  MultiJson.stubs(:load).returns(channel_data)
30
33
  subject.stubs(connections: connections, emit: nil, subscription_data: subscription_data)
31
34
  end
@@ -66,20 +69,55 @@ describe PusherFake::Channel::Presence, "#add" do
66
69
  connections.should have_received(:push).with(connection)
67
70
  end
68
71
 
72
+ it "triggers channel occupied webhook for the first connection added when authorized" do
73
+ subject.unstub(:connections)
74
+ subject.stubs(authorized?: true)
75
+
76
+ subject.add(connection, data)
77
+ PusherFake::Webhook.should have_received(:trigger).with("channel_occupied", channel: name).once
78
+ subject.add(connection, data)
79
+ PusherFake::Webhook.should have_received(:trigger).with("channel_occupied", channel: name).once
80
+ end
81
+
82
+ it "triggers the member added webhook when authorized" do
83
+ subject.stubs(authorized?: true)
84
+ subject.add(connection, data)
85
+ PusherFake::Webhook.should have_received(:trigger).with("member_added", channel: name, user_id: user_id).once
86
+ end
87
+
69
88
  it "unsuccessfully subscribes the connection when not authorized" do
70
89
  subject.stubs(authorized?: false)
71
90
  subject.add(connection, data)
72
91
  connection.should have_received(:emit).with("pusher_internal:subscription_error", {}, subject.name)
73
92
  end
93
+
94
+ it "does not trigger channel occupied webhook when not authorized" do
95
+ subject.unstub(:connections)
96
+ subject.stubs(authorized?: false)
97
+
98
+ subject.add(connection, data)
99
+ PusherFake::Webhook.should have_received(:trigger).never
100
+ subject.add(connection, data)
101
+ PusherFake::Webhook.should have_received(:trigger).never
102
+ end
103
+
104
+ it "does not trigger the member added webhook when not authorized" do
105
+ subject.stubs(authorized?: false)
106
+ subject.add(connection, data)
107
+ PusherFake::Webhook.should have_received(:trigger).never
108
+ end
74
109
  end
75
110
 
76
111
  describe PusherFake::Channel::Presence, "#remove" do
112
+ let(:name) { "name" }
113
+ let(:user_id) { "1234" }
77
114
  let(:connection) { stub }
78
- let(:channel_data) { {} }
115
+ let(:channel_data) { { user_id: user_id } }
79
116
 
80
- subject { PusherFake::Channel::Presence.new("name") }
117
+ subject { PusherFake::Channel::Presence.new(name) }
81
118
 
82
119
  before do
120
+ PusherFake::Webhook.stubs(:trigger)
83
121
  subject.members[connection] = channel_data
84
122
  subject.stubs(connections: [connection], emit: nil)
85
123
  end
@@ -94,6 +132,11 @@ describe PusherFake::Channel::Presence, "#remove" do
94
132
  subject.members.should_not have_key(connection)
95
133
  end
96
134
 
135
+ it "triggers the member removed webhook" do
136
+ subject.remove(connection)
137
+ PusherFake::Webhook.should have_received(:trigger).with("member_removed", channel: name, user_id: user_id).once
138
+ end
139
+
97
140
  it "notifies the channel of the removed member" do
98
141
  subject.remove(connection)
99
142
  subject.should have_received(:emit).with("pusher_internal:member_removed", channel_data)
@@ -10,13 +10,15 @@ end
10
10
 
11
11
  describe PusherFake::Channel::Private, "#add" do
12
12
  let(:data) { { auth: authentication } }
13
+ let(:name) { "name" }
13
14
  let(:connection) { stub(emit: nil) }
14
- let(:connections) { stub(push: nil) }
15
+ let(:connections) { stub(push: nil, length: 0) }
15
16
  let(:authentication) { "auth" }
16
17
 
17
- subject { PusherFake::Channel::Private.new("name") }
18
+ subject { PusherFake::Channel::Private.new(name) }
18
19
 
19
20
  before do
21
+ PusherFake::Webhook.stubs(:trigger)
20
22
  subject.stubs(connections: connections)
21
23
  end
22
24
 
@@ -38,11 +40,31 @@ describe PusherFake::Channel::Private, "#add" do
38
40
  connection.should have_received(:emit).with("pusher_internal:subscription_succeeded", {}, subject.name)
39
41
  end
40
42
 
43
+ it "triggers channel occupied webhook for the first connection added when authorized" do
44
+ subject.unstub(:connections)
45
+ subject.stubs(authorized?: true)
46
+
47
+ subject.add(connection, data)
48
+ PusherFake::Webhook.should have_received(:trigger).with("channel_occupied", channel: name).once
49
+ subject.add(connection, data)
50
+ PusherFake::Webhook.should have_received(:trigger).with("channel_occupied", channel: name).once
51
+ end
52
+
41
53
  it "unsuccessfully subscribes the connection when not authorized" do
42
54
  subject.stubs(authorized?: false)
43
55
  subject.add(connection, data)
44
56
  connection.should have_received(:emit).with("pusher_internal:subscription_error", {}, subject.name)
45
57
  end
58
+
59
+ it "does not trigger channel occupied webhook when not authorized" do
60
+ subject.unstub(:connections)
61
+ subject.stubs(authorized?: false)
62
+
63
+ subject.add(connection, data)
64
+ PusherFake::Webhook.should have_received(:trigger).never
65
+ subject.add(connection, data)
66
+ PusherFake::Webhook.should have_received(:trigger).never
67
+ end
46
68
  end
47
69
 
48
70
  describe PusherFake::Channel::Private, "#authentication_for" do
@@ -17,12 +17,14 @@ describe PusherFake::Channel::Public do
17
17
  end
18
18
 
19
19
  describe PusherFake::Channel, "#add" do
20
+ let(:name) { "name" }
20
21
  let(:connection) { stub(emit: nil) }
21
- let(:connections) { stub(push: nil) }
22
+ let(:connections) { stub(push: nil, length: 0) }
22
23
 
23
- subject { PusherFake::Channel::Public.new("name") }
24
+ subject { PusherFake::Channel::Public.new(name) }
24
25
 
25
26
  before do
27
+ PusherFake::Webhook.stubs(:trigger)
26
28
  subject.stubs(connections: connections)
27
29
  end
28
30
 
@@ -35,6 +37,15 @@ describe PusherFake::Channel, "#add" do
35
37
  subject.add(connection)
36
38
  connection.should have_received(:emit).with("pusher_internal:subscription_succeeded", {}, subject.name)
37
39
  end
40
+
41
+ it "triggers channel occupied webhook for the first connection added" do
42
+ subject.unstub(:connections)
43
+
44
+ subject.add(connection)
45
+ PusherFake::Webhook.should have_received(:trigger).with("channel_occupied", channel: name).once
46
+ subject.add(connection)
47
+ PusherFake::Webhook.should have_received(:trigger).with("channel_occupied", channel: name).once
48
+ end
38
49
  end
39
50
 
40
51
  describe PusherFake::Channel, "#emit" do
@@ -75,17 +86,27 @@ describe PusherFake::Channel, "#includes?" do
75
86
  end
76
87
 
77
88
  describe PusherFake::Channel, "#remove" do
78
- let(:connection) { stub }
89
+ let(:name) { "name" }
90
+ let(:connection_1) { stub }
91
+ let(:connection_2) { stub }
79
92
 
80
- subject { PusherFake::Channel::Public.new("name") }
93
+ subject { PusherFake::Channel::Public.new(name) }
81
94
 
82
95
  before do
83
- subject.stubs(connections: [connection])
96
+ subject.stubs(connections: [connection_1, connection_2])
97
+ PusherFake::Webhook.stubs(:trigger)
84
98
  end
85
99
 
86
100
  it "removes the connection from the channel" do
87
- subject.remove(connection)
88
- subject.connections.should be_empty
101
+ subject.remove(connection_1)
102
+ subject.connections.should_not include(connection_1)
103
+ end
104
+
105
+ it "triggers channel vacated webhook when all connections are removed" do
106
+ subject.remove(connection_1)
107
+ PusherFake::Webhook.should have_received(:trigger).never
108
+ subject.remove(connection_2)
109
+ PusherFake::Webhook.should have_received(:trigger).with("channel_vacated", channel: name).once
89
110
  end
90
111
  end
91
112
 
@@ -6,22 +6,25 @@ describe PusherFake::Channel, ".factory" do
6
6
 
7
7
  subject { PusherFake::Channel }
8
8
 
9
+ before do
10
+ PusherFake::Channel::Public.stubs(new: channel)
11
+ end
12
+
9
13
  after do
10
14
  PusherFake::Channel.reset
11
15
  end
12
16
 
13
17
  it "caches the channel" do
18
+ PusherFake::Channel::Public.unstub(:new)
14
19
  subject.factory(name).should == subject.factory(name)
15
20
  end
16
21
 
17
22
  it "creates a public channel by name" do
18
- PusherFake::Channel::Public.stubs(new: channel)
19
23
  subject.factory(name)
20
24
  PusherFake::Channel::Public.should have_received(:new).with(name)
21
25
  end
22
26
 
23
27
  it "returns the channel instance" do
24
- PusherFake::Channel::Public.stubs(new: channel)
25
28
  subject.factory(name).should == channel
26
29
  end
27
30
  end
@@ -32,22 +35,25 @@ describe PusherFake::Channel, ".factory, for a private channel" do
32
35
 
33
36
  subject { PusherFake::Channel }
34
37
 
38
+ before do
39
+ PusherFake::Channel::Private.stubs(new: channel)
40
+ end
41
+
35
42
  after do
36
43
  PusherFake::Channel.reset
37
44
  end
38
45
 
39
46
  it "caches the channel" do
47
+ PusherFake::Channel::Private.unstub(:new)
40
48
  subject.factory(name).should == subject.factory(name)
41
49
  end
42
50
 
43
51
  it "creates a private channel by name" do
44
- PusherFake::Channel::Private.stubs(new: channel)
45
52
  subject.factory(name)
46
53
  PusherFake::Channel::Private.should have_received(:new).with(name)
47
54
  end
48
55
 
49
56
  it "returns the channel instance" do
50
- PusherFake::Channel::Private.stubs(new: channel)
51
57
  subject.factory(name).should == channel
52
58
  end
53
59
  end
@@ -58,22 +64,25 @@ describe PusherFake::Channel, ".factory, for a presence channel" do
58
64
 
59
65
  subject { PusherFake::Channel }
60
66
 
67
+ before do
68
+ PusherFake::Channel::Presence.stubs(new: channel)
69
+ end
70
+
61
71
  after do
62
72
  PusherFake::Channel.reset
63
73
  end
64
74
 
65
75
  it "caches the channel" do
76
+ PusherFake::Channel::Presence.unstub(:new)
66
77
  subject.factory(name).should == subject.factory(name)
67
78
  end
68
79
 
69
80
  it "creates a presence channel by name" do
70
- PusherFake::Channel::Presence.stubs(new: channel)
71
81
  subject.factory(name)
72
82
  PusherFake::Channel::Presence.should have_received(:new).with(name)
73
83
  end
74
84
 
75
85
  it "returns the channel instance" do
76
- PusherFake::Channel::Presence.stubs(new: channel)
77
86
  subject.factory(name).should == channel
78
87
  end
79
88
  end
@@ -116,6 +125,7 @@ describe PusherFake::Channel, ".reset" do
116
125
  subject { PusherFake::Channel }
117
126
 
118
127
  it "empties the channel cache" do
128
+ subject.factory("example")
119
129
  subject.reset
120
130
  subject.channels.should == {}
121
131
  end
@@ -8,4 +8,5 @@ describe PusherFake::Configuration do
8
8
  it { should have_configuration_option(:socket_port).with_default(8080) }
9
9
  it { should have_configuration_option(:web_host).with_default("127.0.0.1") }
10
10
  it { should have_configuration_option(:web_port).with_default(8081) }
11
+ it { should have_configuration_option(:webhooks).with_default([]) }
11
12
  end
@@ -10,7 +10,7 @@ describe PusherFake::Server, ".start" do
10
10
 
11
11
  it "runs the event loop" do
12
12
  subject.start
13
- EventMachine.should have_received(:run)
13
+ EventMachine.should have_received(:run).with()
14
14
  end
15
15
 
16
16
  it "starts the socket web server when run yields" do
@@ -20,7 +20,7 @@ describe PusherFake::Server, ".start" do
20
20
  EventMachine.stubs(:run).yields
21
21
 
22
22
  subject.start
23
- subject.should have_received(:start_web_server)
23
+ subject.should have_received(:start_web_server).with()
24
24
  end
25
25
 
26
26
  it "starts the socket server when run yields" do
@@ -30,7 +30,7 @@ describe PusherFake::Server, ".start" do
30
30
  EventMachine.stubs(:run).yields
31
31
 
32
32
  subject.start
33
- subject.should have_received(:start_socket_server)
33
+ subject.should have_received(:start_socket_server).with()
34
34
  end
35
35
  end
36
36
 
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ describe PusherFake::Webhook, ".trigger" do
4
+ subject { PusherFake::Webhook }
5
+
6
+ let(:data) { { channel: "name" } }
7
+ let(:http) { stub(post: true) }
8
+ let(:name) { "channel_occupied" }
9
+ let(:payload) { MultiJson.dump({ events: [data.merge(name: name)], time_ms: Time.now.to_i }) }
10
+ let(:webhooks) { ["url"] }
11
+ let(:signature) { "signature" }
12
+ let(:configuration) { stub(key: "key", secret: "secret", webhooks: webhooks) }
13
+
14
+ before do
15
+ OpenSSL::HMAC.stubs(hexdigest: signature)
16
+ EventMachine::HttpRequest.stubs(new: http)
17
+ PusherFake.stubs(configuration: configuration)
18
+ end
19
+
20
+ it "generates a signature" do
21
+ subject.trigger(name, data)
22
+ OpenSSL::HMAC.should have_received(:hexdigest).with(kind_of(OpenSSL::Digest::SHA256), configuration.secret, payload)
23
+ end
24
+
25
+ it "creates a HTTP request for each webhook URL" do
26
+ subject.trigger(name, data)
27
+ EventMachine::HttpRequest.should have_received(:new).with(webhooks.first)
28
+ end
29
+
30
+ it "posts the payload to the webhook URL" do
31
+ subject.trigger(name, data)
32
+ http.should have_received(:post).with(
33
+ body: payload,
34
+ head: {
35
+ "Content-Type" => "application/json",
36
+ "X-Pusher-Key" => configuration.key,
37
+ "X-Pusher-Signature" => signature
38
+ }
39
+ )
40
+ end
41
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,3 @@
1
- require "rubygems"
2
1
  require "bundler/setup"
3
2
 
4
3
  Bundler.require(:default, :development)
@@ -6,11 +6,8 @@ module BartenderHelper
6
6
 
7
7
  def matches?(configuration)
8
8
  @configuration = configuration
9
-
10
9
  @configuration.respond_to?(@option).should == true
11
-
12
10
  @configuration.__send__(@option).should == @default if instance_variables.include?("@default")
13
-
14
11
  @configuration.__send__(:"#{@option}=", "value")
15
12
  @configuration.__send__(@option).should == "value"
16
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pusher-fake
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,16 +9,16 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-28 00:00:00.000000000 Z
12
+ date: 2012-12-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: em-websocket
15
+ name: em-http-request
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - '='
20
20
  - !ruby/object:Gem::Version
21
- version: 0.3.8
21
+ version: 1.0.3
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,15 +26,15 @@ dependencies:
26
26
  requirements:
27
27
  - - '='
28
28
  - !ruby/object:Gem::Version
29
- version: 0.3.8
29
+ version: 1.0.3
30
30
  - !ruby/object:Gem::Dependency
31
- name: thin
31
+ name: em-websocket
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  none: false
34
34
  requirements:
35
35
  - - '='
36
36
  - !ruby/object:Gem::Version
37
- version: 1.5.0
37
+ version: 0.3.8
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,15 +42,15 @@ dependencies:
42
42
  requirements:
43
43
  - - '='
44
44
  - !ruby/object:Gem::Version
45
- version: 1.5.0
45
+ version: 0.3.8
46
46
  - !ruby/object:Gem::Dependency
47
- name: multi_json
47
+ name: thin
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  none: false
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 1.3.7
53
+ version: 1.5.0
54
54
  type: :runtime
55
55
  prerelease: false
56
56
  version_requirements: !ruby/object:Gem::Requirement
@@ -58,47 +58,31 @@ dependencies:
58
58
  requirements:
59
59
  - - '='
60
60
  - !ruby/object:Gem::Version
61
- version: 1.3.7
61
+ version: 1.5.0
62
62
  - !ruby/object:Gem::Dependency
63
- name: bourne
63
+ name: multi_json
64
64
  requirement: !ruby/object:Gem::Requirement
65
65
  none: false
66
66
  requirements:
67
67
  - - '='
68
68
  - !ruby/object:Gem::Version
69
- version: 1.2.1
70
- type: :development
69
+ version: 1.4.0
70
+ type: :runtime
71
71
  prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
73
73
  none: false
74
74
  requirements:
75
75
  - - '='
76
76
  - !ruby/object:Gem::Version
77
- version: 1.2.1
77
+ version: 1.4.0
78
78
  - !ruby/object:Gem::Dependency
79
- name: bundler
80
- requirement: !ruby/object:Gem::Requirement
81
- none: false
82
- requirements:
83
- - - ! '>='
84
- - !ruby/object:Gem::Version
85
- version: 1.1.0
86
- type: :development
87
- prerelease: false
88
- version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
- requirements:
91
- - - ! '>='
92
- - !ruby/object:Gem::Version
93
- version: 1.1.0
94
- - !ruby/object:Gem::Dependency
95
- name: capybara
79
+ name: bourne
96
80
  requirement: !ruby/object:Gem::Requirement
97
81
  none: false
98
82
  requirements:
99
83
  - - '='
100
84
  - !ruby/object:Gem::Version
101
- version: 1.1.3
85
+ version: 1.3.0
102
86
  type: :development
103
87
  prerelease: false
104
88
  version_requirements: !ruby/object:Gem::Requirement
@@ -106,7 +90,7 @@ dependencies:
106
90
  requirements:
107
91
  - - '='
108
92
  - !ruby/object:Gem::Version
109
- version: 1.1.3
93
+ version: 1.3.0
110
94
  - !ruby/object:Gem::Dependency
111
95
  name: capybara-webkit
112
96
  requirement: !ruby/object:Gem::Requirement
@@ -249,22 +233,26 @@ files:
249
233
  - lib/pusher-fake/connection.rb
250
234
  - lib/pusher-fake/server/application.rb
251
235
  - lib/pusher-fake/server.rb
236
+ - lib/pusher-fake/webhook.rb
252
237
  - lib/pusher-fake.rb
253
238
  - features/channel_presence.feature
254
239
  - features/channel_subscribe.feature
255
240
  - features/channel_trigger.feature
241
+ - features/channel_webhooks.feature
256
242
  - features/client_connect.feature
257
243
  - features/step_definitions/channel_steps.rb
258
244
  - features/step_definitions/client_steps.rb
259
245
  - features/step_definitions/event_steps.rb
260
246
  - features/step_definitions/navigation_steps.rb
261
247
  - features/step_definitions/presence_steps.rb
248
+ - features/step_definitions/webhook_steps.rb
262
249
  - features/support/application/public/javascripts/vendor/pusher-1.12.5.js
263
250
  - features/support/application/views/index.erb
264
251
  - features/support/application.rb
265
252
  - features/support/environment.rb
266
253
  - features/support/pusher-fake.rb
267
254
  - features/support/wait.rb
255
+ - features/support/webhooks.rb
268
256
  - spec/lib/pusher-fake/channel/presence_spec.rb
269
257
  - spec/lib/pusher-fake/channel/private_spec.rb
270
258
  - spec/lib/pusher-fake/channel/public_spec.rb
@@ -273,6 +261,7 @@ files:
273
261
  - spec/lib/pusher-fake/connection_spec.rb
274
262
  - spec/lib/pusher-fake/server/application_spec.rb
275
263
  - spec/lib/pusher-fake/server_spec.rb
264
+ - spec/lib/pusher-fake/webhook_spec.rb
276
265
  - spec/spec_helper.rb
277
266
  - spec/support/have_configuration_option_matcher.rb
278
267
  homepage: http://github.com/tristandunn/pusher-fake
@@ -290,7 +279,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
290
279
  version: '0'
291
280
  segments:
292
281
  - 0
293
- hash: 680940724053515135
282
+ hash: 4186586164521781753
294
283
  required_rubygems_version: !ruby/object:Gem::Requirement
295
284
  none: false
296
285
  requirements:
@@ -299,7 +288,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
299
288
  version: '0'
300
289
  segments:
301
290
  - 0
302
- hash: 680940724053515135
291
+ hash: 4186586164521781753
303
292
  requirements: []
304
293
  rubyforge_project:
305
294
  rubygems_version: 1.8.23
@@ -310,18 +299,21 @@ test_files:
310
299
  - features/channel_presence.feature
311
300
  - features/channel_subscribe.feature
312
301
  - features/channel_trigger.feature
302
+ - features/channel_webhooks.feature
313
303
  - features/client_connect.feature
314
304
  - features/step_definitions/channel_steps.rb
315
305
  - features/step_definitions/client_steps.rb
316
306
  - features/step_definitions/event_steps.rb
317
307
  - features/step_definitions/navigation_steps.rb
318
308
  - features/step_definitions/presence_steps.rb
309
+ - features/step_definitions/webhook_steps.rb
319
310
  - features/support/application/public/javascripts/vendor/pusher-1.12.5.js
320
311
  - features/support/application/views/index.erb
321
312
  - features/support/application.rb
322
313
  - features/support/environment.rb
323
314
  - features/support/pusher-fake.rb
324
315
  - features/support/wait.rb
316
+ - features/support/webhooks.rb
325
317
  - spec/lib/pusher-fake/channel/presence_spec.rb
326
318
  - spec/lib/pusher-fake/channel/private_spec.rb
327
319
  - spec/lib/pusher-fake/channel/public_spec.rb
@@ -330,6 +322,7 @@ test_files:
330
322
  - spec/lib/pusher-fake/connection_spec.rb
331
323
  - spec/lib/pusher-fake/server/application_spec.rb
332
324
  - spec/lib/pusher-fake/server_spec.rb
325
+ - spec/lib/pusher-fake/webhook_spec.rb
333
326
  - spec/spec_helper.rb
334
327
  - spec/support/have_configuration_option_matcher.rb
335
328
  has_rdoc: