restforce 4.0.0 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9e4311062c172c1fd0706872045d29fed90d5b36df5f6c2656d88c37ac680fb2
4
- data.tar.gz: f8b6e07bd7533342229f0bb02ef00a731dda64990927bc2c8d822fbfa46f5a29
3
+ metadata.gz: f24606173bf2365a1482e7ed764863aed6bf53ffad8f9e6c380144c5567a7541
4
+ data.tar.gz: fadc63d588d150937b9b4ff102adcdd2ab91691fc26f8bfaa9515b2eb79a4553
5
5
  SHA512:
6
- metadata.gz: 33d02a24a1fc9e7efd40a318f75a48992215b759bcb33c8860a0e4e49fbc5a2eac13cce908dac4605164af58fa6e43473b753668926e0875deeba63eac4cc51c
7
- data.tar.gz: 1a6155b0ec55633ebf061e7e709b7e391fe266563f5476b658040139e977ea00baf356602aacf8c5b2aef313c0b48296bfad984d444fe437249424810b567512
6
+ metadata.gz: 25baee4f91c29f7f3210b23c9a79f9ac1a9ee12a3a6169e20090e860ec62d51b671ef87b84f8ffc25633a2df933cbed0e5e980fb5ef41c7af08db7aa20de4eda
7
+ data.tar.gz: 6ab7f0096e6931fb2a59febb88d84a3c51a2000d3e690402b9f42ae9b85073f0b2623100a6a106035a420d448f64969c39415411def1a8c4f5bc690ef281c591
data/.rubocop_todo.yml CHANGED
@@ -12,7 +12,7 @@
12
12
  # SupportedHashRocketStyles: key, separator, table
13
13
  # SupportedColonStyles: key, separator, table
14
14
  # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
15
- Layout/AlignHash:
15
+ Layout/HashAlignment:
16
16
  Exclude:
17
17
  - 'lib/restforce/middleware/logger.rb'
18
18
  - 'restforce.gemspec'
@@ -23,7 +23,7 @@ Layout/AlignHash:
23
23
  # Cop supports --auto-correct.
24
24
  # Configuration parameters: EnforcedStyle, IndentationWidth.
25
25
  # SupportedStyles: special_inside_parentheses, consistent, align_braces
26
- Layout/IndentFirstHashElement:
26
+ Layout/FirstHashElementIndentation:
27
27
  Exclude:
28
28
  - 'lib/restforce/concerns/connection.rb'
29
29
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## 4.3.0 (Feb 10, 2021)
2
+
3
+ * Add compatability with Faraday versions up to but not including `v1.1.0` (@hongtron)
4
+
5
+ ## 4.2.2 (Jan 23, 2020)
6
+
7
+ * Fix `NoMethodError: undefined method '[]' for nil:NilClass` error when generating objects to return (@apurkiss)
8
+
9
+ ## 4.2.1 (Dec 4, 2019)
10
+
11
+ * Handle empty response bodies returned with authentication errors (@sylvandor)
12
+ * Fix Faraday deprecation warning in newer Faraday versions (`v0.17.1` onwards) caused by inheriting from a deprecated class
13
+
14
+ ## 4.2.0 (Oct 23, 2019)
15
+
16
+ * Add support for platform events, CDC, generic events, etc. in the Streaming API (@nathanKramer)
17
+
18
+ ## 4.1.0 (Oct 20, 2019)
19
+
20
+ * Add support for JWT authentication (@nathanKramer, @tagCincy)
21
+
1
22
  ## 4.0.0 (Oct 9, 2019)
2
23
 
3
24
  * __Deprecate support for Ruby 2.3__, since [Ruby 2.3 reached its end-of-life](https://www.ruby-lang.org/en/news/2019/03/31/support-of-ruby-2-3-has-ended/) in March 2019. (This is the only breaking change included in this version.)
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ gemspec
5
5
 
6
6
  gem 'faraday', '~> 0.17.0'
7
7
  gem 'jruby-openssl', platforms: :jruby
8
+ gem 'jwt'
8
9
  gem 'rake'
9
10
 
10
11
  group :development do
data/README.md CHANGED
@@ -25,7 +25,7 @@ Features include:
25
25
 
26
26
  Add this line to your application's Gemfile:
27
27
 
28
- gem 'restforce', '~> 4.0.0'
28
+ gem 'restforce', '~> 4.3.0'
29
29
 
30
30
  And then execute:
31
31
 
@@ -111,6 +111,21 @@ client = Restforce.new(username: 'foo',
111
111
  api_version: '41.0')
112
112
  ```
113
113
 
114
+ #### JWT Bearer Token
115
+
116
+ If you prefer to use a [JWT Bearer Token](https://developer.salesforce.com/page/Digging_Deeper_into_OAuth_2.0_on_Force.com#Obtaining_an_Access_Token_using_a_JWT_Bearer_Token) to authenticate:
117
+
118
+ ```ruby
119
+ client = Restforce.new(username: 'foo',
120
+ client_id: 'client_id',
121
+ instance_url: 'instance_url',
122
+ jwt_key: 'certificate_private_key',
123
+ api_version: '38.0')
124
+ ```
125
+
126
+ The `jwt_key` option is the private key of the certificate uploaded to your Connected App in Salesforce.
127
+ Choose "use digital signatures" in the Connected App configuration screen to upload your certificate.
128
+
114
129
  You can also set the username, password, security token, client ID, client
115
130
  secret and API version in environment variables:
116
131
 
@@ -473,11 +488,11 @@ document = client.query('select Id, Name, Body from Document').first
473
488
  File.open(document.Name, 'wb') { |f| f.write(document.Body) }
474
489
  ```
475
490
 
476
- **Note:** The example above is only applicable if your SOQL query returns a single Document record. If more than one record is returned,
491
+ **Note:** The example above is only applicable if your SOQL query returns a single Document record. If more than one record is returned,
477
492
  the Body field contains an URL to retrieve the BLOB content for the first 2000 records returned. Subsequent records contain the BLOB content
478
- in the Body field. This is confusing and hard to debug. See notes in [Issue #301](https://github.com/restforce/restforce/issues/301#issuecomment-298972959) explaining this detail.
479
- **Executive Summary:** Don't retrieve the Body field in a SOQL query; instead, use the BLOB retrieval URL documented
480
- in [SObject BLOB Retrieve](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_blob_retrieve.htm)
493
+ in the Body field. This is confusing and hard to debug. See notes in [Issue #301](https://github.com/restforce/restforce/issues/301#issuecomment-298972959) explaining this detail.
494
+ **Executive Summary:** Don't retrieve the Body field in a SOQL query; instead, use the BLOB retrieval URL documented
495
+ in [SObject BLOB Retrieve](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_sobject_blob_retrieve.htm)
481
496
 
482
497
  * * *
483
498
 
@@ -513,8 +528,10 @@ client.get('/services/apexrest/FieldCase', company: 'GenePoint')
513
528
 
514
529
  ### Streaming
515
530
 
516
- Restforce supports the [Streaming API](http://wiki.developerforce.com/page/Getting_Started_with_the_Force.com_Streaming_API), and makes implementing
517
- pub/sub with Salesforce a trivial task:
531
+ Restforce supports the [Streaming API](https://trailhead.salesforce.com/en/content/learn/modules/api_basics/api_basics_streaming), and makes implementing
532
+ pub/sub with Salesforce a trivial task.
533
+
534
+ Here is an example of creating and subscribing to a `PushTopic`:
518
535
 
519
536
  ```ruby
520
537
  # Restforce uses faye as the underlying implementation for CometD.
@@ -538,7 +555,7 @@ client.create!('PushTopic',
538
555
 
539
556
  EM.run do
540
557
  # Subscribe to the PushTopic.
541
- client.subscribe 'AllAccounts' do |message|
558
+ client.subscription '/topic/AllAccounts' do |message|
542
559
  puts message.inspect
543
560
  end
544
561
  end
@@ -559,7 +576,7 @@ that event ID:
559
576
  ```ruby
560
577
  EM.run {
561
578
  # Subscribe to the PushTopic.
562
- client.subscribe 'AllAccounts', replay: 10 do |message|
579
+ client.subscription '/topic/AllAccounts', replay: 10 do |message|
563
580
  puts message.inspect
564
581
  end
565
582
  }
@@ -574,14 +591,14 @@ There are two magic values for the replay ID accepted by Salesforce:
574
591
 
575
592
  **Warning**: Only use a replay ID of a event from the last 24 hours otherwise
576
593
  Salesforce will not send anything, including newer events. If in doubt, use one
577
- of the two magic replay IDs mentioned above.
594
+ of the two magic replay IDs mentioned above.
578
595
 
579
- You might want to store the replay ID in some sort of datastore so you can
596
+ You might want to store the replay ID in some sort of datastore so you can
580
597
  access it, for example between application restarts. In that case, there is the
581
598
  option of passing a custom replay handler which responds to `[]` and `[]=`.
582
599
 
583
600
  Below is a sample replay handler that stores the replay ID for each channel in
584
- memory using a Hash, stores a timestamp and has some rudimentary logic that
601
+ memory using a Hash, stores a timestamp and has some rudimentary logic that
585
602
  will use one of the magic IDs depending on the value of the timestamp:
586
603
 
587
604
  ```ruby
@@ -633,13 +650,13 @@ of the subscription:
633
650
  EM.run {
634
651
  # Subscribe to the PushTopic and use the custom replay handler to store any
635
652
  # received replay ID.
636
- client.subscribe 'AllAccounts', replay: SimpleReplayHandler.new do |message|
653
+ client.subscription '/topic/AllAccounts', replay: SimpleReplayHandler.new do |message|
637
654
  puts message.inspect
638
655
  end
639
656
  }
640
- ```
657
+ ```
641
658
 
642
- _See also_:
659
+ _See also_:
643
660
 
644
661
  * [Force.com Streaming API docs](http://www.salesforce.com/us/developer/docs/api_streaming/index.htm)
645
662
  * [Message Durability docs](https://developer.salesforce.com/docs/atlas.en-us.api_streaming.meta/api_streaming/using_streaming_api_durability.htm)
data/lib/restforce.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'faraday'
4
4
  require 'faraday_middleware'
5
5
  require 'json'
6
+ require 'jwt'
6
7
 
7
8
  require 'restforce/version'
8
9
  require 'restforce/config'
@@ -47,15 +48,15 @@ module Restforce
47
48
  APIVersionError = Class.new(Error)
48
49
  BatchAPIError = Class.new(Error)
49
50
 
50
- # Inherit from Faraday::Error::ResourceNotFound for backwards-compatibility
51
- # Consumers of this library that rescue and handle Faraday::Error::ResourceNotFound
51
+ # Inherit from Faraday::ResourceNotFound for backwards-compatibility
52
+ # Consumers of this library that rescue and handle Faraday::ResourceNotFound
52
53
  # can continue to do so.
53
- NotFoundError = Class.new(Faraday::Error::ResourceNotFound)
54
+ NotFoundError = Class.new(Faraday::ResourceNotFound)
54
55
 
55
- # Inherit from Faraday::Error::ClientError for backwards-compatibility
56
- # Consumers of this library that rescue and handle Faraday::Error::ClientError
56
+ # Inherit from Faraday::ClientError for backwards-compatibility
57
+ # Consumers of this library that rescue and handle Faraday::ClientError
57
58
  # can continue to do so.
58
- ResponseError = Class.new(Faraday::Error::ClientError)
59
+ ResponseError = Class.new(Faraday::ClientError)
59
60
  MatchesMultipleError= Class.new(ResponseError)
60
61
  EntityTooLargeError = Class.new(ResponseError)
61
62
 
@@ -510,7 +510,7 @@ module Restforce
510
510
 
511
511
  # Internal: Errors that should be rescued from in non-bang methods
512
512
  def exceptions
513
- [Faraday::Error::ClientError]
513
+ [Faraday::ClientError]
514
514
  end
515
515
  end
516
516
  end
@@ -19,6 +19,8 @@ module Restforce
19
19
  Restforce::Middleware::Authentication::Password
20
20
  elsif oauth_refresh?
21
21
  Restforce::Middleware::Authentication::Token
22
+ elsif jwt?
23
+ Restforce::Middleware::Authentication::JWTBearer
22
24
  end
23
25
  end
24
26
 
@@ -38,6 +40,14 @@ module Restforce
38
40
  options[:client_id] &&
39
41
  options[:client_secret]
40
42
  end
43
+
44
+ # Internal: Returns true if jwt bearer token flow should be used for
45
+ # authentication.
46
+ def jwt?
47
+ options[:jwt_key] &&
48
+ options[:username] &&
49
+ options[:client_id]
50
+ end
41
51
  end
42
52
  end
43
53
  end
@@ -28,6 +28,8 @@ module Restforce
28
28
  # password and oauth authentication
29
29
  # :client_secret - The oauth client secret to use.
30
30
  #
31
+ # :jwt_key - The private key for JWT authentication
32
+ #
31
33
  # :host - The String hostname to use during
32
34
  # authentication requests
33
35
  # (default: 'login.salesforce.com').
@@ -72,9 +72,9 @@ module Restforce
72
72
  # Internal: Faraday Connection options
73
73
  def connection_options
74
74
  { request: {
75
- timeout: options[:timeout],
76
- open_timeout: options[:timeout]
77
- },
75
+ timeout: options[:timeout],
76
+ open_timeout: options[:timeout]
77
+ },
78
78
  proxy: options[:proxy_uri],
79
79
  ssl: options[:ssl] }
80
80
  end
@@ -5,13 +5,28 @@ module Restforce
5
5
  module Streaming
6
6
  # Public: Subscribe to a PushTopic
7
7
  #
8
- # channels - The name of the PushTopic channel(s) to subscribe to.
8
+ # topics - The name of the PushTopic channel(s) to subscribe to.
9
9
  # block - A block to run when a new message is received.
10
10
  #
11
11
  # Returns a Faye::Subscription
12
- def subscribe(channels, options = {}, &block)
13
- Array(channels).each { |channel| replay_handlers[channel] = options[:replay] }
14
- faye.subscribe Array(channels).map { |channel| "/topic/#{channel}" }, &block
12
+ def legacy_subscribe(topics, options = {}, &block)
13
+ topics = Array(topics).map { |channel| "/topic/#{channel}" }
14
+ subscription(topics, options, &block)
15
+ end
16
+ alias subscribe legacy_subscribe
17
+
18
+ # Public: Subscribe to one or more Streaming API channels
19
+ #
20
+ # channels - The name of the Streaming API (cometD) channel(s) to subscribe to.
21
+ # block - A block to run when a new message is received.
22
+ #
23
+ # Returns a Faye::Subscription
24
+ def subscription(channels, options = {}, &block)
25
+ one_or_more_channels = Array(channels)
26
+ one_or_more_channels.each do |channel|
27
+ replay_handlers[channel] = options[:replay]
28
+ end
29
+ faye.subscribe(one_or_more_channels, &block)
15
30
  end
16
31
 
17
32
  # Public: Faye client to use for subscribing to PushTopics
@@ -49,7 +64,7 @@ module Restforce
49
64
 
50
65
  def incoming(message, callback)
51
66
  callback.call(message).tap do
52
- channel = message.fetch('channel').gsub('/topic/', '')
67
+ channel = message.fetch('channel')
53
68
  replay_id = message.fetch('data', {}).fetch('event', {})['replayId']
54
69
 
55
70
  handler = @replay_handlers[channel]
@@ -64,12 +79,12 @@ module Restforce
64
79
  # Leave non-subscribe messages alone
65
80
  return callback.call(message) unless message['channel'] == '/meta/subscribe'
66
81
 
67
- channel = message['subscription'].gsub('/topic/', '')
82
+ channel = message['subscription']
68
83
 
69
84
  # Set the replay value for the channel
70
85
  message['ext'] ||= {}
71
86
  message['ext']['replay'] = {
72
- "/topic/#{channel}" => replay_id(channel)
87
+ channel => replay_id(channel)
73
88
  }
74
89
 
75
90
  # Carry on and send the message to the server
@@ -108,6 +108,9 @@ module Restforce
108
108
  # The OAuth client secret
109
109
  option :client_secret, default: lambda { ENV['SALESFORCE_CLIENT_SECRET'] }
110
110
 
111
+ # The private key for JWT authentication
112
+ option :jwt_key
113
+
111
114
  # Set this to true if you're authenticating with a Sandbox instance.
112
115
  # Defaults to false.
113
116
  option :host, default: lambda { ENV['SALESFORCE_HOST'] || 'login.salesforce.com' }
@@ -28,7 +28,7 @@ module Restforce
28
28
  # of sobject records.
29
29
  Restforce::Collection
30
30
  elsif val.key? 'attributes'
31
- case (val['attributes']['type'])
31
+ case val.dig('attributes', 'type')
32
32
  when "Attachment"
33
33
  Restforce::Attachment
34
34
  when "Document"
@@ -6,8 +6,9 @@ module Restforce
6
6
  # will attempt to either reauthenticate (username and password) or refresh
7
7
  # the oauth access token (if a refresh token is present).
8
8
  class Middleware::Authentication < Restforce::Middleware
9
- autoload :Password, 'restforce/middleware/authentication/password'
10
- autoload :Token, 'restforce/middleware/authentication/token'
9
+ autoload :Password, 'restforce/middleware/authentication/password'
10
+ autoload :Token, 'restforce/middleware/authentication/token'
11
+ autoload :JWTBearer, 'restforce/middleware/authentication/jwt_bearer'
11
12
 
12
13
  # Rescue from 401's, authenticate then raise the error again so the client
13
14
  # can reissue the request.
@@ -62,7 +63,10 @@ module Restforce
62
63
 
63
64
  # Internal: The parsed error response.
64
65
  def error_message(response)
65
- "#{response.body['error']}: #{response.body['error_description']}"
66
+ return response.status.to_s unless response.body
67
+
68
+ "#{response.body['error']}: #{response.body['error_description']} " \
69
+ "(#{response.status})"
66
70
  end
67
71
 
68
72
  # Featured detect form encoding.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt'
4
+
5
+ module Restforce
6
+ class Middleware
7
+ class Authentication
8
+ class JWTBearer < Restforce::Middleware::Authentication
9
+ def params
10
+ {
11
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
12
+ assertion: jwt_bearer_token
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ def jwt_bearer_token
19
+ JWT.encode claim_set, private_key, 'RS256'
20
+ end
21
+
22
+ def claim_set
23
+ {
24
+ iss: @options[:client_id],
25
+ sub: @options[:username],
26
+ aud: @options[:host],
27
+ iat: Time.now.utc.to_i,
28
+ exp: Time.now.utc.to_i + 180
29
+ }
30
+ end
31
+
32
+ def private_key
33
+ OpenSSL::PKey::RSA.new(@options[:jwt_key])
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -14,7 +14,7 @@ module Restforce
14
14
  end
15
15
 
16
16
  def url_prefix_set?
17
- !!(connection.url_prefix&.host)
17
+ !!connection.url_prefix&.host
18
18
  end
19
19
  end
20
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restforce
4
- VERSION = '4.0.0'
4
+ VERSION = '4.3.0'
5
5
  end
data/restforce.gemspec CHANGED
@@ -28,13 +28,14 @@ Gem::Specification.new do |gem|
28
28
  gem.add_dependency 'faraday_middleware', ['>= 0.8.8', '<= 1.0']
29
29
 
30
30
  gem.add_dependency 'json', '>= 1.7.5'
31
+ gem.add_dependency 'jwt', ['>= 1.5.6']
31
32
 
32
- gem.add_dependency 'hashie', ['>= 1.2.0', '< 4.0']
33
+ gem.add_dependency 'hashie', '>= 1.2.0', '< 5.0'
33
34
 
34
35
  gem.add_development_dependency 'faye' unless RUBY_PLATFORM == 'java'
35
36
  gem.add_development_dependency 'rspec', '~> 2.14.0'
36
- gem.add_development_dependency 'rspec_junit_formatter', '~> 0.3.0'
37
- gem.add_development_dependency 'rubocop', '~> 0.75.0'
37
+ gem.add_development_dependency 'rspec_junit_formatter', '~> 0.4.1'
38
+ gem.add_development_dependency 'rubocop', '~> 0.77.0'
38
39
  gem.add_development_dependency 'simplecov', '~> 0.17.1'
39
40
  gem.add_development_dependency 'webmock', '~> 3.7.6'
40
41
  end
@@ -0,0 +1,27 @@
1
+ -----BEGIN RSA PRIVATE KEY-----
2
+ MIIEpAIBAAKCAQEAy3KYqxZIgVDgFwdA+OQcKMJQu3iUTlyCSk9b3RLBOudnvk8u
3
+ n0ShtKkOKB4b4RZeedcrlKESoak/6NS+M7CDemRT0EagqUiz/ZsZxB2KUp7au+d8
4
+ 0KWX99/loBjDttuon8ITDw2WFC9X0+TZqfsXcQ0iV1/9Sf8WHShd8ZqShjJBlEvf
5
+ 7u7VdNW8dXrl+4cvpPzspVxg6jVotEpmp875jmGRvshgx0iz0jtfAyxaaKStITC6
6
+ MxufVNDgIYQDl6queh8b9noDLtt17Eq6YnropYN1hOjaLtoLBP7AN2gsXG7N3vqC
7
+ JG619W9X4zCmKztv4oGjymInrS2msC2J02dNGQIDAQABAoIBAAurTARsJ8Z7DA9m
8
+ FBzygIb59kV6eg8wkSyP9rXscHbfdPzeb88k0Z2aILy+VV0IumyEofRJdNce7RJ+
9
+ uVYfprrrbD9C/c4X5HMEZWrxQtDQWb1zXp5dESVfiz0ujnM7kCVxrUQsxFHuETyP
10
+ IMj2JPcQCMs4L0ACSJNtkE3eTs8xko5kwDHZGiLTi5jD1bLgaHl1A+9CTU8LosTy
11
+ hEIrNSZfNidDPU4QSbwoElYZxpDMSbtyHaIk1WHz7zLzWoogK3x5AIQh64wWAQVd
12
+ zzlp2j2jSM7oQ9j+k1aNiUBdDoRX53jmaIwE/1WDW/LT33qAoqRw+5qHeLRoRcfu
13
+ 3uj/WI0CgYEA6wnpIUhqqWT+febhXtCr1mAJlAJpzUUQncN6Zk0Kj/kE1V52OqgL
14
+ gtOactII7J3+0zK7KGptqseTank0ghmGNdRBQ7+1JTQhpjLrCm/huKDhl+sBk95u
15
+ opxw/ZTwMFYPwsmZlFcy4uWRjtI+QzaV+2Xk5JF57H8vUiX/+XqseQcCgYEA3Zdw
16
+ zVHpcVPlyiXCbSvwb9IYXiJaQl/Rg96Klxah3MZNyRRKe5IoKUTJwEDuQ1MAHrve
17
+ cWrNLcXhX6r/PzIXSSLe71wgwpn7UcaqWzZJqqN7OIGEeTzYWbB6tGhse7Dw7tWB
18
+ hRkQSE0LPzZqboHz5msRM02sa61qiI5+ASJvIN8CgYEAvT+IoEzv3R89ruBVPQPm
19
+ KMHBVJSw3iArJex8xJxp0c0fMDJUHhyq0BdTd/pYRzVcNm/VtNAlJ2p07zlSpyKo
20
+ JvWV61gUIjWclnbPO+MkK4YWvzzxUz+5c2NlszjWQQU6wYuUBpZDmeBg2E++5F2y
21
+ W+8KY2QjeOJbltiUCCvXbccCgYEAqARYB5aARumyZqBS16xlVqQazeWGQqWcmzx2
22
+ ITGL8XZ7LGgyQZgE06XQw/F3t5yLjsIsXBr7ECXmST/C4gv9E/tYxm04edV/dfYI
23
+ 3bhACx6CI8owxCyabwcdQwWam/8B8FX7KwxiCDBCwt9ju/7VDHVKSXgvsEWBbaF9
24
+ cSbG1EkCgYBZFztTUnD/cLMcvLUegN0K+6Qa3x3nRSrlrJ+v51mU1X8G8qNyFO67
25
+ gUq9h4xbCl4Z5ZTuFKXwPM4XaMzfYdrWNS2zl5IG14FXS077GhDKe062b9mFoxtm
26
+ aViCit4Hm8xpLTS8x9KB7yYAiF9sR/GklW1SUCIqnpL9JShkhzjfZw==
27
+ -----END RSA PRIVATE KEY-----
@@ -125,7 +125,7 @@ shared_examples_for Restforce::AbstractClient do
125
125
 
126
126
  it {
127
127
  should raise_error(
128
- Faraday::Error::ResourceNotFound,
128
+ Faraday::ResourceNotFound,
129
129
  "#{error.first['errorCode']}: #{error.first['message']}"
130
130
  )
131
131
  }
@@ -239,7 +239,7 @@ shared_examples_for Restforce::AbstractClient do
239
239
  status: 404
240
240
 
241
241
  subject { lambda { destroy! } }
242
- it { should raise_error Faraday::Error::ResourceNotFound }
242
+ it { should raise_error Faraday::ResourceNotFound }
243
243
  end
244
244
 
245
245
  context 'with success' do
@@ -274,7 +274,7 @@ describe Restforce::Concerns::API do
274
274
  end
275
275
 
276
276
  it 'rescues exceptions' do
277
- [Faraday::Error::ClientError].each do |exception_klass|
277
+ [Faraday::ClientError].each do |exception_klass|
278
278
  client.should_receive(:"#{method}!").
279
279
  with(*args).
280
280
  and_raise(exception_klass.new(nil))
@@ -52,6 +52,16 @@ describe Restforce::Concerns::Authentication do
52
52
 
53
53
  it { should eq Restforce::Middleware::Authentication::Token }
54
54
  end
55
+
56
+ context 'when jwt option is provided' do
57
+ before do
58
+ client.stub username_password?: false
59
+ client.stub oauth_refresh?: false
60
+ client.stub jwt?: true
61
+ end
62
+
63
+ it { should eq Restforce::Middleware::Authentication::JWTBearer }
64
+ end
55
65
  end
56
66
 
57
67
  describe '.username_password?' do
@@ -100,4 +110,29 @@ describe Restforce::Concerns::Authentication do
100
110
  it { should_not be_true }
101
111
  end
102
112
  end
113
+
114
+ describe '.jwt?' do
115
+ subject { client.jwt? }
116
+ let(:options) do
117
+ {}
118
+ end
119
+
120
+ before do
121
+ client.stub options: options
122
+ end
123
+
124
+ context 'when jwt options are provided' do
125
+ let(:options) do
126
+ { jwt_key: 'JWT_PRIVATE_KEY',
127
+ username: 'foo',
128
+ client_id: 'client' }
129
+ end
130
+
131
+ it { should be_true }
132
+ end
133
+
134
+ context 'when jwt options are not provided' do
135
+ it { should_not be_true }
136
+ end
137
+ end
103
138
  end
@@ -4,18 +4,19 @@ require 'spec_helper'
4
4
 
5
5
  describe Restforce::Concerns::Streaming, event_machine: true do
6
6
  describe '.subscribe' do
7
- let(:channels) { %w[channel1 channel2] }
8
- let(:topics) { channels.map { |c| "/topic/#{c}" } }
7
+ let(:channels) do
8
+ ['/topic/topic1', '/event/MyCustomEvent__e', '/data/ChangeEvents']
9
+ end
9
10
  let(:subscribe_block) { lambda { 'subscribe' } }
10
11
  let(:faye_double) { double('Faye') }
11
12
 
12
13
  it 'subscribes to the topics with faye' do
13
14
  faye_double.
14
15
  should_receive(:subscribe).
15
- with(topics, &subscribe_block)
16
+ with(channels, &subscribe_block)
16
17
  client.stub faye: faye_double
17
18
 
18
- client.subscribe(channels, &subscribe_block)
19
+ client.subscription(channels, &subscribe_block)
19
20
  end
20
21
 
21
22
  context "replay_handlers" do
@@ -25,24 +26,51 @@ describe Restforce::Concerns::Streaming, event_machine: true do
25
26
  }
26
27
 
27
28
  it 'registers nil handlers when no replay option is given' do
28
- client.subscribe(channels, &subscribe_block)
29
- client.replay_handlers.should eq('channel1' => nil, 'channel2' => nil)
29
+ client.subscription(channels, &subscribe_block)
30
+ client.replay_handlers.should eq(
31
+ '/topic/topic1' => nil,
32
+ '/event/MyCustomEvent__e' => nil,
33
+ '/data/ChangeEvents' => nil
34
+ )
30
35
  end
31
36
 
32
37
  it 'registers a replay_handler for each channel given' do
33
- client.subscribe(channels, replay: -2, &subscribe_block)
34
- client.replay_handlers.should eq('channel1' => -2, 'channel2' => -2)
38
+ client.subscription(channels, replay: -2, &subscribe_block)
39
+ client.replay_handlers.should eq(
40
+ '/topic/topic1' => -2,
41
+ '/event/MyCustomEvent__e' => -2,
42
+ '/data/ChangeEvents' => -2
43
+ )
35
44
  end
36
45
 
37
46
  it 'replaces earlier handlers in subsequent calls' do
38
- client.subscribe(%w[channel1 channel2], replay: 2, &subscribe_block)
39
- client.subscribe(%w[channel2 channel3], replay: 3, &subscribe_block)
47
+ client.subscription(
48
+ ['/topic/channel1', '/topic/channel2'],
49
+ replay: 2,
50
+ &subscribe_block
51
+ )
52
+ client.subscription(
53
+ ['/topic/channel2', '/topic/channel3'],
54
+ replay: 3,
55
+ &subscribe_block
56
+ )
57
+
40
58
  client.replay_handlers.should eq(
41
- 'channel1' => 2,
42
- 'channel2' => 3,
43
- 'channel3' => 3
59
+ '/topic/channel1' => 2,
60
+ '/topic/channel2' => 3,
61
+ '/topic/channel3' => 3
44
62
  )
45
63
  end
64
+
65
+ context 'backwards compatibility' do
66
+ it 'it assumes channels are push topics' do
67
+ client.subscribe(%w[channel1 channel2], replay: -2, &subscribe_block)
68
+ client.replay_handlers.should eq(
69
+ '/topic/channel1' => -2,
70
+ '/topic/channel2' => -2
71
+ )
72
+ end
73
+ end
46
74
  end
47
75
  end
48
76
 
@@ -87,41 +115,41 @@ describe Restforce::Concerns::Streaming, event_machine: true do
87
115
  let(:extension) { Restforce::Concerns::Streaming::ReplayExtension.new(handlers) }
88
116
 
89
117
  it 'sends nil without a specified handler' do
90
- output = subscribe(extension, to: "channel1")
118
+ output = subscribe(extension, to: "/topic/channel1")
91
119
  read_replay(output).should eq('/topic/channel1' => nil)
92
120
  end
93
121
 
94
122
  it 'with a scalar replay id' do
95
- handlers['channel1'] = -2
96
- output = subscribe(extension, to: "channel1")
123
+ handlers['/topic/channel1'] = -2
124
+ output = subscribe(extension, to: "/topic/channel1")
97
125
  read_replay(output).should eq('/topic/channel1' => -2)
98
126
  end
99
127
 
100
128
  it 'with a hash' do
101
- hash_handler = { 'channel1' => -1, 'channel2' => -2 }
129
+ hash_handler = { '/topic/channel1' => -1, '/topic/channel2' => -2 }
102
130
 
103
- handlers['channel1'] = hash_handler
104
- handlers['channel2'] = hash_handler
131
+ handlers['/topic/channel1'] = hash_handler
132
+ handlers['/topic/channel2'] = hash_handler
105
133
 
106
- output = subscribe(extension, to: "channel1")
134
+ output = subscribe(extension, to: "/topic/channel1")
107
135
  read_replay(output).should eq('/topic/channel1' => -1)
108
136
 
109
- output = subscribe(extension, to: "channel2")
137
+ output = subscribe(extension, to: "/topic/channel2")
110
138
  read_replay(output).should eq('/topic/channel2' => -2)
111
139
  end
112
140
 
113
141
  it 'with an object' do
114
142
  custom_handler = double('custom_handler')
115
143
  custom_handler.should_receive(:[]).and_return(123)
116
- handlers['channel1'] = custom_handler
144
+ handlers['/topic/channel1'] = custom_handler
117
145
 
118
- output = subscribe(extension, to: "channel1")
146
+ output = subscribe(extension, to: "/topic/channel1")
119
147
  read_replay(output).should eq('/topic/channel1' => 123)
120
148
  end
121
149
 
122
150
  it 'remembers the last replayId' do
123
- handler = { 'channel1' => 41 }
124
- handlers['channel1'] = handler
151
+ handler = { '/topic/channel1' => 41 }
152
+ handlers['/topic/channel1'] = handler
125
153
  message = {
126
154
  'channel' => '/topic/channel1',
127
155
  'data' => {
@@ -130,12 +158,12 @@ describe Restforce::Concerns::Streaming, event_machine: true do
130
158
  }
131
159
 
132
160
  extension.incoming(message, ->(m) {})
133
- handler.should eq('channel1' => 42)
161
+ handler.should eq('/topic/channel1' => 42)
134
162
  end
135
163
 
136
164
  it 'when an incoming message has no replayId' do
137
- handler = { 'channel1' => 41 }
138
- handlers['channel1'] = handler
165
+ handler = { '/topic/channel1' => 41 }
166
+ handlers['/topic/channel1'] = handler
139
167
 
140
168
  message = {
141
169
  'channel' => '/topic/channel1',
@@ -143,7 +171,7 @@ describe Restforce::Concerns::Streaming, event_machine: true do
143
171
  }
144
172
 
145
173
  extension.incoming(message, ->(m) {})
146
- handler.should eq('channel1' => 41)
174
+ handler.should eq('/topic/channel1' => 41)
147
175
  end
148
176
 
149
177
  private
@@ -152,7 +180,7 @@ describe Restforce::Concerns::Streaming, event_machine: true do
152
180
  output = nil
153
181
  message = {
154
182
  'channel' => '/meta/subscribe',
155
- 'subscription' => "/topic/#{options[:to]}"
183
+ 'subscription' => options[:to]
156
184
  }
157
185
  extension.outgoing(message, ->(m) {
158
186
  output = m
@@ -33,6 +33,11 @@ describe Restforce::Mash do
33
33
  let(:input) { { 'attributes' => { 'type' => 'Document' } } }
34
34
  it { should eq Restforce::Document }
35
35
  end
36
+
37
+ context 'when the attributes value is nil' do
38
+ let(:input) { { 'attributes' => nil } }
39
+ it { should eq Restforce::SObject }
40
+ end
36
41
  end
37
42
 
38
43
  context 'else' do
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ describe Restforce::Middleware::Authentication::JWTBearer do
5
+ let(:jwt_key) { File.read('spec/fixtures/test_private.key') }
6
+
7
+ let(:options) do
8
+ { host: 'login.salesforce.com',
9
+ client_id: 'client_id',
10
+ username: 'foo',
11
+ jwt_key: jwt_key,
12
+ instance_url: 'https://na1.salesforce.com',
13
+ adapter: :net_http }
14
+ end
15
+
16
+ it_behaves_like 'authentication middleware' do
17
+ let(:success_request) do
18
+ stub_login_request(
19
+ body: "grant_type=grant_type—urn:ietf:params:oauth:grant-type:jwt-bearer&" \
20
+ "assertion=abc1234567890"
21
+ ).to_return(status: 200, body: fixture(:auth_success_response))
22
+ end
23
+
24
+ let(:fail_request) do
25
+ stub_login_request(
26
+ body: "grant_type=grant_type—urn:ietf:params:oauth:grant-type:jwt-bearer&" \
27
+ "assertion=abc1234567890"
28
+ ).to_return(status: 400, body: fixture(:refresh_error_response))
29
+ end
30
+ end
31
+
32
+ context 'allows jwt_key as string' do
33
+ let(:jwt_key) do
34
+ File.read('spec/fixtures/test_private.key')
35
+ end
36
+
37
+ let(:options) do
38
+ { host: 'login.salesforce.com',
39
+ client_id: 'client_id',
40
+ username: 'foo',
41
+ jwt_key: jwt_key,
42
+ instance_url: 'https://na1.salesforce.com',
43
+ adapter: :net_http }
44
+ end
45
+
46
+ it_behaves_like 'authentication middleware' do
47
+ let(:success_request) do
48
+ stub_login_request(
49
+ body: "grant_type=grant_type—urn:ietf:params:oauth:grant-type:jwt-bearer&" \
50
+ "assertion=abc1234567890"
51
+ ).to_return(status: 200, body: fixture(:auth_success_response))
52
+ end
53
+
54
+ let(:fail_request) do
55
+ stub_login_request(
56
+ body: "grant_type=grant_type—urn:ietf:params:oauth:grant-type:jwt-bearer&" \
57
+ "assertion=abc1234567890"
58
+ ).to_return(status: 400, body: fixture(:refresh_error_response))
59
+ end
60
+ end
61
+ end
62
+ end
@@ -89,4 +89,25 @@ describe Restforce::Middleware::Authentication do
89
89
  connection.ssl[:version].should eq(:TLSv1_2)
90
90
  end
91
91
  end
92
+
93
+ describe '.error_message' do
94
+ context 'when response.body is present' do
95
+ let(:response) {
96
+ Faraday::Response.new(
97
+ body: { 'error' => 'error', 'error_description' => 'description' },
98
+ status: 401
99
+ )
100
+ }
101
+
102
+ subject { middleware.error_message(response) }
103
+ it { should eq "error: description (401)" }
104
+ end
105
+
106
+ context 'when response.body is nil' do
107
+ let(:response) { Faraday::Response.new(status: 401) }
108
+
109
+ subject { middleware.error_message(response) }
110
+ it { should eq "401" }
111
+ end
112
+ end
92
113
  end
@@ -18,8 +18,8 @@ describe Restforce::Middleware::RaiseError do
18
18
  'INVALID_FIELD: error_message'
19
19
  end
20
20
 
21
- it 'raises an error that inherits from Faraday::Error::ResourceNotFound' do
22
- expect { on_complete }.to raise_error Faraday::Error::ResourceNotFound
21
+ it 'raises an error that inherits from Faraday::ResourceNotFound' do
22
+ expect { on_complete }.to raise_error Faraday::ResourceNotFound
23
23
  end
24
24
  end
25
25
 
@@ -31,8 +31,8 @@ describe Restforce::Middleware::RaiseError do
31
31
  /300: The external ID provided/
32
32
  end
33
33
 
34
- it 'raises an error that inherits from Faraday::Error::ClientError' do
35
- expect { on_complete }.to raise_error Faraday::Error::ClientError
34
+ it 'raises an error that inherits from Faraday::ClientError' do
35
+ expect { on_complete }.to raise_error Faraday::ClientError
36
36
  end
37
37
  end
38
38
 
@@ -44,8 +44,8 @@ describe Restforce::Middleware::RaiseError do
44
44
  'INVALID_FIELD: error_message'
45
45
  end
46
46
 
47
- it 'raises an error that inherits from Faraday::Error::ClientError' do
48
- expect { on_complete }.to raise_error Faraday::Error::ClientError
47
+ it 'raises an error that inherits from Faraday::ClientError' do
48
+ expect { on_complete }.to raise_error Faraday::ClientError
49
49
  end
50
50
  end
51
51
 
@@ -66,8 +66,8 @@ describe Restforce::Middleware::RaiseError do
66
66
  '413: Request Entity Too Large'
67
67
  end
68
68
 
69
- it 'raises an error that inherits from Faraday::Error::ClientError' do
70
- expect { on_complete }.to raise_error Faraday::Error::ClientError
69
+ it 'raises an error that inherits from Faraday::ClientError' do
70
+ expect { on_complete }.to raise_error Faraday::ClientError
71
71
  end
72
72
  end
73
73
 
@@ -80,8 +80,8 @@ describe Restforce::Middleware::RaiseError do
80
80
  "(error code missing): #{body}"
81
81
  end
82
82
 
83
- it 'raises an error that inherits from Faraday::Error::ClientError' do
84
- expect { on_complete }.to raise_error Faraday::Error::ClientError,
83
+ it 'raises an error that inherits from Faraday::ClientError' do
84
+ expect { on_complete }.to raise_error Faraday::ClientError,
85
85
  "(error code missing): #{body}"
86
86
  end
87
87
  end
@@ -45,13 +45,10 @@ describe Restforce::SObject do
45
45
  destroy: :destroy,
46
46
  destroy!: :destroy! }.each do |method, receiver|
47
47
  describe ".#{method}" do
48
- subject(:send_method) { sobject.send(method) }
48
+ subject(:send_method) { lambda { sobject.send(method) } }
49
49
 
50
50
  context 'when an Id was not queried' do
51
- it "raises an error" do
52
- expect { send_method }.to raise_error ArgumentError,
53
- /need to query the Id for the record/
54
- end
51
+ it { should raise_error ArgumentError, /need to query the Id for the record/ }
55
52
  end
56
53
 
57
54
  context 'when an Id is present' do
metadata CHANGED
@@ -1,36 +1,36 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restforce
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric J. Holmes
8
8
  - Tim Rogers
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-10-09 00:00:00.000000000 Z
12
+ date: 2021-02-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - ">="
19
- - !ruby/object:Gem::Version
20
- version: 0.9.0
21
18
  - - "<="
22
19
  - !ruby/object:Gem::Version
23
20
  version: '1.0'
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.9.0
24
24
  type: :runtime
25
25
  prerelease: false
26
26
  version_requirements: !ruby/object:Gem::Requirement
27
27
  requirements:
28
- - - ">="
29
- - !ruby/object:Gem::Version
30
- version: 0.9.0
31
28
  - - "<="
32
29
  - !ruby/object:Gem::Version
33
30
  version: '1.0'
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.9.0
34
34
  - !ruby/object:Gem::Dependency
35
35
  name: faraday_middleware
36
36
  requirement: !ruby/object:Gem::Requirement
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: 1.7.5
68
+ - !ruby/object:Gem::Dependency
69
+ name: jwt
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 1.5.6
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.5.6
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: hashie
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -74,7 +88,7 @@ dependencies:
74
88
  version: 1.2.0
75
89
  - - "<"
76
90
  - !ruby/object:Gem::Version
77
- version: '4.0'
91
+ version: '5.0'
78
92
  type: :runtime
79
93
  prerelease: false
80
94
  version_requirements: !ruby/object:Gem::Requirement
@@ -84,7 +98,7 @@ dependencies:
84
98
  version: 1.2.0
85
99
  - - "<"
86
100
  - !ruby/object:Gem::Version
87
- version: '4.0'
101
+ version: '5.0'
88
102
  - !ruby/object:Gem::Dependency
89
103
  name: faye
90
104
  requirement: !ruby/object:Gem::Requirement
@@ -119,28 +133,28 @@ dependencies:
119
133
  requirements:
120
134
  - - "~>"
121
135
  - !ruby/object:Gem::Version
122
- version: 0.3.0
136
+ version: 0.4.1
123
137
  type: :development
124
138
  prerelease: false
125
139
  version_requirements: !ruby/object:Gem::Requirement
126
140
  requirements:
127
141
  - - "~>"
128
142
  - !ruby/object:Gem::Version
129
- version: 0.3.0
143
+ version: 0.4.1
130
144
  - !ruby/object:Gem::Dependency
131
145
  name: rubocop
132
146
  requirement: !ruby/object:Gem::Requirement
133
147
  requirements:
134
148
  - - "~>"
135
149
  - !ruby/object:Gem::Version
136
- version: 0.75.0
150
+ version: 0.77.0
137
151
  type: :development
138
152
  prerelease: false
139
153
  version_requirements: !ruby/object:Gem::Requirement
140
154
  requirements:
141
155
  - - "~>"
142
156
  - !ruby/object:Gem::Version
143
- version: 0.75.0
157
+ version: 0.77.0
144
158
  - !ruby/object:Gem::Dependency
145
159
  name: simplecov
146
160
  requirement: !ruby/object:Gem::Requirement
@@ -210,6 +224,7 @@ files:
210
224
  - lib/restforce/mash.rb
211
225
  - lib/restforce/middleware.rb
212
226
  - lib/restforce/middleware/authentication.rb
227
+ - lib/restforce/middleware/authentication/jwt_bearer.rb
213
228
  - lib/restforce/middleware/authentication/password.rb
214
229
  - lib/restforce/middleware/authentication/token.rb
215
230
  - lib/restforce/middleware/authorization.rb
@@ -266,6 +281,7 @@ files:
266
281
  - spec/fixtures/sobject/upsert_multiple_error_response.json
267
282
  - spec/fixtures/sobject/upsert_updated_success_response.json
268
283
  - spec/fixtures/sobject/write_error_response.json
284
+ - spec/fixtures/test_private.key
269
285
  - spec/integration/abstract_client_spec.rb
270
286
  - spec/integration/data/client_spec.rb
271
287
  - spec/spec_helper.rb
@@ -291,6 +307,7 @@ files:
291
307
  - spec/unit/data/client_spec.rb
292
308
  - spec/unit/document_spec.rb
293
309
  - spec/unit/mash_spec.rb
310
+ - spec/unit/middleware/authentication/jwt_bearer_spec.rb
294
311
  - spec/unit/middleware/authentication/password_spec.rb
295
312
  - spec/unit/middleware/authentication/token_spec.rb
296
313
  - spec/unit/middleware/authentication_spec.rb
@@ -310,7 +327,7 @@ licenses:
310
327
  metadata:
311
328
  source_code_uri: https://github.com/restforce/restforce
312
329
  changelog_uri: https://github.com/restforce/restforce/blob/master/CHANGELOG.md
313
- post_install_message:
330
+ post_install_message:
314
331
  rdoc_options: []
315
332
  require_paths:
316
333
  - lib
@@ -325,8 +342,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
325
342
  - !ruby/object:Gem::Version
326
343
  version: '0'
327
344
  requirements: []
328
- rubygems_version: 3.0.3
329
- signing_key:
345
+ rubygems_version: 3.2.5
346
+ signing_key:
330
347
  specification_version: 4
331
348
  summary: A lightweight ruby client for the Salesforce REST API.
332
349
  test_files:
@@ -364,6 +381,7 @@ test_files:
364
381
  - spec/fixtures/sobject/upsert_multiple_error_response.json
365
382
  - spec/fixtures/sobject/upsert_updated_success_response.json
366
383
  - spec/fixtures/sobject/write_error_response.json
384
+ - spec/fixtures/test_private.key
367
385
  - spec/integration/abstract_client_spec.rb
368
386
  - spec/integration/data/client_spec.rb
369
387
  - spec/spec_helper.rb
@@ -389,6 +407,7 @@ test_files:
389
407
  - spec/unit/data/client_spec.rb
390
408
  - spec/unit/document_spec.rb
391
409
  - spec/unit/mash_spec.rb
410
+ - spec/unit/middleware/authentication/jwt_bearer_spec.rb
392
411
  - spec/unit/middleware/authentication/password_spec.rb
393
412
  - spec/unit/middleware/authentication/token_spec.rb
394
413
  - spec/unit/middleware/authentication_spec.rb