pusher 1.3.1 → 2.0.3

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.
data/lib/pusher/client.rb CHANGED
@@ -1,57 +1,63 @@
1
+ require 'base64'
1
2
  require 'pusher-signature'
2
3
 
3
4
  module Pusher
4
5
  class Client
5
- attr_accessor :scheme, :host, :port, :app_id, :key, :secret, :notification_host, :notification_scheme
6
+ attr_accessor :scheme, :host, :port, :app_id, :key, :secret, :encryption_master_key
6
7
  attr_reader :http_proxy, :proxy
7
8
  attr_writer :connect_timeout, :send_timeout, :receive_timeout,
8
9
  :keep_alive_timeout
9
10
 
10
11
  ## CONFIGURATION ##
12
+ DEFAULT_CONNECT_TIMEOUT = 5
13
+ DEFAULT_SEND_TIMEOUT = 5
14
+ DEFAULT_RECEIVE_TIMEOUT = 5
15
+ DEFAULT_KEEP_ALIVE_TIMEOUT = 30
16
+ DEFAULT_CLUSTER = "mt1"
11
17
 
12
18
  # Loads the configuration from an url in the environment
13
19
  def self.from_env(key = 'PUSHER_URL')
14
20
  url = ENV[key] || raise(ConfigurationError, key)
21
+ from_url(url)
22
+ end
23
+
24
+ # Loads the configuration from a url
25
+ def self.from_url(url)
15
26
  client = new
16
27
  client.url = url
17
28
  client
18
29
  end
19
30
 
20
31
  def initialize(options = {})
21
- default_options = {
22
- :scheme => 'http',
23
- :port => 80,
24
- }
25
- merged_options = default_options.merge(options)
32
+ @scheme = "https"
33
+ @port = options[:port] || 443
26
34
 
27
- if options.has_key?(:host)
28
- merged_options[:host] = options[:host]
29
- elsif options.has_key?(:cluster)
30
- merged_options[:host] = "api-#{options[:cluster]}.pusher.com"
31
- else
32
- merged_options[:host] = "api.pusherapp.com"
35
+ if options.key?(:encrypted)
36
+ warn "[DEPRECATION] `encrypted` is deprecated and will be removed in the next major version. Use `use_tls` instead."
33
37
  end
34
38
 
35
- # TODO: Change host name when finalized
36
- merged_options[:notification_host] =
37
- options.fetch(:notification_host, "nativepush-cluster1.pusher.com")
39
+ if options[:use_tls] == false || options[:encrypted] == false
40
+ @scheme = "http"
41
+ @port = options[:port] || 80
42
+ end
38
43
 
39
- merged_options[:notification_scheme] =
40
- options.fetch(:notification_scheme, "https")
44
+ @app_id = options[:app_id]
45
+ @key = options[:key]
46
+ @secret = options[:secret]
41
47
 
42
- @scheme, @host, @port, @app_id, @key, @secret, @notification_host, @notification_scheme =
43
- merged_options.values_at(
44
- :scheme, :host, :port, :app_id, :key, :secret, :notification_host, :notification_scheme
45
- )
48
+ @host = options[:host]
49
+ @host ||= "api-#{options[:cluster]}.pusher.com" unless options[:cluster].nil? || options[:cluster].empty?
50
+ @host ||= "api-#{DEFAULT_CLUSTER}.pusher.com"
51
+
52
+ @encryption_master_key = Base64.strict_decode64(options[:encryption_master_key_base64]) if options[:encryption_master_key_base64]
46
53
 
47
- @http_proxy = nil
48
- self.http_proxy = options[:http_proxy] if options[:http_proxy]
54
+ @http_proxy = options[:http_proxy]
49
55
 
50
56
  # Default timeouts
51
- @connect_timeout = 5
52
- @send_timeout = 5
53
- @receive_timeout = 5
54
- @keep_alive_timeout = 30
57
+ @connect_timeout = DEFAULT_CONNECT_TIMEOUT
58
+ @send_timeout = DEFAULT_SEND_TIMEOUT
59
+ @receive_timeout = DEFAULT_RECEIVE_TIMEOUT
60
+ @keep_alive_timeout = DEFAULT_KEEP_ALIVE_TIMEOUT
55
61
  end
56
62
 
57
63
  # @private Returns the authentication token for the client
@@ -65,10 +71,10 @@ module Pusher
65
71
  def url(path = nil)
66
72
  raise ConfigurationError, :app_id unless @app_id
67
73
  URI::Generic.build({
68
- :scheme => @scheme,
69
- :host => @host,
70
- :port => @port,
71
- :path => "/apps/#{@app_id}#{path}"
74
+ scheme: @scheme,
75
+ host: @host,
76
+ port: @port,
77
+ path: "/apps/#{@app_id}#{path}"
72
78
  })
73
79
  end
74
80
 
@@ -92,13 +98,12 @@ module Pusher
92
98
  @http_proxy = http_proxy
93
99
  uri = URI.parse(http_proxy)
94
100
  @proxy = {
95
- :scheme => uri.scheme,
96
- :host => uri.host,
97
- :port => uri.port,
98
- :user => uri.user,
99
- :password => uri.password
101
+ scheme: uri.scheme,
102
+ host: uri.host,
103
+ port: uri.port,
104
+ user: uri.user,
105
+ password: uri.password
100
106
  }
101
- @http_proxy
102
107
  end
103
108
 
104
109
  # Configure whether Pusher API calls should be made over SSL
@@ -118,6 +123,8 @@ module Pusher
118
123
  end
119
124
 
120
125
  def cluster=(cluster)
126
+ cluster = DEFAULT_CLUSTER if cluster.nil? || cluster.empty?
127
+
121
128
  @host = "api-#{cluster}.pusher.com"
122
129
  end
123
130
 
@@ -127,7 +134,13 @@ module Pusher
127
134
  @connect_timeout, @send_timeout, @receive_timeout = value, value, value
128
135
  end
129
136
 
130
- ## INTERACE WITH THE API ##
137
+ # Set an encryption_master_key to use with private-encrypted channels from
138
+ # a base64 encoded string.
139
+ def encryption_master_key_base64=(s)
140
+ @encryption_master_key = s ? Base64.strict_decode64(s) : nil
141
+ end
142
+
143
+ ## INTERACT WITH THE API ##
131
144
 
132
145
  def resource(path)
133
146
  Resource.new(self, path)
@@ -306,24 +319,6 @@ module Pusher
306
319
  post_async('/batch_events', trigger_batch_params(events.flatten))
307
320
  end
308
321
 
309
- def notification_client
310
- @notification_client ||=
311
- NativeNotification::Client.new(@app_id, @notification_host, @notification_scheme, self)
312
- end
313
-
314
-
315
- # Send a push notification
316
- #
317
- # POST /apps/[app_id]/notifications
318
- #
319
- # @param interests [Array] An array of interests
320
- # @param message [String] Message to send
321
- # @param options [Hash] Additional platform specific options
322
- #
323
- # @return [Hash]
324
- def notify(interests, data = {})
325
- notification_client.notify(interests, data)
326
- end
327
322
 
328
323
  # Generate the expected response for an authentication endpoint.
329
324
  # See http://pusher.com/docs/authenticating_users for details.
@@ -351,14 +346,20 @@ module Pusher
351
346
  #
352
347
  def authenticate(channel_name, socket_id, custom_data = nil)
353
348
  channel_instance = channel(channel_name)
354
- channel_instance.authenticate(socket_id, custom_data)
349
+ r = channel_instance.authenticate(socket_id, custom_data)
350
+ if channel_name.match(/^private-encrypted-/)
351
+ r[:shared_secret] = Base64.strict_encode64(
352
+ channel_instance.shared_secret(encryption_master_key)
353
+ )
354
+ end
355
+ r
355
356
  end
356
357
 
357
358
  # @private Construct a net/http http client
358
359
  def sync_http_client
359
- @client ||= begin
360
- require 'httpclient'
360
+ require 'httpclient'
361
361
 
362
+ @client ||= begin
362
363
  HTTPClient.new(@http_proxy).tap do |c|
363
364
  c.connect_timeout = @connect_timeout
364
365
  c.send_timeout = @send_timeout
@@ -377,14 +378,14 @@ module Pusher
377
378
  require 'em-http' unless defined?(EventMachine::HttpRequest)
378
379
 
379
380
  connection_opts = {
380
- :connect_timeout => @connect_timeout,
381
- :inactivity_timeout => @receive_timeout,
381
+ connect_timeout: @connect_timeout,
382
+ inactivity_timeout: @receive_timeout,
382
383
  }
383
384
 
384
385
  if defined?(@proxy)
385
386
  proxy_opts = {
386
- :host => @proxy[:host],
387
- :port => @proxy[:port]
387
+ host: @proxy[:host],
388
+ port: @proxy[:port]
388
389
  }
389
390
  if @proxy[:user]
390
391
  proxy_opts[:authorization] = [@proxy[:user], @proxy[:password]]
@@ -400,12 +401,19 @@ module Pusher
400
401
 
401
402
  def trigger_params(channels, event_name, data, params)
402
403
  channels = Array(channels).map(&:to_s)
403
- raise Pusher::Error, "Too many channels (#{channels.length}), max 10" if channels.length > 10
404
+ raise Pusher::Error, "Too many channels (#{channels.length}), max 100" if channels.length > 100
405
+
406
+ encoded_data = if channels.any?{ |c| c.match(/^private-encrypted-/) } then
407
+ raise Pusher::Error, "Cannot trigger to multiple channels if any are encrypted" if channels.length > 1
408
+ encrypt(channels[0], encode_data(data))
409
+ else
410
+ encode_data(data)
411
+ end
404
412
 
405
413
  params.merge({
406
414
  name: event_name,
407
415
  channels: channels,
408
- data: encode_data(data),
416
+ data: encoded_data,
409
417
  })
410
418
  end
411
419
 
@@ -413,7 +421,11 @@ module Pusher
413
421
  {
414
422
  batch: events.map do |event|
415
423
  event.dup.tap do |e|
416
- e[:data] = encode_data(e[:data])
424
+ e[:data] = if e[:channel].match(/^private-encrypted-/) then
425
+ encrypt(e[:channel], encode_data(e[:data]))
426
+ else
427
+ encode_data(e[:data])
428
+ end
417
429
  end
418
430
  end
419
431
  }
@@ -425,8 +437,37 @@ module Pusher
425
437
  MultiJson.encode(data)
426
438
  end
427
439
 
440
+ # Encrypts a message with a key derived from the master key and channel
441
+ # name
442
+ def encrypt(channel_name, encoded_data)
443
+ raise ConfigurationError, :encryption_master_key unless @encryption_master_key
444
+
445
+ # Only now load rbnacl, so that people that aren't using it don't need to
446
+ # install libsodium
447
+ require_rbnacl
448
+
449
+ secret_box = RbNaCl::SecretBox.new(
450
+ channel(channel_name).shared_secret(@encryption_master_key)
451
+ )
452
+
453
+ nonce = RbNaCl::Random.random_bytes(secret_box.nonce_bytes)
454
+ ciphertext = secret_box.encrypt(nonce, encoded_data)
455
+
456
+ MultiJson.encode({
457
+ "nonce" => Base64::strict_encode64(nonce),
458
+ "ciphertext" => Base64::strict_encode64(ciphertext),
459
+ })
460
+ end
461
+
428
462
  def configured?
429
463
  host && scheme && key && secret && app_id
430
464
  end
465
+
466
+ def require_rbnacl
467
+ require 'rbnacl'
468
+ rescue LoadError => e
469
+ $stderr.puts "You don't have rbnacl installed in your application. Please add it to your Gemfile and run bundle install"
470
+ raise e
471
+ end
431
472
  end
432
473
  end
@@ -94,6 +94,8 @@ module Pusher
94
94
  raise Error, "404 Not found (#{@uri.path})"
95
95
  when 407
96
96
  raise Error, "Proxy Authentication Required"
97
+ when 413
98
+ raise Error, "Payload Too Large > 10KB"
97
99
  else
98
100
  raise Error, "Unknown error (status code #{status_code}): #{body}"
99
101
  end
@@ -1,3 +1,3 @@
1
1
  module Pusher
2
- VERSION = '1.3.1'
2
+ VERSION = '2.0.3'
3
3
  end
data/lib/pusher.rb CHANGED
@@ -27,12 +27,12 @@ module Pusher
27
27
  class << self
28
28
  extend Forwardable
29
29
 
30
- def_delegators :default_client, :scheme, :host, :port, :app_id, :key, :secret, :http_proxy
31
- def_delegators :default_client, :notification_host, :notification_scheme
32
- def_delegators :default_client, :scheme=, :host=, :port=, :app_id=, :key=, :secret=, :http_proxy=
33
- def_delegators :default_client, :notification_host=, :notification_scheme=
30
+ def_delegators :default_client, :scheme, :host, :port, :app_id, :key,
31
+ :secret, :http_proxy, :encryption_master_key_base64
32
+ def_delegators :default_client, :scheme=, :host=, :port=, :app_id=, :key=,
33
+ :secret=, :http_proxy=, :encryption_master_key_base64=
34
34
 
35
- def_delegators :default_client, :authentication_token, :url
35
+ def_delegators :default_client, :authentication_token, :url, :cluster
36
36
  def_delegators :default_client, :encrypted=, :url=, :cluster=
37
37
  def_delegators :default_client, :timeout=, :connect_timeout=, :send_timeout=, :receive_timeout=, :keep_alive_timeout=
38
38
 
@@ -66,4 +66,3 @@ require 'pusher/channel'
66
66
  require 'pusher/request'
67
67
  require 'pusher/resource'
68
68
  require 'pusher/webhook'
69
- require 'pusher/native_notification/client'
metadata CHANGED
@@ -1,185 +1,186 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pusher
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 2.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pusher
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-15 00:00:00.000000000 Z
11
+ date: 2022-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: multi_json
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.0'
19
+ version: '1.15'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.0'
26
+ version: '1.15'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: pusher-signature
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: 0.1.8
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ~>
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.1.8
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: httpclient
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.7'
47
+ version: '2.8'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ~>
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '2.7'
54
+ version: '2.8'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ~>
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '3.0'
61
+ version: '3.9'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ~>
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '3.0'
68
+ version: '3.9'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: webmock
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ! '>='
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: '3.9'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ! '>='
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '3.9'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: em-http-request
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ~>
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 1.1.0
89
+ version: '1.1'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ~>
94
+ - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 1.1.0
96
+ version: '1.1'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: addressable
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - '='
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 2.4.0
103
+ version: '2.7'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - '='
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 2.4.0
110
+ version: '2.7'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rake
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ~>
115
+ - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 10.4.2
117
+ version: '13.0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ~>
122
+ - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: 10.4.2
124
+ version: '13.0'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: rack
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ~>
129
+ - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: 1.6.4
131
+ version: '2.2'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - ~>
136
+ - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: 1.6.4
138
+ version: '2.2'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: json
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - ~>
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '2.3'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '2.3'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rbnacl
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
144
158
  - !ruby/object:Gem::Version
145
- version: 1.8.3
159
+ version: '7.1'
146
160
  type: :development
147
161
  prerelease: false
148
162
  version_requirements: !ruby/object:Gem::Requirement
149
163
  requirements:
150
- - - ~>
164
+ - - "~>"
151
165
  - !ruby/object:Gem::Version
152
- version: 1.8.3
153
- description: Wrapper for pusher.com REST api
166
+ version: '7.1'
167
+ description: 'Wrapper for Pusher Channels REST api: : https://pusher.com/channels'
154
168
  email:
155
169
  - support@pusher.com
156
170
  executables: []
157
171
  extensions: []
158
172
  extra_rdoc_files: []
159
173
  files:
160
- - .document
161
- - .gemtest
162
- - .gitignore
163
- - .travis.yml
164
174
  - CHANGELOG.md
165
- - Gemfile
166
175
  - LICENSE
167
176
  - README.md
168
- - Rakefile
169
- - examples/async_message.rb
170
177
  - lib/pusher.rb
171
178
  - lib/pusher/channel.rb
172
179
  - lib/pusher/client.rb
173
- - lib/pusher/native_notification/client.rb
174
180
  - lib/pusher/request.rb
175
181
  - lib/pusher/resource.rb
176
182
  - lib/pusher/version.rb
177
183
  - lib/pusher/webhook.rb
178
- - pusher.gemspec
179
- - spec/channel_spec.rb
180
- - spec/client_spec.rb
181
- - spec/spec_helper.rb
182
- - spec/web_hook_spec.rb
183
184
  homepage: http://github.com/pusher/pusher-http-ruby
184
185
  licenses:
185
186
  - MIT
@@ -190,22 +191,17 @@ require_paths:
190
191
  - lib
191
192
  required_ruby_version: !ruby/object:Gem::Requirement
192
193
  requirements:
193
- - - ! '>='
194
+ - - ">="
194
195
  - !ruby/object:Gem::Version
195
- version: '0'
196
+ version: '2.6'
196
197
  required_rubygems_version: !ruby/object:Gem::Requirement
197
198
  requirements:
198
- - - ! '>='
199
+ - - ">="
199
200
  - !ruby/object:Gem::Version
200
201
  version: '0'
201
202
  requirements: []
202
- rubyforge_project:
203
- rubygems_version: 2.6.10
203
+ rubygems_version: 3.1.2
204
204
  signing_key:
205
205
  specification_version: 4
206
- summary: Pusher API client
207
- test_files:
208
- - spec/channel_spec.rb
209
- - spec/client_spec.rb
210
- - spec/spec_helper.rb
211
- - spec/web_hook_spec.rb
206
+ summary: Pusher Channels API client
207
+ test_files: []
data/.document DELETED
@@ -1,5 +0,0 @@
1
- README.rdoc
2
- lib/**/*.rb
3
- bin/*
4
- features/**/*.feature
5
- LICENSE
data/.gemtest DELETED
File without changes
data/.gitignore DELETED
@@ -1,24 +0,0 @@
1
- ## MAC OS
2
- .DS_Store
3
-
4
- ## TEXTMATE
5
- *.tmproj
6
- tmtags
7
-
8
- ## EMACS
9
- *~
10
- \#*
11
- .\#*
12
-
13
- ## VIM
14
- *.swp
15
-
16
- ## PROJECT::GENERAL
17
- coverage
18
- rdoc
19
- pkg
20
- .yardoc
21
- Gemfile.lock
22
-
23
- ## PROJECT::SPECIFIC
24
- .bundle
data/.travis.yml DELETED
@@ -1,15 +0,0 @@
1
- language: ruby
2
- sudo: false
3
- rvm:
4
- - 1.9.3
5
- - 2.0
6
- - 2.1
7
- - 2.2
8
- - 2.3.0
9
- - jruby
10
- - rbx-2
11
-
12
- matrix:
13
- allow_failures:
14
- - rvm: jruby
15
- - rvm: rbx-2
data/Gemfile DELETED
@@ -1,2 +0,0 @@
1
- source "https://rubygems.org"
2
- gemspec
data/Rakefile DELETED
@@ -1,11 +0,0 @@
1
- require 'bundler'
2
- Bundler::GemHelper.install_tasks
3
-
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec) do |s|
7
- s.pattern = 'spec/**/*.rb'
8
- end
9
-
10
- task :default => :spec
11
- task :test => :spec