pusher-fake 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: