Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit 393543efce97936ac7faaf204160fdccea94a517 0 parents
@tristandunn authored
Showing with 1,658 additions and 0 deletions.
  1. +1 −0  .gitignore
  2. +3 −0  Gemfile
  3. +97 −0 Gemfile.lock
  4. +21 −0 LICENSE
  5. +7 −0 README.markdown
  6. +21 −0 Rakefile
  7. +11 −0 features/client_connect.feature
  8. +13 −0 features/step_definitions/client_steps.rb
  9. +3 −0  features/step_definitions/navigation_steps.rb
  10. +13 −0 features/support/application.rb
  11. +7 −0 features/support/application/public/javascripts/application.js
  12. +1,146 −0 features/support/application/public/javascripts/vendor/pusher-1.9.6.js
  13. +16 −0 features/support/application/views/index.erb
  14. +12 −0 features/support/environment.rb
  15. +29 −0 features/support/pusher-fake-instance.rb
  16. +26 −0 lib/pusher-fake.rb
  17. +15 −0 lib/pusher-fake/configuration.rb
  18. +29 −0 lib/pusher-fake/connection.rb
  19. +23 −0 lib/pusher-fake/server.rb
  20. +3 −0  lib/pusher-fake/version.rb
  21. +29 −0 pusher-fake.gemspec
  22. +6 −0 spec/lib/pusher-fake/configuration_spec.rb
  23. +42 −0 spec/lib/pusher-fake/connection_spec.rb
  24. +54 −0 spec/lib/pusher-fake/server_spec.rb
  25. +12 −0 spec/spec_helper.rb
  26. +19 −0 spec/support/have_configuration_option_matcher.rb
1  .gitignore
@@ -0,0 +1 @@
+pkg
3  Gemfile
@@ -0,0 +1,3 @@
+source "http://rubygems.org"
+
+gemspec
97 Gemfile.lock
@@ -0,0 +1,97 @@
+PATH
+ remote: .
+ specs:
+ pusher-fake (0.1.0)
+ em-websocket (= 0.3.5)
+ yajl-ruby (= 1.1.0)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ addressable (2.2.6)
+ bourne (1.0)
+ mocha (= 0.9.8)
+ builder (3.0.0)
+ capybara (1.1.2)
+ mime-types (>= 1.16)
+ nokogiri (>= 1.3.3)
+ rack (>= 1.0.0)
+ rack-test (>= 0.5.4)
+ selenium-webdriver (~> 2.0)
+ xpath (~> 0.1.4)
+ capybara-webkit (0.7.2)
+ capybara (>= 1.0.0, < 1.2)
+ childprocess (0.2.3)
+ ffi (~> 1.0.6)
+ cucumber (1.1.3)
+ builder (>= 2.1.2)
+ diff-lcs (>= 1.1.2)
+ gherkin (~> 2.6.7)
+ json (>= 1.4.6)
+ term-ansicolor (>= 1.0.6)
+ daemons (1.1.4)
+ diff-lcs (1.1.3)
+ em-websocket (0.3.5)
+ addressable (>= 2.1.1)
+ eventmachine (>= 0.12.9)
+ eventmachine (0.12.10)
+ ffi (1.0.11)
+ gherkin (2.6.9)
+ json (>= 1.4.6)
+ json (1.6.3)
+ mime-types (1.17.2)
+ mocha (0.9.8)
+ rake
+ multi_json (1.0.4)
+ nokogiri (1.5.0)
+ rack (1.3.5)
+ rack-protection (1.1.4)
+ rack
+ rack-test (0.6.1)
+ rack (>= 1.0)
+ rake (0.9.2.2)
+ redcarpet (2.0.0)
+ rspec (2.7.0)
+ rspec-core (~> 2.7.0)
+ rspec-expectations (~> 2.7.0)
+ rspec-mocks (~> 2.7.0)
+ rspec-core (2.7.1)
+ rspec-expectations (2.7.0)
+ diff-lcs (~> 1.1.2)
+ rspec-mocks (2.7.0)
+ rubyzip (0.9.5)
+ selenium-webdriver (2.14.0)
+ childprocess (>= 0.2.1)
+ ffi (~> 1.0.9)
+ multi_json (~> 1.0.4)
+ rubyzip
+ sinatra (1.3.1)
+ rack (~> 1.3, >= 1.3.4)
+ rack-protection (~> 1.1, >= 1.1.2)
+ tilt (~> 1.3, >= 1.3.3)
+ term-ansicolor (1.0.7)
+ thin (1.3.1)
+ daemons (>= 1.0.9)
+ eventmachine (>= 0.12.6)
+ rack (>= 1.0.0)
+ tilt (1.3.3)
+ xpath (0.1.4)
+ nokogiri (~> 1.3)
+ yajl-ruby (1.1.0)
+ yard (0.7.4)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ bourne (= 1.0.0)
+ bundler (= 1.1.rc)
+ capybara (= 1.1.2)
+ capybara-webkit (= 0.7.2)
+ cucumber (= 1.1.3)
+ pusher-fake!
+ redcarpet (= 2.0.0)
+ rspec (= 2.7.0)
+ sinatra (= 1.3.1)
+ thin (= 1.3.1)
+ yard (= 0.7.4)
21 LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2011 Tristan Dunn
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
7 README.markdown
@@ -0,0 +1,7 @@
+# pusher-fake
+
+A fake [Pusher](http://pusher.com) server for development and testing.
+
+## License
+
+pusher-fake uses the MIT license. See LICENSE for more details.
21 Rakefile
@@ -0,0 +1,21 @@
+require "bundler/setup"
+require "cucumber/rake/task"
+require "rspec/core/rake_task"
+require "yard"
+
+Bundler::GemHelper.install_tasks
+
+Cucumber::Rake::Task.new do |t|
+ t.cucumber_opts = %w{--format progress --strict}
+end
+
+RSpec::Core::RakeTask.new do |t|
+ t.pattern = "spec/**/*_spec.rb"
+end
+
+YARD::Rake::YardocTask.new do |t|
+ t.files = ["lib/**/*.rb"]
+ t.options = ["--no-private"]
+end
+
+task default: [:spec, :cucumber]
11 features/client_connect.feature
@@ -0,0 +1,11 @@
+@javascript
+Feature: Client connecting to the server
+
+ Scenario: Client connects to the server
+ Given I am on the homepage
+ Then I should be connected
+
+ @disable-server
+ Scenario: Client unsuccessfully connects to the server
+ Given I am on the homepage
+ Then I should not be connected
13 features/step_definitions/client_steps.rb
@@ -0,0 +1,13 @@
+Then "I should be connected" do
+ wait_until(2) do
+ state = page.evaluate_script("Pusher.instance.connection.state")
+ state == "connected"
+ end
+end
+
+Then "I should not be connected" do
+ wait_until(2) do
+ state = page.evaluate_script("Pusher.instance.connection.state")
+ state == "unavailable"
+ end
+end
3  features/step_definitions/navigation_steps.rb
@@ -0,0 +1,3 @@
+Given "I am on the homepage" do
+ visit "/"
+end
13 features/support/application.rb
@@ -0,0 +1,13 @@
+require "sinatra"
+
+class Sinatra::Application
+ set :root, Proc.new { File.join(File.dirname(__FILE__), "application") }
+ set :views, Proc.new { File.join(root, "views") }
+ set :public_folder, Proc.new { File.join(root, "public") }
+
+ disable :logging
+
+ get "/" do
+ erb :index
+ end
+end
7 features/support/application/public/javascripts/application.js
@@ -0,0 +1,7 @@
+window.addEventListener("load", function() {
+ // Create the client instance.
+ Pusher.instance = new Pusher("API_KEY");
+
+ // Force the connection to go unavailable after a single attempt.
+ Pusher.instance.connection.connectionAttempts = 4;
+}, false);
1,146 features/support/application/public/javascripts/vendor/pusher-1.9.6.js
@@ -0,0 +1,1146 @@
+/*!
+ * Pusher JavaScript Library v1.9.6
+ * http://pusherapp.com/
+ *
+ * Copyright 2011, Pusher
+ * Released under the MIT licence.
+ */
+
+if (typeof Function.prototype.scopedTo === 'undefined') {
+ Function.prototype.scopedTo = function(context, args) {
+ var f = this;
+ return function() {
+ return f.apply(context, Array.prototype.slice.call(args || [])
+ .concat(Array.prototype.slice.call(arguments)));
+ };
+ };
+}
+
+var Pusher = function(app_key, options) {
+ this.options = options || {};
+ this.path = '/app/' + app_key + '?client=js&version=' + Pusher.VERSION;
+ this.key = app_key;
+ this.channels = new Pusher.Channels();
+ this.global_channel = new Pusher.Channel('pusher_global_channel');
+ this.global_channel.global = true;
+
+ var self = this;
+
+ this.connection = new Pusher.Connection(this.key, this.options);
+
+ // Setup / teardown connection
+ this.connection
+ .bind('connected', function() {
+ self.subscribeAll();
+ })
+ .bind('message', function(params) {
+ self.send_local_event(params.event, params.data, params.channel);
+ })
+ .bind('disconnected', function() {
+ self.channels.disconnect();
+ })
+ .bind('error', function(err) {
+ Pusher.debug('Error', err);
+ });
+
+ Pusher.instances.push(this);
+
+ if (Pusher.isReady) self.connect();
+};
+Pusher.instances = [];
+Pusher.prototype = {
+ channel: function(name) {
+ return this.channels.find(name);
+ },
+
+ connect: function() {
+ this.connection.connect();
+ },
+
+ disconnect: function() {
+ this.connection.disconnect();
+ },
+
+ bind: function(event_name, callback) {
+ this.global_channel.bind(event_name, callback);
+ return this;
+ },
+
+ bind_all: function(callback) {
+ this.global_channel.bind_all(callback);
+ return this;
+ },
+
+ subscribeAll: function() {
+ var channel;
+ for (channel in this.channels.channels) {
+ if (this.channels.channels.hasOwnProperty(channel)) {
+ this.subscribe(channel);
+ }
+ }
+ },
+
+ subscribe: function(channel_name) {
+ var self = this;
+ var channel = this.channels.add(channel_name, this);
+ if (this.connection.state === 'connected') {
+ channel.authorize(this, function(err, data) {
+ if (err) {
+ channel.emit('subscription_error', data);
+ } else {
+ self.send_event('pusher:subscribe', {
+ channel: channel_name,
+ auth: data.auth,
+ channel_data: data.channel_data
+ });
+ }
+ });
+ }
+ return channel;
+ },
+
+ unsubscribe: function(channel_name) {
+ this.channels.remove(channel_name);
+ if (this.connection.state === 'connected') {
+ this.send_event('pusher:unsubscribe', {
+ channel: channel_name
+ });
+ }
+ },
+
+ send_event: function(event_name, data, channel) {
+ Pusher.debug("Event sent (channel,event,data)", channel, event_name, data);
+
+ var payload = {
+ event: event_name,
+ data: data
+ };
+ if (channel) payload['channel'] = channel;
+
+ this.connection.send(JSON.stringify(payload));
+ return this;
+ },
+
+ send_local_event: function(event_name, event_data, channel_name) {
+ event_data = Pusher.data_decorator(event_name, event_data);
+ if (channel_name) {
+ var channel = this.channel(channel_name);
+ if (channel) {
+ channel.dispatch_with_all(event_name, event_data);
+ }
+ } else {
+ // Bit hacky but these events won't get logged otherwise
+ Pusher.debug("Event recd (event,data)", event_name, event_data);
+ }
+
+ this.global_channel.dispatch_with_all(event_name, event_data);
+ }
+};
+
+Pusher.Util = {
+ extend: function extend(target, extensions) {
+ for (var property in extensions) {
+ if (extensions[property] && extensions[property].constructor &&
+ extensions[property].constructor === Object) {
+ target[property] = extend(target[property] || {}, extensions[property]);
+ } else {
+ target[property] = extensions[property];
+ }
+ }
+ return target;
+ }
+};
+
+// To receive log output provide a Pusher.log function, for example
+// Pusher.log = function(m){console.log(m)}
+Pusher.debug = function() {
+ if (!Pusher.log) { return }
+ var m = ["Pusher"]
+ for (var i = 0; i < arguments.length; i++){
+ if (typeof arguments[i] === "string") {
+ m.push(arguments[i])
+ } else {
+ if (window['JSON'] == undefined) {
+ m.push(arguments[i].toString());
+ } else {
+ m.push(JSON.stringify(arguments[i]))
+ }
+ }
+ };
+ Pusher.log(m.join(" : "))
+}
+
+// Pusher defaults
+Pusher.VERSION = '1.9.6';
+
+Pusher.host = 'ws.pusherapp.com';
+Pusher.ws_port = 80;
+Pusher.wss_port = 443;
+Pusher.channel_auth_endpoint = '/pusher/auth';
+Pusher.connection_timeout = 5000;
+Pusher.cdn_http = 'http://js.pusherapp.com/'
+Pusher.cdn_https = 'https://d3ds63zw57jt09.cloudfront.net/'
+Pusher.dependency_suffix = '';
+Pusher.data_decorator = function(event_name, event_data){ return event_data }; // wrap event_data before dispatching
+Pusher.allow_reconnect = true;
+Pusher.channel_auth_transport = 'ajax';
+
+Pusher.isReady = false;
+Pusher.ready = function() {
+ Pusher.isReady = true;
+ for (var i = 0, l = Pusher.instances.length; i < l; i++) {
+ Pusher.instances[i].connect();
+ }
+};
+
+;(function() {
+/* Abstract event binding
+Example:
+
+ var MyEventEmitter = function(){};
+ MyEventEmitter.prototype = new Pusher.EventsDispatcher;
+
+ var emitter = new MyEventEmitter();
+
+ // Bind to single event
+ emitter.bind('foo_event', function(data){ alert(data)} );
+
+ // Bind to all
+ emitter.bind_all(function(event_name, data){ alert(data) });
+
+--------------------------------------------------------*/
+ function EventsDispatcher() {
+ this.callbacks = {};
+ this.global_callbacks = [];
+ }
+
+ EventsDispatcher.prototype.bind = function(event_name, callback) {
+ this.callbacks[event_name] = this.callbacks[event_name] || [];
+ this.callbacks[event_name].push(callback);
+ return this;// chainable
+ };
+
+ EventsDispatcher.prototype.emit = function(event_name, data) {
+ this.dispatch_global_callbacks(event_name, data);
+ this.dispatch(event_name, data);
+ return this;
+ };
+
+ EventsDispatcher.prototype.bind_all = function(callback) {
+ this.global_callbacks.push(callback);
+ return this;
+ };
+
+ EventsDispatcher.prototype.dispatch = function(event_name, event_data) {
+ var callbacks = this.callbacks[event_name];
+
+ if (callbacks) {
+ for (var i = 0; i < callbacks.length; i++) {
+ callbacks[i](event_data);
+ }
+ } else {
+ // Log is un-necessary in case of global channel or connection object
+ if (!(this.global || this instanceof Pusher.Connection || this instanceof Pusher.Machine)) {
+ Pusher.debug('No callbacks for ' + event_name, event_data);
+ }
+ }
+ };
+
+ EventsDispatcher.prototype.dispatch_global_callbacks = function(event_name, data) {
+ for (var i = 0; i < this.global_callbacks.length; i++) {
+ this.global_callbacks[i](event_name, data);
+ }
+ };
+
+ EventsDispatcher.prototype.dispatch_with_all = function(event_name, data) {
+ this.dispatch(event_name, data);
+ this.dispatch_global_callbacks(event_name, data);
+ };
+
+ this.Pusher.EventsDispatcher = EventsDispatcher;
+}).call(this);
+
+;(function() {
+ var Pusher = this.Pusher;
+
+ /*-----------------------------------------------
+ Helpers:
+ -----------------------------------------------*/
+
+ // MSIE doesn't have array.indexOf
+ var nativeIndexOf = Array.prototype.indexOf;
+ function indexOf(array, item) {
+ if (array == null) return -1;
+ if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
+ for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
+ return -1;
+ }
+
+
+ function capitalize(str) {
+ return str.substr(0, 1).toUpperCase() + str.substr(1);
+ }
+
+
+ function safeCall(method, obj, data) {
+ if (obj[method] !== undefined) {
+ obj[method](data);
+ }
+ }
+
+ /*-----------------------------------------------
+ The State Machine
+ -----------------------------------------------*/
+ function Machine(actor, initialState, transitions, stateActions) {
+ Pusher.EventsDispatcher.call(this);
+
+ this.actor = actor;
+ this.state = undefined;
+ this.errors = [];
+
+ // functions for each state
+ this.stateActions = stateActions;
+
+ // set up the transitions
+ this.transitions = transitions;
+
+ this.transition(initialState);
+ };
+
+ Machine.prototype.transition = function(nextState, data) {
+ var prevState = this.state;
+ var stateCallbacks = this.stateActions;
+
+ if (prevState && (indexOf(this.transitions[prevState], nextState) == -1)) {
+ throw new Error(this.actor.key + ': Invalid transition [' + prevState + ' to ' + nextState + ']');
+ }
+
+ // exit
+ safeCall(prevState + 'Exit', stateCallbacks, data);
+
+ // tween
+ safeCall(prevState + 'To' + capitalize(nextState), stateCallbacks, data);
+
+ // pre
+ safeCall(nextState + 'Pre', stateCallbacks, data);
+
+ // change state:
+ this.state = nextState;
+
+ // handy to bind to
+ this.emit('state_change', {
+ oldState: prevState,
+ newState: nextState
+ });
+
+ // Post:
+ safeCall(nextState + 'Post', stateCallbacks, data);
+ };
+
+ Machine.prototype.is = function(state) {
+ return this.state === state;
+ };
+
+ Machine.prototype.isNot = function(state) {
+ return this.state !== state;
+ };
+
+ Pusher.Util.extend(Machine.prototype, Pusher.EventsDispatcher.prototype);
+
+ this.Pusher.Machine = Machine;
+}).call(this);
+
+;(function() {
+ var Pusher = this.Pusher;
+
+ /*
+ A little bauble to interface with window.navigator.onLine,
+ window.ononline and window.onoffline. Easier to mock.
+ */
+ var NetInfo = function() {
+ var self = this;
+ Pusher.EventsDispatcher.call(this);
+ // This is okay, as IE doesn't support this stuff anyway.
+ if (window.addEventListener !== undefined) {
+ window.addEventListener("online", function() {
+ self.emit('online', null);
+ }, false);
+ window.addEventListener("offline", function() {
+ self.emit('offline', null);
+ }, false);
+ }
+ };
+
+ // Offline means definitely offline (no connection to router).
+ // Inverse does NOT mean definitely online (only currently supported in Safari
+ // and even there only means the device has a connection to the router).
+ NetInfo.prototype.isOnLine = function() {
+ if (window.navigator.onLine === undefined) {
+ return true;
+ } else {
+ return window.navigator.onLine;
+ }
+ };
+
+ Pusher.Util.extend(NetInfo.prototype, Pusher.EventsDispatcher.prototype);
+ this.Pusher.NetInfo = Pusher.NetInfo = NetInfo;
+
+ var machineTransitions = {
+ 'initialized': ['waiting', 'failed'],
+ 'waiting': ['connecting', 'permanentlyClosed'],
+ 'connecting': ['open', 'permanentlyClosing', 'impermanentlyClosing', 'waiting'],
+ 'open': ['connected', 'permanentlyClosing', 'impermanentlyClosing', 'waiting'],
+ 'connected': ['permanentlyClosing', 'impermanentlyClosing', 'waiting'],
+ 'impermanentlyClosing': ['waiting', 'permanentlyClosing'],
+ 'permanentlyClosing': ['permanentlyClosed'],
+ 'permanentlyClosed': ['waiting'],
+ 'failed': ['permanentlyClosing']
+ };
+
+
+ // Amount to add to time between connection attemtpts per failed attempt.
+ var UNSUCCESSFUL_CONNECTION_ATTEMPT_ADDITIONAL_WAIT = 2000;
+ var UNSUCCESSFUL_OPEN_ATTEMPT_ADDITIONAL_TIMEOUT = 2000;
+ var UNSUCCESSFUL_CONNECTED_ATTEMPT_ADDITIONAL_TIMEOUT = 2000;
+
+ var MAX_CONNECTION_ATTEMPT_WAIT = 5 * UNSUCCESSFUL_CONNECTION_ATTEMPT_ADDITIONAL_WAIT;
+ var MAX_OPEN_ATTEMPT_TIMEOUT = 5 * UNSUCCESSFUL_OPEN_ATTEMPT_ADDITIONAL_TIMEOUT;
+ var MAX_CONNECTED_ATTEMPT_TIMEOUT = 5 * UNSUCCESSFUL_CONNECTED_ATTEMPT_ADDITIONAL_TIMEOUT;
+
+ function resetConnectionParameters(connection) {
+ connection.connectionWait = 0;
+
+ if (Pusher.TransportType === 'flash') {
+ // Flash needs a bit more time
+ connection.openTimeout = 5000;
+ } else {
+ connection.openTimeout = 2000;
+ }
+ connection.connectedTimeout = 2000;
+ connection.connectionSecure = connection.compulsorySecure;
+ connection.connectionAttempts = 0;
+ }
+
+ function Connection(key, options) {
+ var self = this;
+
+ Pusher.EventsDispatcher.call(this);
+
+ this.options = Pusher.Util.extend({encrypted: false}, options || {});
+
+ this.netInfo = new Pusher.NetInfo();
+
+ this.netInfo.bind('online', function(){
+ if (self._machine.is('waiting')) {
+ self._machine.transition('connecting');
+ triggerStateChange('connecting');
+ }
+ });
+
+ this.netInfo.bind('offline', function() {
+ if (self._machine.is('connected')) {
+ // These are for Chrome 15, which ends up
+ // having two sockets hanging around.
+ self.socket.onclose = undefined;
+ self.socket.onmessage = undefined;
+ self.socket.onerror = undefined;
+ self.socket.onopen = undefined;
+
+ self.socket.close();
+ self.socket = undefined;
+ self._machine.transition('waiting');
+ }
+ });
+
+ // define the state machine that runs the connection
+ this._machine = new Pusher.Machine(self, 'initialized', machineTransitions, {
+
+ // TODO: Use the constructor for this.
+ initializedPre: function() {
+ self.compulsorySecure = self.options.encrypted;
+
+ self.key = key;
+ self.socket = null;
+ self.socket_id = null;
+
+ self.state = 'initialized';
+ },
+
+ waitingPre: function() {
+ if (self.connectionWait > 0) {
+ informUser('connecting_in', self.connectionWait);
+ }
+
+ if (self.netInfo.isOnLine() === false || self.connectionAttempts > 4){
+ triggerStateChange('unavailable');
+ } else {
+ triggerStateChange('connecting');
+ }
+
+ if (self.netInfo.isOnLine() === true) {
+ self._waitingTimer = setTimeout(function() {
+ self._machine.transition('connecting');
+ }, self.connectionWait);
+ }
+ },
+
+ waitingExit: function() {
+ clearTimeout(self._waitingTimer);
+ },
+
+ connectingPre: function() {
+ // Case that a user manages to get to the connecting
+ // state even when offline.
+ if (self.netInfo.isOnLine() === false) {
+ self._machine.transition('waiting');
+ triggerStateChange('unavailable');
+
+ return;
+ }
+
+ // removed: if not closed, something is wrong that we should fix
+ // if(self.socket !== undefined) self.socket.close();
+ var url = formatURL(self.key, self.connectionSecure);
+ Pusher.debug('Connecting', url);
+ self.socket = new Pusher.Transport(url);
+ // now that the socket connection attempt has been started,
+ // set up the callbacks fired by the socket for different outcomes
+ self.socket.onopen = ws_onopen;
+ self.socket.onclose = transitionToWaiting;
+ self.socket.onerror = ws_onError;
+
+ // allow time to get ws_onOpen, otherwise close socket and try again
+ self._connectingTimer = setTimeout(TransitionToImpermanentClosing, self.openTimeout);
+ },
+
+ connectingExit: function() {
+ clearTimeout(self._connectingTimer);
+ },
+
+ connectingToWaiting: function() {
+ updateConnectionParameters();
+
+ // FUTURE: update only ssl
+ },
+
+ connectingToImpermanentlyClosing: function() {
+ updateConnectionParameters();
+
+ // FUTURE: update only timeout
+ },
+
+ openPre: function() {
+ self.socket.onmessage = ws_onMessage;
+ self.socket.onerror = ws_onError;
+ self.socket.onclose = transitionToWaiting;
+
+ // allow time to get connected-to-Pusher message, otherwise close socket, try again
+ self._openTimer = setTimeout(TransitionToImpermanentClosing, self.connectedTimeout);
+ },
+
+ openExit: function() {
+ clearTimeout(self._openTimer);
+ },
+
+ openToWaiting: function() {
+ updateConnectionParameters();
+ },
+
+ openToImpermanentlyClosing: function() {
+ updateConnectionParameters();
+ },
+
+ connectedPre: function(socket_id) {
+ self.socket_id = socket_id;
+
+ self.socket.onmessage = ws_onMessage;
+ self.socket.onerror = ws_onError;
+ self.socket.onclose = transitionToWaiting;
+
+ resetConnectionParameters(self);
+ },
+
+ connectedPost: function() {
+ triggerStateChange('connected');
+ },
+
+ connectedExit: function() {
+ triggerStateChange('disconnected');
+ },
+
+ impermanentlyClosingPost: function() {
+ if (self.socket) {
+ self.socket.onclose = transitionToWaiting;
+ self.socket.close();
+ }
+ },
+
+ permanentlyClosingPost: function() {
+ if (self.socket) {
+ self.socket.onclose = function() {
+ resetConnectionParameters(self);
+ self._machine.transition('permanentlyClosed');
+ };
+
+ self.socket.close();
+ } else {
+ resetConnectionParameters(self);
+ self._machine.transition('permanentlyClosed');
+ }
+ },
+
+ failedPre: function() {
+ triggerStateChange('failed');
+ Pusher.debug('WebSockets are not available in this browser.');
+ }
+ });
+
+ /*-----------------------------------------------
+ -----------------------------------------------*/
+
+ function updateConnectionParameters() {
+ if (self.connectionWait < MAX_CONNECTION_ATTEMPT_WAIT) {
+ self.connectionWait += UNSUCCESSFUL_CONNECTION_ATTEMPT_ADDITIONAL_WAIT;
+ }
+
+ if (self.openTimeout < MAX_OPEN_ATTEMPT_TIMEOUT) {
+ self.openTimeout += UNSUCCESSFUL_OPEN_ATTEMPT_ADDITIONAL_TIMEOUT;
+ }
+
+ if (self.connectedTimeout < MAX_CONNECTED_ATTEMPT_TIMEOUT) {
+ self.connectedTimeout += UNSUCCESSFUL_CONNECTED_ATTEMPT_ADDITIONAL_TIMEOUT;
+ }
+
+ if (self.compulsorySecure !== true) {
+ self.connectionSecure = !self.connectionSecure;
+ }
+
+ self.connectionAttempts++;
+ }
+
+ function formatURL(key, isSecure) {
+ var port = Pusher.ws_port;
+ var protocol = 'ws://';
+
+ // Always connect with SSL if the current page has
+ // been loaded via HTTPS.
+ //
+ // FUTURE: Always connect using SSL.
+ //
+ if (isSecure || document.location.protocol === 'https:') {
+ port = Pusher.wss_port;
+ protocol = 'wss://';
+ }
+
+ return protocol + Pusher.host + ':' + port + '/app/' + key + '?client=js&version=' + Pusher.VERSION;
+ }
+
+ // callback for close and retry. Used on timeouts.
+ function TransitionToImpermanentClosing() {
+ self._machine.transition('impermanentlyClosing');
+ }
+
+ /*-----------------------------------------------
+ WebSocket Callbacks
+ -----------------------------------------------*/
+
+ // no-op, as we only care when we get pusher:connection_established
+ function ws_onopen() {
+ self._machine.transition('open');
+ };
+
+ function ws_onMessage(event) {
+ var params = parseWebSocketEvent(event);
+
+ // case of invalid JSON payload sent
+ // we have to handle the error in the parseWebSocketEvent
+ // method as JavaScript error objects are kinda icky.
+ if (typeof params === 'undefined') return;
+
+ Pusher.debug('Event recd (event,data)', params.event, params.data);
+
+ // Continue to work with valid payloads:
+ if (params.event === 'pusher:connection_established') {
+ self._machine.transition('connected', params.data.socket_id);
+ } else if (params.event === 'pusher:error') {
+ // first inform the end-developer of this error
+ informUser('error', {type: 'PusherError', data: params.data});
+
+ // App not found by key - close connection
+ if (params.data.code === 4001) {
+ self._machine.transition('permanentlyClosing');
+ }
+
+ if (params.data.code === 4000) {
+ Pusher.debug(params.data.message);
+
+ self.compulsorySecure = true;
+ self.connectionSecure = true;
+ self.options.encrypted = true;
+ }
+ } else if (params.event === 'pusher:heartbeat') {
+ } else if (self._machine.is('connected')) {
+ informUser('message', params);
+ }
+ }
+
+
+ /**
+ * Parses an event from the WebSocket to get
+ * the JSON payload that we require
+ *
+ * @param {MessageEvent} event The event from the WebSocket.onmessage handler.
+ **/
+ function parseWebSocketEvent(event) {
+ try {
+ var params = JSON.parse(event.data);
+
+ if (typeof params.data === 'string') {
+ try {
+ params.data = JSON.parse(params.data);
+ } catch (e) {
+ if (!(e instanceof SyntaxError)) {
+ throw e;
+ }
+ }
+ }
+
+ return params;
+ } catch (e) {
+ informUser('error', {type: 'MessageParseError', error: e, data: event.data});
+ }
+ }
+
+ function transitionToWaiting() {
+ self._machine.transition('waiting');
+ }
+
+ function ws_onError() {
+ informUser('error', {
+ type: 'WebSocketError'
+ });
+
+ // note: required? is the socket auto closed in the case of error?
+ self.socket.close();
+ self._machine.transition('impermanentlyClosing');
+ }
+
+ function informUser(eventName, data) {
+ self.emit(eventName, data);
+ }
+
+ function triggerStateChange(newState, data) {
+ // avoid emitting and changing the state
+ // multiple times when it's the same.
+ if (self.state === newState) return;
+
+ var prevState = self.state;
+
+ self.state = newState;
+
+ Pusher.debug('State changed', prevState + ' -> ' + newState);
+
+ self.emit('state_change', {previous: prevState, current: newState});
+ self.emit(newState, data);
+ }
+ };
+
+ Connection.prototype.connect = function() {
+ // no WebSockets
+ if (Pusher.Transport === null || typeof Pusher.Transport === 'undefined') {
+ this._machine.transition('failed');
+ }
+ // initial open of connection
+ else if(this._machine.is('initialized')) {
+ resetConnectionParameters(this);
+ this._machine.transition('waiting');
+ }
+ // user skipping connection wait
+ else if (this._machine.is('waiting') && this.netInfo.isOnLine() === true) {
+ this._machine.transition('connecting');
+ }
+ // user re-opening connection after closing it
+ else if(this._machine.is("permanentlyClosed")) {
+ this._machine.transition('waiting');
+ }
+ };
+
+ Connection.prototype.send = function(data) {
+ if (this._machine.is('connected')) {
+ this.socket.send(data);
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ Connection.prototype.disconnect = function() {
+ if (this._machine.is('permanentlyClosed')) {
+ return;
+ }
+
+ Pusher.debug('Disconnecting');
+
+ if (this._machine.is('waiting')) {
+ this._machine.transition('permanentlyClosed');
+ } else {
+ this._machine.transition('permanentlyClosing');
+ }
+ };
+
+ Pusher.Util.extend(Connection.prototype, Pusher.EventsDispatcher.prototype);
+ this.Pusher.Connection = Connection;
+}).call(this);
+
+Pusher.Channels = function() {
+ this.channels = {};
+};
+
+Pusher.Channels.prototype = {
+ add: function(channel_name, pusher) {
+ var existing_channel = this.find(channel_name);
+ if (!existing_channel) {
+ var channel = Pusher.Channel.factory(channel_name, pusher);
+ this.channels[channel_name] = channel;
+ return channel;
+ } else {
+ return existing_channel;
+ }
+ },
+
+ find: function(channel_name) {
+ return this.channels[channel_name];
+ },
+
+ remove: function(channel_name) {
+ delete this.channels[channel_name];
+ },
+
+ disconnect: function () {
+ for(var channel_name in this.channels){
+ this.channels[channel_name].disconnect()
+ }
+ }
+};
+
+Pusher.Channel = function(channel_name, pusher) {
+ Pusher.EventsDispatcher.call(this);
+
+ this.pusher = pusher;
+ this.name = channel_name;
+ this.subscribed = false;
+};
+
+Pusher.Channel.prototype = {
+ // inheritable constructor
+ init: function(){
+
+ },
+
+ disconnect: function(){
+
+ },
+
+ // Activate after successful subscription. Called on top-level pusher:subscription_succeeded
+ acknowledge_subscription: function(data){
+ this.subscribed = true;
+ },
+
+ is_private: function(){
+ return false;
+ },
+
+ is_presence: function(){
+ return false;
+ },
+
+ authorize: function(pusher, callback){
+ callback(false, {}); // normal channels don't require auth
+ },
+
+ trigger: function(event, data) {
+ this.pusher.send_event(event, data, this.name);
+ return this;
+ }
+};
+
+Pusher.Util.extend(Pusher.Channel.prototype, Pusher.EventsDispatcher.prototype);
+
+
+
+Pusher.auth_callbacks = {};
+
+Pusher.authorizers = {
+ ajax: function(pusher, callback){
+ var self = this, xhr;
+
+ if (Pusher.XHR) {
+ xhr = new Pusher.XHR();
+ } else {
+ xhr = (window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"));
+ }
+
+ xhr.open("POST", Pusher.channel_auth_endpoint, true);
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200) {
+ var data, parsed = false;
+
+ try {
+ data = JSON.parse(xhr.responseText);
+ parsed = true;
+ } catch (e) {
+ callback(true, 'JSON returned from webapp was invalid, yet status code was 200. Data was: ' + xhr.responseText);
+ }
+
+ if (parsed) { // prevents double execution.
+ callback(false, data);
+ }
+ } else {
+ Pusher.debug("Couldn't get auth info from your webapp", status);
+ callback(true, xhr.status);
+ }
+ }
+ };
+ xhr.send('socket_id=' + encodeURIComponent(pusher.connection.socket_id) + '&channel_name=' + encodeURIComponent(self.name));
+ },
+ jsonp: function(pusher, callback){
+ var qstring = 'socket_id=' + encodeURIComponent(pusher.connection.socket_id) + '&channel_name=' + encodeURIComponent(this.name);
+ var script = document.createElement("script");
+ // Hacked wrapper.
+ Pusher.auth_callbacks[this.name] = function(data) {
+ callback(false, data);
+ };
+ var callback_name = "Pusher.auth_callbacks['" + this.name + "']";
+ script.src = Pusher.channel_auth_endpoint+'?callback='+encodeURIComponent(callback_name)+'&'+qstring;
+ var head = document.getElementsByTagName("head")[0] || document.documentElement;
+ head.insertBefore( script, head.firstChild );
+ }
+};
+
+Pusher.Channel.PrivateChannel = {
+ is_private: function(){
+ return true;
+ },
+
+ authorize: function(pusher, callback){
+ Pusher.authorizers[Pusher.channel_auth_transport].scopedTo(this)(pusher, callback);
+ }
+};
+
+Pusher.Channel.PresenceChannel = {
+
+ init: function(){
+ this.bind('pusher_internal:subscription_succeeded', function(sub_data){
+ this.acknowledge_subscription(sub_data);
+ this.dispatch_with_all('pusher:subscription_succeeded', this.members);
+ }.scopedTo(this));
+
+ this.bind('pusher_internal:member_added', function(data){
+ var member = this.members.add(data.user_id, data.user_info);
+ this.dispatch_with_all('pusher:member_added', member);
+ }.scopedTo(this))
+
+ this.bind('pusher_internal:member_removed', function(data){
+ var member = this.members.remove(data.user_id);
+ if (member) {
+ this.dispatch_with_all('pusher:member_removed', member);
+ }
+ }.scopedTo(this))
+ },
+
+ disconnect: function(){
+ this.members.clear();
+ },
+
+ acknowledge_subscription: function(sub_data){
+ this.members._members_map = sub_data.presence.hash;
+ this.members.count = sub_data.presence.count;
+ this.subscribed = true;
+ },
+
+ is_presence: function(){
+ return true;
+ },
+
+ members: {
+ _members_map: {},
+ count: 0,
+
+ each: function(callback) {
+ for(var i in this._members_map) {
+ callback({
+ id: i,
+ info: this._members_map[i]
+ });
+ }
+ },
+
+ add: function(id, info) {
+ this._members_map[id] = info;
+ this.count++;
+ return this.get(id);
+ },
+
+ remove: function(user_id) {
+ var member = this.get(user_id);
+ if (member) {
+ delete this._members_map[user_id];
+ this.count--;
+ }
+ return member;
+ },
+
+ get: function(user_id) {
+ if (this._members_map.hasOwnProperty(user_id)) { // have heard of this user user_id
+ return {
+ id: user_id,
+ info: this._members_map[user_id]
+ }
+ } else { // have never heard of this user
+ return null;
+ }
+ },
+
+ clear: function() {
+ this._members_map = {};
+ this.count = 0;
+ }
+ }
+};
+
+Pusher.Channel.factory = function(channel_name, pusher){
+ var channel = new Pusher.Channel(channel_name, pusher);
+ if(channel_name.indexOf(Pusher.Channel.private_prefix) === 0) {
+ Pusher.Util.extend(channel, Pusher.Channel.PrivateChannel);
+ } else if(channel_name.indexOf(Pusher.Channel.presence_prefix) === 0) {
+ Pusher.Util.extend(channel, Pusher.Channel.PrivateChannel);
+ Pusher.Util.extend(channel, Pusher.Channel.PresenceChannel);
+ };
+ channel.init();// inheritable constructor
+ return channel;
+};
+
+Pusher.Channel.private_prefix = "private-";
+Pusher.Channel.presence_prefix = "presence-";
+
+var _require = (function () {
+
+ var handleScriptLoaded;
+ if (document.addEventListener) {
+ handleScriptLoaded = function (elem, callback) {
+ elem.addEventListener('load', callback, false)
+ }
+ } else {
+ handleScriptLoaded = function(elem, callback) {
+ elem.attachEvent('onreadystatechange', function () {
+ if(elem.readyState == 'loaded' || elem.readyState == 'complete') callback()
+ })
+ }
+ }
+
+ return function (deps, callback) {
+ var dep_count = 0,
+ dep_length = deps.length;
+
+ function checkReady (callback) {
+ dep_count++;
+ if ( dep_length == dep_count ) {
+ // Opera needs the timeout for page initialization weirdness
+ setTimeout(callback, 0);
+ }
+ }
+
+ function addScript (src, callback) {
+ callback = callback || function(){}
+ var head = document.getElementsByTagName('head')[0];
+ var script = document.createElement('script');
+ script.setAttribute('src', src);
+ script.setAttribute("type","text/javascript");
+ script.setAttribute('async', true);
+
+ handleScriptLoaded(script, function () {
+ checkReady(callback);
+ });
+
+ head.appendChild(script);
+ }
+
+ for(var i = 0; i < dep_length; i++) {
+ addScript(deps[i], callback);
+ }
+ }
+})();
+
+;(function() {
+ var cdn = (document.location.protocol == 'http:') ? Pusher.cdn_http : Pusher.cdn_https;
+ var root = cdn + Pusher.VERSION;
+ var deps = [];
+
+ if (typeof window['JSON'] === 'undefined') {
+ deps.push(root + '/json2' + Pusher.dependency_suffix + '.js');
+ }
+ if (typeof window['WebSocket'] === 'undefined' && typeof window['MozWebSocket'] === 'undefined') {
+ // We manually initialize web-socket-js to iron out cross browser issues
+ window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true;
+ deps.push(root + '/flashfallback' + Pusher.dependency_suffix + '.js');
+ }
+
+ var initialize = function() {
+ if (typeof window['WebSocket'] === 'undefined' && typeof window['MozWebSocket'] === 'undefined') {
+ return function() {
+ // This runs after flashfallback.js has loaded
+ if (typeof window['WebSocket'] !== 'undefined' && typeof window['MozWebSocket'] === 'undefined') {
+ // window['WebSocket'] is a flash emulation of WebSocket
+ Pusher.Transport = window['WebSocket'];
+ Pusher.TransportType = 'flash';
+
+ window.WEB_SOCKET_SWF_LOCATION = root + "/WebSocketMain.swf";
+ WebSocket.__addTask(function() {
+ Pusher.ready();
+ })
+ WebSocket.__initialize();
+ } else {
+ // Flash must not be installed
+ Pusher.Transport = null;
+ Pusher.TransportType = 'none';
+ Pusher.ready();
+ }
+ }
+ } else {
+ return function() {
+ // This is because Mozilla have decided to
+ // prefix the WebSocket constructor with "Moz".
+ if (typeof window['MozWebSocket'] !== 'undefined') {
+ Pusher.Transport = window['MozWebSocket'];
+ } else {
+ Pusher.Transport = window['WebSocket'];
+ }
+ // We have some form of a native websocket,
+ // even if the constructor is prefixed:
+ Pusher.TransportType = 'native';
+
+ // Initialise Pusher.
+ Pusher.ready();
+ }
+ }
+ }();
+
+ var ondocumentbody = function(callback) {
+ var load_body = function() {
+ document.body ? callback() : setTimeout(load_body, 0);
+ }
+ load_body();
+ };
+
+ var initializeOnDocumentBody = function() {
+ ondocumentbody(initialize);
+ }
+
+ if (deps.length > 0) {
+ _require(deps, initializeOnDocumentBody);
+ } else {
+ initializeOnDocumentBody();
+ }
+})();
16 features/support/application/views/index.erb
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>PusherFake Test Application</title>
+</head>
+<body>
+
+<script src="/javascripts/vendor/pusher-1.9.6.js"></script>
+<script src="/javascripts/application.js"></script>
+<script>
+Pusher.host = <%= PusherFake.configuration.host.inspect %>;
+Pusher.ws_port = <%= PusherFake.configuration.port %>;
+</script>
+
+</body>
+</html>
12 features/support/environment.rb
@@ -0,0 +1,12 @@
+require "rubygems"
+require "bundler/setup"
+require "capybara/cucumber"
+
+Bundler.require(:default, :development)
+
+Dir[File.expand_path("../support/**/*.rb", __FILE__)].each do |file|
+ require file
+end
+
+Capybara.app = Sinatra::Application
+Capybara.javascript_driver = :webkit
29 features/support/pusher-fake-instance.rb
@@ -0,0 +1,29 @@
+module PusherFake
+ class Instance
+ class << self
+ attr_accessor :instance
+ end
+
+ def self.start
+ self.instance ||= fork { PusherFake::Server.start }
+ end
+
+ def self.stop
+ Process.kill("QUIT", self.instance) if self.instance
+ end
+ end
+end
+
+PusherFake::Instance.start
+
+Before("@disable-server") do
+ PusherFake::Instance.stop
+end
+
+After("@disable-server") do
+ PusherFake::Instance.start
+end
+
+at_exit do
+ PusherFake::Instance.stop
+end
26 lib/pusher-fake.rb
@@ -0,0 +1,26 @@
+require "em-websocket"
+require "yajl"
+
+require "pusher-fake/configuration"
+require "pusher-fake/connection"
+require "pusher-fake/server"
+require "pusher-fake/version"
+
+module PusherFake
+ # Call this method to modify the defaults.
+ #
+ # @example
+ # PusherFake.configure do |configuration|
+ # configuration.port = 443
+ # end
+ #
+ # @yield [Configuration] The current configuration.
+ def self.configure
+ yield(configuration)
+ end
+
+ # @return [Configuration] Current configuration.
+ def self.configuration
+ @@configuration ||= Configuration.new
+ end
+end
15 lib/pusher-fake/configuration.rb
@@ -0,0 +1,15 @@
+module PusherFake
+ class Configuration
+ # @return [String] The host on which the WebSocket server listens. (Defaults to +127.0.0.1+.)
+ attr_accessor :host
+
+ # @return [Fixnum] The port on which the WebSocket server listens. (Defaults to +8080+.)
+ attr_accessor :port
+
+ # Instantiated from {PusherFake.configuration}. Sets the defaults.
+ def initialize
+ self.host = "127.0.0.1"
+ self.port = 8080
+ end
+ end
+end
29 lib/pusher-fake/connection.rb
@@ -0,0 +1,29 @@
+module PusherFake
+ class Connection
+ # @return [EventMachine::WebSocket::Connection] The socket object for this connection.
+ attr_reader :socket
+
+ # Create a new {Connection} object.
+ #
+ # @param [EventMachine::WebSocket::Connection] socket The socket object for the connection.
+ def initialize(socket)
+ @socket = socket
+ end
+
+ # Emit an event to the connection.
+ #
+ # @param [String] event The event name.
+ # @param [Hash] data The event data.
+ def emit(event, data = {})
+ message = { event: event, data: data }
+ message = Yajl::Encoder.encode(message)
+
+ socket.send(message)
+ end
+
+ # Notifies the Pusher client that a connection has been established.
+ def establish
+ emit("pusher:connection_established", socket_id: socket.object_id)
+ end
+ end
+end
23 lib/pusher-fake/server.rb
@@ -0,0 +1,23 @@
+module PusherFake
+ class Server
+ # Start the WebSocket server.
+ def self.start
+ configuration = PusherFake.configuration
+ options = { host: configuration.host, port: configuration.port }
+
+ EventMachine::WebSocket.start(options) do |socket|
+ socket.onopen { onopen(socket) }
+ end
+ end
+
+ # Creates and establishes a new connection.
+ #
+ # @param [EventMachine::WebSocket::Connection] socket The socket object for the connection.
+ def self.onopen(socket)
+ EventMachine.next_tick do
+ connection = Connection.new(socket)
+ connection.establish
+ end
+ end
+ end
+end
3  lib/pusher-fake/version.rb
@@ -0,0 +1,3 @@
+module PusherFake
+ VERSION = "0.1.0"
+end
29 pusher-fake.gemspec
@@ -0,0 +1,29 @@
+Gem::Specification.new do |s|
+ s.name = "pusher-fake"
+ s.version = "0.1.0"
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["Tristan Dunn"]
+ s.email = "hello@tristandunn.com"
+ s.homepage = "http://github.com/tristandunn/pusher-fake"
+ s.summary = "A fake Pusher server for development and testing."
+ s.description = "A fake Pusher server for development and testing."
+ s.license = "MIT"
+
+ s.files = Dir["lib/**/*"].to_a
+ s.test_files = Dir["{features,spec}/**/*"].to_a
+ s.require_path = "lib"
+
+ s.add_dependency "em-websocket", "0.3.5"
+ s.add_dependency "yajl-ruby", "1.1.0"
+
+ s.add_development_dependency "bourne", "1.0.0"
+ s.add_development_dependency "bundler", "1.1.rc"
+ s.add_development_dependency "capybara", "1.1.2"
+ s.add_development_dependency "capybara-webkit", "0.7.2"
+ s.add_development_dependency "cucumber", "1.1.3"
+ s.add_development_dependency "redcarpet", "2.0.0"
+ s.add_development_dependency "rspec", "2.7.0"
+ s.add_development_dependency "sinatra", "1.3.1"
+ s.add_development_dependency "thin", "1.3.1"
+ s.add_development_dependency "yard", "0.7.4"
+end
6 spec/lib/pusher-fake/configuration_spec.rb
@@ -0,0 +1,6 @@
+require "spec_helper"
+
+describe PusherFake::Configuration do
+ it { should have_configuration_option(:host).with_default("127.0.0.1") }
+ it { should have_configuration_option(:port).with_default(8080) }
+end
42 spec/lib/pusher-fake/connection_spec.rb
@@ -0,0 +1,42 @@
+require "spec_helper"
+
+describe PusherFake::Connection do
+ let(:socket) { stub }
+
+ subject { PusherFake::Connection }
+
+ it "assigns the provided socket" do
+ connection = subject.new(socket)
+ connection.socket.should == socket
+ end
+end
+
+describe PusherFake::Connection, "#emit" do
+ let(:data) { { some: "data", good: true } }
+ let(:json) { Yajl::Encoder.encode(message) }
+ let(:event) { "name" }
+ let(:socket) { stub(:send) }
+ let(:message) { { event: event, data: data } }
+
+ subject { PusherFake::Connection.new(socket) }
+
+ it "sends the event to the socket as JSON" do
+ subject.emit(event, data)
+ socket.should have_received(:send).with(json)
+ end
+end
+
+describe PusherFake::Connection, "#establish" do
+ let(:socket) { stub }
+
+ subject { PusherFake::Connection.new(socket) }
+
+ before do
+ subject.stubs(:emit)
+ end
+
+ it "emits the connection established event with the socket ID" do
+ subject.establish
+ subject.should have_received(:emit).with("pusher:connection_established", socket_id: socket.object_id)
+ end
+end
54 spec/lib/pusher-fake/server_spec.rb
@@ -0,0 +1,54 @@
+require "spec_helper"
+
+describe PusherFake::Server, ".start" do
+ let(:socket) { stub }
+ let(:options) { { host: configuration.host, port: configuration.port } }
+ let(:configuration) { stub(host: "192.168.0.1", port: 8181) }
+
+ subject { PusherFake::Server }
+
+ before do
+ socket.stubs(:onopen)
+ subject.stubs(:onopen)
+ PusherFake.stubs(:configuration).returns(configuration)
+ EventMachine::WebSocket.stubs(:start).yields(socket)
+ end
+
+ it "creates a WebSocket server" do
+ subject.start
+ EventMachine::WebSocket.should have_received(:start).with(options)
+ end
+
+ it "defines an open callback" do
+ subject.start
+ socket.should have_received(:onopen).with()
+ end
+
+ it "triggers onopen with the socket yields to onopen" do
+ socket.stubs(:onopen).yields
+ subject.start
+ subject.should have_received(:onopen).with(socket)
+ end
+end
+
+describe PusherFake::Server, ".onopen" do
+ let(:socket) { stub }
+ let(:connection) { stub(:establish) }
+
+ subject { PusherFake::Server }
+
+ before do
+ EventMachine.stubs(:next_tick).yields
+ PusherFake::Connection.stubs(:new).returns(connection)
+ end
+
+ it "creates a connection with the provided socket" do
+ subject.onopen(socket)
+ PusherFake::Connection.should have_received(:new).with(socket)
+ end
+
+ it "establishes the connection" do
+ subject.onopen(socket)
+ connection.should have_received(:establish).with()
+ end
+end
12 spec/spec_helper.rb
@@ -0,0 +1,12 @@
+require "rubygems"
+require "bundler/setup"
+
+Bundler.require(:default, :development)
+
+Dir[File.expand_path("../support/**/*.rb", __FILE__)].each do |file|
+ require file
+end
+
+RSpec.configure do |config|
+ config.mock_with :mocha
+end
19 spec/support/have_configuration_option_matcher.rb
@@ -0,0 +1,19 @@
+RSpec::Matchers.define :have_configuration_option do |option|
+ match do |configuration|
+ configuration.should respond_to(option)
+ configuration.__send__(option).should == @default if defined?(@default)
+ configuration.__send__(:"#{option}=", "value")
+ configuration.__send__(option).should == "value"
+ end
+
+ chain :with_default do |default|
+ @default = default
+ end
+
+ failure_message do
+ description = "expected #{subject} to have"
+ description << " configuration option #{option.inspect}"
+ description << " with a default of #{@default.inspect}" if defined?(@default)
+ description
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.