edmond-danthes 2.1.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.
@@ -0,0 +1,11 @@
1
+ ---
2
+ NestedIterators:
3
+ max_allowed_nesting: 2
4
+ UtilityFunction:
5
+ enabled: false
6
+ IrresponsibleModule:
7
+ enabled: false
8
+ DuplicateMethodCall:
9
+ max_calls: 5
10
+ FeatureEnvy:
11
+ enabled: false
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
3
+ require 'danthes/version'
4
+ Gem::Specification.new do |s|
5
+ s.name = 'edmond-danthes'
6
+ s.version = Danthes::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.author = ['Alexander Simonov', 'Dmitry Zuev']
9
+ s.email = ['alex@simonov.me', 'mail@dmitryzuev.com']
10
+ s.homepage = 'https://github.com/dmitryzuev/edmond-danthes'
11
+ s.summary = 'Private pub/sub messaging through Faye.'
12
+ s.description = 'Private pub/sub messaging in Rails through Faye. More Faye features supported. Based on PrivatePub.'
13
+ s.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
14
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
15
+ s.require_paths = ['lib']
16
+ s.add_dependency 'faye', '>= 1.0.1'
17
+ s.add_dependency 'faye-redis'
18
+ s.add_dependency 'yajl-ruby', '>= 1.2.0'
19
+
20
+ s.add_development_dependency 'rake'
21
+ s.add_development_dependency 'guard'
22
+ s.add_development_dependency 'guard-coffeescript'
23
+ s.add_development_dependency 'jasmine', '>= 2.0.0'
24
+ s.add_development_dependency 'rspec', '>= 3.0.0'
25
+ s.add_development_dependency 'rspec-mocks', '>= 3.0.0'
26
+ s.add_development_dependency 'webmock'
27
+ s.add_development_dependency 'coveralls'
28
+ s.add_development_dependency 'therubyracer'
29
+ s.add_development_dependency 'rails'
30
+ end
@@ -0,0 +1,123 @@
1
+ require 'digest/sha1'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'yajl/json_gem'
5
+ require 'erb'
6
+
7
+ require 'danthes/faye_extension'
8
+
9
+ module Danthes
10
+ class Error < StandardError; end
11
+
12
+ class << self
13
+ attr_reader :config
14
+ attr_accessor :env
15
+
16
+ # List of accepted options in config file
17
+ ACCEPTED_KEYS = %w(adapter server secret_token mount signature_expiration timeout)
18
+
19
+ # List of accepted options in redis config file
20
+ REDIS_ACCEPTED_KEYS = %w(host port password database namespace socket)
21
+
22
+ # Default options
23
+ DEFAULT_OPTIONS = { mount: '/faye', timeout: 60, extensions: [FayeExtension.new] }
24
+
25
+ # Resets the configuration to the default
26
+ # Set environment
27
+ def startup
28
+ @config = DEFAULT_OPTIONS.dup
29
+ @env = if defined? ::Rails
30
+ ::Rails.env
31
+ else
32
+ ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
33
+ end
34
+ end
35
+
36
+ # Loads the configuration from a given YAML file
37
+ def load_config(filename)
38
+ yaml = ::YAML.load(::ERB.new(::File.read(filename)).result)[env]
39
+ fail ArgumentError, "The #{env} environment does not exist in #{filename}" if yaml.nil?
40
+ yaml.each do |key, val|
41
+ config[key.to_sym] = val if ACCEPTED_KEYS.include?(key)
42
+ end
43
+ end
44
+
45
+ # Loads the options from a given YAML file
46
+ def load_redis_config(filename)
47
+ require 'faye/redis'
48
+ yaml = ::YAML.load(::ERB.new(::File.read(filename)).result)[env]
49
+ # default redis options
50
+ options = { type: Faye::Redis, host: 'localhost', port: 6379 }
51
+ yaml.each do |key, val|
52
+ options[key.to_sym] = val if REDIS_ACCEPTED_KEYS.include?(key)
53
+ end
54
+ config[:engine] = options
55
+ end
56
+
57
+ # Publish the given data to a specific channel. This ends up sending
58
+ # a Net::HTTP POST request to the Faye server.
59
+ def publish_to(channel, data)
60
+ publish_message(message(channel, data))
61
+ end
62
+
63
+ # Sends the given message hash to the Faye server using Net::HTTP.
64
+ def publish_message(message)
65
+ fail Error, 'No server specified, ensure danthes.yml was loaded properly.' unless config[:server]
66
+ url = URI.parse(server_url)
67
+
68
+ form = ::Net::HTTP::Post.new(url.path.empty? ? '/' : url.path)
69
+ form.set_form_data(message: message.to_json)
70
+
71
+ http = ::Net::HTTP.new(url.host, url.port)
72
+ http.use_ssl = url.scheme == 'https'
73
+ http.start { |h| h.request(form) }
74
+ end
75
+
76
+ # Returns a message hash for sending to Faye
77
+ def message(channel, data)
78
+ message = { channel: channel,
79
+ data: { channel: channel },
80
+ ext: { danthes_token: config[:secret_token] }
81
+ }
82
+ if data.is_a? String
83
+ message[:data][:eval] = data
84
+ else
85
+ message[:data][:data] = data
86
+ end
87
+ message
88
+ end
89
+
90
+ def server_url
91
+ [config[:server], config[:mount].gsub(/^\//, '')].join('/')
92
+ end
93
+
94
+ # Returns a subscription hash to pass to the PrivatePub.sign call in JavaScript.
95
+ # Any options passed are merged to the hash.
96
+ def subscription(options = {})
97
+ sub = { server: server_url, timestamp: (Time.now.to_f * 1000).round }.merge(options)
98
+ sub[:signature] = ::Digest::SHA1.hexdigest([config[:secret_token],
99
+ sub[:channel],
100
+ sub[:timestamp]].join)
101
+ sub
102
+ end
103
+
104
+ # Determine if the signature has expired given a timestamp.
105
+ def signature_expired?(timestamp)
106
+ return false unless config[:signature_expiration]
107
+ timestamp < ((Time.now.to_f - config[:signature_expiration]) * 1000).round
108
+ end
109
+
110
+ # Returns the Faye Rack application.
111
+ def faye_app
112
+ rack_config = {}
113
+ [:engine, :mount, :ping, :timeout, :extensions, :websocket_extensions ].each do |k|
114
+ rack_config[k] = config[k] if config[k]
115
+ end
116
+ ::Faye::RackAdapter.new(rack_config)
117
+ end
118
+ end
119
+
120
+ startup
121
+ end
122
+
123
+ require 'danthes/engine' if defined? ::Rails
@@ -0,0 +1,16 @@
1
+ require 'danthes/view_helpers'
2
+
3
+ module Danthes
4
+ class Engine < Rails::Engine
5
+ # Loads the danthes.yml file if it exists.
6
+ initializer 'danthes.config' do |app|
7
+ path = app.root.join('config/danthes.yml')
8
+ ::Danthes.load_config(path) if path.exist?
9
+ end
10
+
11
+ # Adds the ViewHelpers into ActionView::Base
12
+ initializer 'danthes.view_helpers' do
13
+ ActionView::Base.send :include, ViewHelpers
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,40 @@
1
+ module Danthes
2
+ # This class is an extension for the Faye::RackAdapter.
3
+ # It is used inside of Danthes.faye_app.
4
+ class FayeExtension
5
+ # Callback to handle incoming Faye messages. This authenticates both
6
+ # subscribe and publish calls.
7
+ def incoming(message, callback)
8
+ if message['channel'] == '/meta/subscribe'
9
+ authenticate_subscribe(message)
10
+ elsif message['channel'] !~ %r{^/meta/}
11
+ authenticate_publish(message)
12
+ end
13
+ callback.call(message)
14
+ end
15
+
16
+ private
17
+
18
+ # Ensure the subscription signature is correct and that it has not expired.
19
+ def authenticate_subscribe(message)
20
+ subscription = Danthes.subscription(channel: message['subscription'],
21
+ timestamp: message['ext']['danthes_timestamp'])
22
+ if message['ext']['danthes_signature'] != subscription[:signature]
23
+ message['error'] = 'Incorrect signature.'
24
+ elsif Danthes.signature_expired? message['ext']['danthes_timestamp'].to_i
25
+ message['error'] = 'Signature has expired.'
26
+ end
27
+ end
28
+
29
+ # Ensures the secret token is correct before publishing.
30
+ def authenticate_publish(message)
31
+ if Danthes.config[:secret_token].nil?
32
+ fail Error, 'No secret_token config set, ensure danthes.yml is loaded properly.'
33
+ elsif message['ext']['danthes_token'] != Danthes.config[:secret_token]
34
+ message['error'] = 'Incorrect token.'
35
+ else
36
+ message['ext']['danthes_token'] = nil
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module Danthes
2
+ VERSION = '2.1.0'
3
+ end
@@ -0,0 +1,22 @@
1
+ module Danthes
2
+ module ViewHelpers
3
+ # Publish the given data or block to the client by sending
4
+ # a Net::HTTP POST request to the Faye server. If a block
5
+ # or string is passed in, it is evaluated as JavaScript
6
+ # on the client. Otherwise it will be converted to JSON
7
+ # for use in a JavaScript callback.
8
+ def publish_to(channel, data = nil, &block)
9
+ Danthes.publish_to(channel, data || capture(&block))
10
+ end
11
+
12
+ # Subscribe the client to the given channel. This generates
13
+ # some JavaScript calling Danthes.sign with the subscription
14
+ # options.
15
+ def subscribe_to(channel, opts = {})
16
+ js_tag = opts.delete(:include_js_tag){ true }
17
+ subscription = Danthes.subscription(channel: channel)
18
+ content = raw("if (typeof Danthes != 'undefined') { Danthes.sign(#{subscription.to_json}) }")
19
+ js_tag ? content_tag('script', content, type: 'text/javascript') : content
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ require 'generators/danthes_generator'
2
+
3
+ module Danthes
4
+ module Generators
5
+ class InstallGenerator < Base
6
+ desc 'Create sample config file and add rackup file'
7
+ def copy_files
8
+ template 'danthes.yml', 'config/danthes.yml'
9
+ copy_file 'danthes.ru', 'danthes.ru'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ require 'generators/danthes_generator'
2
+
3
+ module Danthes
4
+ module Generators
5
+ class RedisInstallGenerator < Base
6
+ desc 'Create sample redis config file and add faye-redis gem to Gemfile'
7
+
8
+ def copy_files
9
+ template 'danthes_redis.yml', 'config/danthes_redis.yml'
10
+ end
11
+
12
+ def add_redis_gem
13
+ add_gem 'faye-redis'
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ require 'rails/generators/base'
2
+
3
+ module Danthes
4
+ module Generators
5
+ class Base < Rails::Generators::Base
6
+ def self.source_root
7
+ File.dirname(__FILE__) + '/templates'
8
+ end
9
+
10
+ def self.banner
11
+ "rails generate danthes:#{generator_name}"
12
+ end
13
+
14
+ private
15
+
16
+ def add_gem(name, options = {})
17
+ gemfile_path = File.join(destination_root, 'Gemfile')
18
+ gemfile_content = File.read(gemfile_path)
19
+ File.open(gemfile_path, 'a') { |f| f.write("\n") } unless gemfile_content =~ /\n\Z/
20
+ gem name, options unless gemfile_content.include? name
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ # Run with: rackup danthes.ru -s thin -E production
2
+ require "bundler/setup"
3
+ require "yaml"
4
+ require "faye"
5
+ require "danthes"
6
+
7
+ ::Danthes.load_config(File.expand_path("../config/danthes.yml", __FILE__))
8
+ Faye::WebSocket.load_adapter(Danthes.config[:adapter])
9
+
10
+ path = File.expand_path("../config/danthes_redis.yml", __FILE__)
11
+ if File.exist?(path)
12
+ ::Danthes.load_redis_config(path)
13
+ end
14
+
15
+ run ::Danthes.faye_app
@@ -0,0 +1,16 @@
1
+ development:
2
+ adapter: thin
3
+ server: "http://localhost:9292"
4
+ secret_token: "secret"
5
+ mount: '/faye'
6
+ test:
7
+ adapter: thin
8
+ server: "http://localhost:9292"
9
+ secret_token: "secret"
10
+ mount: '/faye'
11
+ production:
12
+ adapter: thin
13
+ server: "http://faye.example.com"
14
+ mount: '/faye'
15
+ secret_token: "<%= defined?(SecureRandom) ? SecureRandom.hex(32) : ActiveSupport::SecureRandom.hex(32) %>"
16
+ signature_expiration: 3600 # one hour
@@ -0,0 +1,14 @@
1
+ development:
2
+ host: localhost
3
+ port: 6379
4
+ namespace: '/danthes'
5
+ test:
6
+ host: localhost
7
+ port: 6379
8
+ namespace: '/danthes'
9
+ production:
10
+ host: redis_host
11
+ port: redis_port
12
+ password: redis_password
13
+ database: redis_database
14
+ namespace: '/namespace'
@@ -0,0 +1,211 @@
1
+ describe "Danthes", ->
2
+ window.Faye = undefined
3
+ pub = undefined
4
+
5
+ signToChannel = (channel, addOptions = {}) ->
6
+ sub = {callback: jasmine.createSpy(), errback: jasmine.createSpy()}
7
+ faye = {subscribe: jasmine.createSpy().and.returnValue(sub)}
8
+ spyOn(pub, 'faye').and.callFake (callback) ->
9
+ callback(faye)
10
+ options = {server: "server", channel: "#{channel}", timestamp: 1234567890, signature: '1234567890'}
11
+ options['connect'] = addOptions['connect'] if addOptions['connect']?
12
+ options['error'] = addOptions['error'] if addOptions['error']?
13
+ pub.sign(options)
14
+ return [faye, options]
15
+
16
+ beforeEach ->
17
+ pub = window.Danthes
18
+ pub.reset()
19
+ script = document.getElementById('faye-connection-script')
20
+ if script?
21
+ script.parentNode.removeChild(script)
22
+
23
+ it "not adds a subscription callback without signing", ->
24
+ expect(pub.subscribe("hello", "callback")).toBe(false)
25
+ expect(pub.subscriptions).toEqual({})
26
+
27
+ it "adds a subscription callback", ->
28
+ signToChannel('hello')
29
+ pub.subscribe("hello", "callback")
30
+ expect(pub.subscriptions["hello"]['callback']).toEqual("callback")
31
+
32
+ it "has a fayeExtension which adds matching subscription signature and timestamp to outgoing message", ->
33
+ called = false
34
+ message = {channel: "/meta/subscribe", subscription: "hello"}
35
+ pub.subscriptions['hello'] = {}
36
+ pub.subscriptions['hello']['opts'] = {signature: "abcd", timestamp: "1234"}
37
+ pub.fayeExtension.outgoing message, (message) ->
38
+ expect(message.ext.danthes_signature).toEqual("abcd")
39
+ expect(message.ext.danthes_timestamp).toEqual("1234")
40
+ called = true
41
+ expect(called).toBeTruthy()
42
+
43
+ it "evaluates javascript in message response", ->
44
+ pub.handleResponse(eval: 'this.subscriptions.foo = "bar"')
45
+ expect(pub.subscriptions.foo).toEqual("bar")
46
+
47
+ it "triggers callback matching message channel in response", ->
48
+ called = false
49
+ signToChannel('test')
50
+ pub.subscribe "test", (data, channel) ->
51
+ expect(data).toEqual("abcd")
52
+ expect(channel).toEqual("test")
53
+ called = true
54
+ pub.handleResponse(channel: "test", data: "abcd")
55
+ expect(called).toBeTruthy()
56
+
57
+ it "triggers faye callback function immediately when fayeClient is available", ->
58
+ called = false
59
+ pub.fayeClient = "faye"
60
+ pub.faye (faye) ->
61
+ expect(faye).toEqual("faye")
62
+ called = true
63
+ expect(called).toBeTruthy()
64
+
65
+ it "adds fayeCallback when client and server aren't available", ->
66
+ pub.faye("callback")
67
+ expect(pub.fayeCallbacks[0]).toEqual("callback")
68
+
69
+ it "adds a script tag loading faye js when the server is present", ->
70
+ client = {addExtension: jasmine.createSpy()}
71
+ callback = jasmine.createSpy()
72
+ pub.server = "path/to/faye"
73
+ pub.faye(callback)
74
+ expect(pub.fayeCallbacks[0]).toEqual(callback)
75
+ script = document.getElementById('faye-connection-script')
76
+ expect(script).toBeDefined()
77
+ expect(script.type).toEqual("text/javascript")
78
+ expect(script.src).toMatch("path/to/faye/client.js")
79
+
80
+ it "adds a signed channel to subscribe later", ->
81
+ pub.fayeClient = 'string'
82
+ [faye, options] = signToChannel('somechannel')
83
+ expect(faye.subscribe).not.toHaveBeenCalled()
84
+ expect(pub.subscriptions.somechannel.activated).toBeUndefined()
85
+ expect(pub.subscriptions.somechannel).toBeDefined()
86
+ expect(pub.subscriptions.somechannel.opts.signature).toEqual('1234567890')
87
+ expect(pub.subscriptions.somechannel.opts.timestamp).toEqual(1234567890)
88
+ expect(pub.subscriptions.somechannel.activated).toBeUndefined()
89
+
90
+ it "adds a faye subscription with response handler when sign with connect option", ->
91
+ pub.fayeClient = 'string'
92
+ [faye, options] = signToChannel('somechannel', {'connect': jasmine.createSpy()})
93
+ expect(faye.subscribe).toHaveBeenCalled()
94
+ expect(pub.subscriptions.somechannel.activated).toBeDefined()
95
+
96
+ it "adds a faye subscription with response handler when sign with error option", ->
97
+ pub.fayeClient = 'string'
98
+ [faye, options] = signToChannel('somechannel', {'error': jasmine.createSpy()})
99
+ expect(faye.subscribe).toHaveBeenCalled()
100
+ expect(pub.subscriptions.somechannel.activated).toBeDefined()
101
+
102
+ it "adds a faye subscription with response handler when first time subscribing", ->
103
+ pub.fayeClient = 'string'
104
+ [faye, options] = signToChannel('somechannel')
105
+ pub.subscribe('somechannel', ->)
106
+ expect(faye.subscribe).toHaveBeenCalled()
107
+ expect(pub.subscriptions.somechannel.activated).toBeDefined()
108
+
109
+ it "connects to faye server, adds extension, and executes callbacks", ->
110
+ callback = jasmine.createSpy()
111
+ client = {addExtension: jasmine.createSpy()}
112
+ window.Faye = {}
113
+ window.Faye.Client = (server) ->
114
+ expect(server).toEqual("server")
115
+ return client
116
+ pub.server = "server"
117
+ pub.fayeCallbacks.push(callback)
118
+ spyOn(pub, 'fayeExtension')
119
+ pub.connectToFaye()
120
+ expect(pub.fayeClient).toEqual(client)
121
+ expect(client.addExtension).toHaveBeenCalledWith(pub.fayeExtension)
122
+ expect(callback).toHaveBeenCalledWith(client)
123
+
124
+ it "adds transport to disables", ->
125
+ expect(pub.disableTransport('websocket')).toBeTruthy()
126
+ expect(pub.disables).toEqual(['websocket'])
127
+
128
+ it "adds transport to disables only one time", ->
129
+ pub.disableTransport('websocket')
130
+ pub.disableTransport('websocket')
131
+ expect(pub.disables).toEqual(['websocket'])
132
+
133
+ it "returns false if not accepted transport wants to be disabled", ->
134
+ expect(pub.disableTransport('websocket123')).toBeUndefined()
135
+ expect(pub.disables).toEqual([])
136
+
137
+ it "connects to faye server, and executes disable once", ->
138
+ callback = jasmine.createSpy()
139
+ client = {addExtension: jasmine.createSpy(), disable: jasmine.createSpy()}
140
+ window.Faye = {}
141
+ window.Faye.Client = (server) -> client
142
+ pub.server = "server"
143
+ pub.disableTransport('websocket')
144
+ pub.connectToFaye()
145
+ expect(client.disable).toHaveBeenCalledWith('websocket')
146
+
147
+ it "connects to faye server, and executes disable once", ->
148
+ callback = jasmine.createSpy()
149
+ client = {addExtension: jasmine.createSpy(), disable: jasmine.createSpy()}
150
+ window.Faye = {}
151
+ window.Faye.Client = (server) -> client
152
+ pub.server = "server"
153
+ pub.disableTransport('websocket')
154
+ pub.disableTransport('long-polling')
155
+ pub.connectToFaye()
156
+ expect(client.disable).toHaveBeenCalledWith('websocket')
157
+ expect(client.disable).toHaveBeenCalledWith('long-polling')
158
+
159
+ it "adds subscription faye object into channel object", ->
160
+ sub = {callback: jasmine.createSpy(), errback: jasmine.createSpy()}
161
+ pub.fayeClient = {subscribe: jasmine.createSpy().and.returnValue(sub)}
162
+ options = {server: "server", channel: 'somechannel'}
163
+ pub.sign(options)
164
+ pub.subscribe("somechannel", jasmine.createSpy())
165
+ expect(sub.callback).toHaveBeenCalled()
166
+ expect(sub.errback).toHaveBeenCalled()
167
+ expect(pub.subscriptions.somechannel.sub).toEqual(sub)
168
+
169
+ it "adds subscription faye object into channel object and call connect callback after connection", ->
170
+ sub =
171
+ callback: (f) ->
172
+ f()
173
+ errback: jasmine.createSpy()
174
+ pub.fayeClient = {subscribe: jasmine.createSpy().and.returnValue(sub)}
175
+ options = {server: "server", channel: 'somechannel'}
176
+ pub.sign(options)
177
+ connectSpy = jasmine.createSpy()
178
+ pub.subscribe('somechannel', jasmine.createSpy(), connect: connectSpy)
179
+ expect(connectSpy).toHaveBeenCalledWith(sub)
180
+
181
+ it "adds subscription faye object into channel object and call error callback after connection", ->
182
+ sub =
183
+ callback: jasmine.createSpy()
184
+ errback: (f) ->
185
+ f('error')
186
+ pub.fayeClient = {subscribe: jasmine.createSpy().and.returnValue(sub)}
187
+ erroSpy = jasmine.createSpy()
188
+ options = {server: "server", channel: 'somechannel'}
189
+ pub.sign(options)
190
+ pub.subscribe("somechannel", jasmine.createSpy(), error: erroSpy)
191
+ expect(erroSpy).toHaveBeenCalledWith(sub, 'error')
192
+
193
+ it "removes subscription to the channel", ->
194
+ sub = {callback: jasmine.createSpy(), errback: jasmine.createSpy(), cancel: jasmine.createSpy()}
195
+ pub.fayeClient = {subscribe: jasmine.createSpy().and.returnValue(sub)}
196
+ options = {server: "server", channel: 'somechannel'}
197
+ pub.sign(options)
198
+ pub.subscribe('somechannel', jasmine.createSpy())
199
+ pub.unsubscribe('somechannel')
200
+ expect(sub.cancel).toHaveBeenCalled()
201
+ expect(pub.subscriptions.somechannel.sub).toBeUndefined()
202
+
203
+ it "removes all subscription to the channels", ->
204
+ sub = {callback: jasmine.createSpy(), errback: jasmine.createSpy(), cancel: jasmine.createSpy()}
205
+ pub.fayeClient = {subscribe: jasmine.createSpy().and.returnValue(sub)}
206
+ options = {server: "server", channel: 'somechannel'}
207
+ pub.sign(options)
208
+ pub.subscribe "somechannel", jasmine.createSpy()
209
+ pub.unsubscribeAll()
210
+ expect(sub.cancel).toHaveBeenCalled()
211
+ expect(pub.subscriptions.somechannel.sub).toBeUndefined()