vkontakte_api 1.2 → 1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +5 -0
- data/Guardfile +3 -1
- data/README.md +9 -3
- data/lib/vkontakte_api/client.rb +10 -0
- data/lib/vkontakte_api/namespace.rb +30 -1
- data/lib/vkontakte_api/resolvable.rb +1 -1
- data/lib/vkontakte_api/resolver.rb +13 -28
- data/lib/vkontakte_api/uploading.rb +3 -3
- data/lib/vkontakte_api/version.rb +1 -1
- data/spec/integration_spec.rb +24 -27
- data/spec/vkontakte_api/api_spec.rb +13 -13
- data/spec/vkontakte_api/authorization_spec.rb +27 -24
- data/spec/vkontakte_api/client_spec.rb +74 -38
- data/spec/vkontakte_api/configuration_spec.rb +12 -11
- data/spec/vkontakte_api/error_spec.rb +10 -12
- data/spec/vkontakte_api/logger_spec.rb +31 -28
- data/spec/vkontakte_api/method_spec.rb +21 -18
- data/spec/vkontakte_api/namespace_spec.rb +47 -1
- data/spec/vkontakte_api/resolvable_spec.rb +5 -7
- data/spec/vkontakte_api/resolver_spec.rb +6 -66
- data/spec/vkontakte_api/result_spec.rb +43 -34
- data/spec/vkontakte_api/uploading_spec.rb +20 -16
- data/spec/vkontakte_api/utils_spec.rb +15 -18
- data/spec/vkontakte_api_spec.rb +9 -7
- data/vkontakte_api.gemspec +7 -2
- metadata +34 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60dffad7bbac6d779ff73eaeb39221648bd06b81
|
4
|
+
data.tar.gz: c702f943689840e842d49859f2bcc87b10fce869
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fc49252ddc7b95434e0f2f5ac59cb4ea4f749c1a30a7026b1bbefa5f570ae836f3b25125f70c9f8026aee2322b5cd4f878e19d90bc9a90f26948951f250d14b8
|
7
|
+
data.tar.gz: 90a2c6d2da5ac5e7e08851575373c7bc5e53684a88d5111e7a128026d3aa2accfb8b5c41bd2bedc373b3759009cf35d68c1132eb2f55691da93f37438fc9a1b0
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
## 1.3 (01.11.2013)
|
2
|
+
|
3
|
+
* Корректная работа API-методов `*.search` (спасибо [leonko](https://github.com/leonko))
|
4
|
+
* Поддержка IO-объектов в методе `upload` (спасибо [undr](https://github.com/undr))
|
5
|
+
|
1
6
|
## 1.2 (14.07.2013)
|
2
7
|
|
3
8
|
* Повтор запроса при определенных ошибках
|
data/Guardfile
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
guard 'rspec' do
|
1
|
+
guard 'rspec', all_on_start: true, all_after_pass: true do
|
2
2
|
watch(%r{^spec/.+_spec\.rb$})
|
3
3
|
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
4
4
|
watch('spec/spec_helper.rb') { 'spec' }
|
5
|
+
|
6
|
+
watch('spec/support/mechanized_authorization.rb') { 'spec/integration_spec.rb' }
|
5
7
|
end
|
6
8
|
|
7
9
|
notification :terminal_notifier, activate: 'com.googlecode.iTerm2'
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
|
7
7
|
``` ruby
|
8
8
|
# Gemfile
|
9
|
-
gem 'vkontakte_api', '~> 1.
|
9
|
+
gem 'vkontakte_api', '~> 1.3'
|
10
10
|
```
|
11
11
|
|
12
12
|
или просто
|
@@ -47,7 +47,7 @@ users.first.last_name # => "Дуров"
|
|
47
47
|
# то блок будет выполнен для каждого элемента,
|
48
48
|
# и метод вернет обработанный массив
|
49
49
|
fields = [:first_name, :last_name, :screen_name]
|
50
|
-
@vk.friends.get(fields: fields) do |friend|
|
50
|
+
@vk.friends.get(uid: 2, fields: fields) do |friend|
|
51
51
|
"#{friend.first_name} '#{friend.screen_name}' #{friend.last_name}"
|
52
52
|
end
|
53
53
|
# => ["Павел 'durov' Дуров"]
|
@@ -64,6 +64,13 @@ url = 'http://cs303110.vkontakte.ru/upload.php?act=do_add'
|
|
64
64
|
VkontakteApi.upload(url: url, photo: ['/path/to/file.jpg', 'image/jpeg'])
|
65
65
|
```
|
66
66
|
|
67
|
+
Если загружаемый файл доступен как открытый IO-объект, его можно передать альтернативным синтаксисом - IO-объект, MIME-тип и путь к файлу:
|
68
|
+
|
69
|
+
``` ruby
|
70
|
+
url = 'http://cs303110.vkontakte.ru/upload.php?act=do_add'
|
71
|
+
VkontakteApi.upload(url: url, photo: [file_io, 'image/jpeg', '/path/to/file.jpg'])
|
72
|
+
```
|
73
|
+
|
67
74
|
Метод вернет ответ сервера ВКонтакте, преобразованный в `Hashie::Mash`; его можно использовать при вызове метода API на последнем этапе процесса загрузки.
|
68
75
|
|
69
76
|
### Авторизация
|
@@ -262,7 +269,6 @@ $ rails generate vkontakte_api:install
|
|
262
269
|
|
263
270
|
## Roadmap
|
264
271
|
|
265
|
-
* настраиваемый retry запроса при стандартных исключениях
|
266
272
|
* CLI-интерфейс с автоматической авторизацией
|
267
273
|
|
268
274
|
## Участие в разработке
|
data/lib/vkontakte_api/client.rb
CHANGED
@@ -69,6 +69,16 @@ module VkontakteApi
|
|
69
69
|
end.keys
|
70
70
|
end
|
71
71
|
|
72
|
+
# If the called method is a namespace, it creates and returns a new `VkontakteApi::Namespace` instance.
|
73
|
+
# Otherwise it creates a `VkontakteApi::Method` instance and calls it passing the arguments and a block.
|
74
|
+
def method_missing(*args, &block)
|
75
|
+
if Namespace.exists?(args.first)
|
76
|
+
create_namespace(args.first)
|
77
|
+
else
|
78
|
+
call_method(args, &block)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
72
82
|
private
|
73
83
|
def settings
|
74
84
|
@settings ||= self.get_user_settings
|
@@ -1,7 +1,36 @@
|
|
1
1
|
module VkontakteApi
|
2
|
-
# An API method namespace (such as `users` or `friends`).
|
2
|
+
# An API method namespace (such as `users` or `friends`).
|
3
|
+
#
|
4
|
+
# It includes `Resolvable` and `Resolver` and calls API methods via `Resolver#call_method`.
|
5
|
+
# It also holds the list of all known namespaces.
|
3
6
|
class Namespace
|
4
7
|
include Resolvable
|
5
8
|
include Resolver
|
9
|
+
|
10
|
+
# Creates and calls the `VkontakteApi::Method` using `VkontakteApi::Resolver#call_method`.
|
11
|
+
def method_missing(*args, &block)
|
12
|
+
call_method(args, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# An array of all method namespaces.
|
17
|
+
#
|
18
|
+
# Lazily loads the list from `namespaces.yml` and caches it.
|
19
|
+
# @return [Array] An array of strings
|
20
|
+
def names
|
21
|
+
if @names.nil?
|
22
|
+
filename = File.expand_path('../namespaces.yml', __FILE__)
|
23
|
+
@names = YAML.load_file(filename)
|
24
|
+
end
|
25
|
+
|
26
|
+
@names
|
27
|
+
end
|
28
|
+
|
29
|
+
# Does a given namespace exist?
|
30
|
+
# @param [String, Symbol] name
|
31
|
+
def exists?(name)
|
32
|
+
names.include?(name.to_s)
|
33
|
+
end
|
34
|
+
end
|
6
35
|
end
|
7
36
|
end
|
@@ -7,7 +7,7 @@ module VkontakteApi
|
|
7
7
|
# @param [String] name The name of this resolvable.
|
8
8
|
# @option options [Hashie::Mash] :resolver A mash holding information about the previous resolver.
|
9
9
|
def initialize(name, options = {})
|
10
|
-
@name = name
|
10
|
+
@name = name.to_s
|
11
11
|
@previous_resolver = options.delete(:resolver)
|
12
12
|
end
|
13
13
|
|
@@ -1,41 +1,26 @@
|
|
1
1
|
module VkontakteApi
|
2
2
|
# A mixin for classes that will resolve other classes' objects via `#method_missing`.
|
3
3
|
module Resolver
|
4
|
-
# Main methods dispatch.
|
5
|
-
#
|
6
|
-
# If the called method is a namespace, it creates and returns a new `VkontakteApi::Namespace` instance.
|
7
|
-
# Otherwise it creates a `VkontakteApi::Method` instance and invokes it's `#call` method passing it the arguments and a block.
|
8
|
-
def method_missing(method_name, *args, &block)
|
9
|
-
method_name = method_name.to_s
|
10
|
-
|
11
|
-
if Resolver.namespaces.include?(method_name)
|
12
|
-
# called from Client
|
13
|
-
Namespace.new(method_name, resolver: resolver)
|
14
|
-
else
|
15
|
-
# called from Namespace or one-level method
|
16
|
-
Method.new(method_name, resolver: resolver).call(args.first || {}, &block)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
4
|
# A `Hashie::Mash` structure holding the name and token of current instance.
|
21
5
|
# @return [Hashie::Mash]
|
22
6
|
def resolver
|
23
7
|
@resolver ||= Hashie::Mash.new(name: @name, token: token)
|
24
8
|
end
|
25
9
|
|
10
|
+
private
|
11
|
+
def create_namespace(name)
|
12
|
+
Namespace.new(name, resolver: resolver)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_method(name)
|
16
|
+
Method.new(name, resolver: resolver)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call_method(args, &block)
|
20
|
+
create_method(args.shift).call(args.first || {}, &block)
|
21
|
+
end
|
22
|
+
|
26
23
|
class << self
|
27
|
-
# An array of method namespaces.
|
28
|
-
# Lazily loads the list from `namespaces.yml` and caches it.
|
29
|
-
# @return [Array]
|
30
|
-
def namespaces
|
31
|
-
if @namespaces.nil?
|
32
|
-
filename = File.expand_path('../namespaces.yml', __FILE__)
|
33
|
-
@namespaces = YAML.load_file(filename)
|
34
|
-
end
|
35
|
-
|
36
|
-
@namespaces
|
37
|
-
end
|
38
|
-
|
39
24
|
# When this module is included, it undefines the `:send` instance method in the `base_class`
|
40
25
|
# so it can be resolved via `method_missing`.
|
41
26
|
def included(base_class)
|
@@ -12,15 +12,15 @@ module VkontakteApi
|
|
12
12
|
# VkontakteApi.upload(
|
13
13
|
# url: 'http://example.com/upload',
|
14
14
|
# file1: ['/path/to/file1.jpg', 'image/jpeg'],
|
15
|
-
# file2: ['/path/to/file2.png'
|
15
|
+
# file2: [io_object, 'image/png', '/path/to/file2.png']
|
16
16
|
# )
|
17
17
|
def upload(params = {})
|
18
18
|
url = params.delete(:url)
|
19
19
|
raise ArgumentError, 'You should pass :url parameter' unless url
|
20
20
|
|
21
21
|
files = {}
|
22
|
-
params.each do |param_name, (
|
23
|
-
files[param_name] = Faraday::UploadIO.new(
|
22
|
+
params.each do |param_name, (file_path_or_io, file_type, file_path)|
|
23
|
+
files[param_name] = Faraday::UploadIO.new(file_path_or_io, file_type, file_path)
|
24
24
|
end
|
25
25
|
|
26
26
|
API.connection.post(url, files).body
|
data/spec/integration_spec.rb
CHANGED
@@ -13,70 +13,67 @@ describe "Integration" do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
describe "unauthorized requests" do
|
16
|
-
|
17
|
-
@vk = VK::Client.new
|
18
|
-
end
|
16
|
+
let(:vk) { VK::Client.new }
|
19
17
|
|
20
18
|
it "get users" do
|
21
|
-
user =
|
22
|
-
user.uid.
|
23
|
-
user.last_name.
|
24
|
-
user.first_name.
|
19
|
+
user = vk.users.get(uid: 1).first
|
20
|
+
expect(user.uid).to eq(1)
|
21
|
+
expect(user.last_name).not_to be_empty
|
22
|
+
expect(user.first_name).not_to be_empty
|
23
|
+
end
|
24
|
+
|
25
|
+
it "search newsfeed" do
|
26
|
+
news = vk.newsfeed.search(q: 'vk', count: 1)
|
27
|
+
expect(news).to be_a(Enumerable)
|
25
28
|
end
|
26
29
|
end
|
27
30
|
|
28
31
|
if MechanizedAuthorization.on?
|
29
32
|
describe "authorized requests" do
|
30
|
-
|
31
|
-
@vk = MechanizedAuthorization.client
|
32
|
-
end
|
33
|
+
let(:vk) { MechanizedAuthorization.client }
|
33
34
|
|
34
35
|
it "get groups" do
|
35
|
-
|
36
|
-
groups.
|
36
|
+
pending 'MechanizedAuthorization not working'
|
37
|
+
expect(vk.groups.get).to include(1)
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
40
41
|
describe "requests with camelCase and predicate methods" do
|
41
|
-
|
42
|
-
@vk = MechanizedAuthorization.client
|
43
|
-
end
|
42
|
+
let(:vk) { MechanizedAuthorization.client }
|
44
43
|
|
45
44
|
it "convert method names to vk.com format" do
|
46
|
-
|
45
|
+
pending 'MechanizedAuthorization not working'
|
46
|
+
expect(vk.is_app_user?).to be_true
|
47
47
|
end
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
51
|
describe "requests with array arguments" do
|
52
|
-
|
53
|
-
@vk = VK::Client.new
|
54
|
-
end
|
52
|
+
let(:vk) { VK::Client.new }
|
55
53
|
|
56
54
|
it "join arrays with a comma" do
|
57
|
-
users =
|
58
|
-
users.first.screen_name.
|
55
|
+
users = vk.users.get(uids: [1, 2, 3], fields: %w[first_name last_name screen_name])
|
56
|
+
expect(users.first.screen_name).not_to be_empty
|
59
57
|
end
|
60
58
|
end
|
61
59
|
|
62
60
|
describe "requests with blocks" do
|
63
|
-
|
64
|
-
@vk = VK::Client.new
|
65
|
-
end
|
61
|
+
let(:vk) { VK::Client.new }
|
66
62
|
|
67
63
|
it "map the result with a block" do
|
68
|
-
users =
|
64
|
+
users = vk.users.get(uid: 1) do |user|
|
69
65
|
"#{user.last_name} #{user.first_name}"
|
70
66
|
end
|
71
67
|
|
72
|
-
users.first.
|
68
|
+
expect(users.first).not_to be_empty
|
73
69
|
end
|
74
70
|
end
|
75
71
|
|
76
72
|
describe "authorization" do
|
77
73
|
context "with a scope" do
|
78
74
|
it "returns a correct url" do
|
79
|
-
VK.authorization_url(scope: %w[friends groups])
|
75
|
+
url = VK.authorization_url(scope: %w[friends groups])
|
76
|
+
expect(url).to include('scope=friends%2Cgroups')
|
80
77
|
end
|
81
78
|
end
|
82
79
|
end
|
@@ -23,13 +23,13 @@ describe VkontakteApi::API do
|
|
23
23
|
|
24
24
|
context "called with a token parameter" do
|
25
25
|
it "sends it to .connection" do
|
26
|
-
subject.
|
26
|
+
expect(subject).to receive(:connection).with(url: VkontakteApi::API::URL_PREFIX, token: 'token')
|
27
27
|
subject.call('apiMethod', { some: :params }, 'token')
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
31
|
it "returns the response body" do
|
32
|
-
subject.call('apiMethod').
|
32
|
+
expect(subject.call('apiMethod')).to eq(@result)
|
33
33
|
end
|
34
34
|
|
35
35
|
it "uses an HTTP verb from VkontakteApi.http_verb" do
|
@@ -37,7 +37,7 @@ describe VkontakteApi::API do
|
|
37
37
|
VkontakteApi.http_verb = http_verb
|
38
38
|
|
39
39
|
response = double("Response", body: double)
|
40
|
-
@connection.
|
40
|
+
expect(@connection).to receive(:send).with(http_verb, 'apiMethod', {}).and_return(response)
|
41
41
|
subject.call('apiMethod')
|
42
42
|
end
|
43
43
|
|
@@ -51,27 +51,27 @@ describe VkontakteApi::API do
|
|
51
51
|
faraday_options = double("Faraday options")
|
52
52
|
VkontakteApi.stub(:faraday_options).and_return(faraday_options)
|
53
53
|
url = double("URL")
|
54
|
-
|
55
|
-
|
54
|
+
|
55
|
+
expect(Faraday).to receive(:new).with(url, faraday_options)
|
56
|
+
subject.connection(url: url)
|
56
57
|
end
|
57
58
|
|
58
59
|
context "without a token" do
|
59
60
|
it "creates a connection without an oauth2 middleware" do
|
60
|
-
|
61
|
-
|
61
|
+
handler_names = subject.connection.builder.handlers.map(&:name)
|
62
|
+
expect(handler_names).not_to include('FaradayMiddleware::OAuth2')
|
62
63
|
end
|
63
64
|
end
|
64
65
|
|
65
66
|
context "with a token" do
|
66
|
-
|
67
|
-
@token = double("Token")
|
68
|
-
end
|
67
|
+
let(:token) { double("Token") }
|
69
68
|
|
70
69
|
it "creates a connection with an oauth2 middleware" do
|
71
|
-
connection = subject.connection(token:
|
70
|
+
connection = subject.connection(token: token)
|
72
71
|
handler = connection.builder.handlers.first
|
73
|
-
|
74
|
-
handler.
|
72
|
+
|
73
|
+
expect(handler.name).to eq('FaradayMiddleware::OAuth2')
|
74
|
+
expect(handler.instance_variable_get(:@args)).to eq([token])
|
75
75
|
end
|
76
76
|
end
|
77
77
|
end
|
@@ -18,35 +18,38 @@ describe VkontakteApi::Authorization do
|
|
18
18
|
|
19
19
|
@client = double("OAuth2::Client instance", auth_code: @auth_code, implicit: @implicit, client_credentials: @client_credentials)
|
20
20
|
OAuth2::Client.stub(:new).and_return(@client)
|
21
|
-
|
22
|
-
@auth = Object.new
|
23
|
-
@auth.extend VkontakteApi::Authorization
|
24
21
|
end
|
25
22
|
|
23
|
+
let(:auth) do
|
24
|
+
Object.new.tap do |object|
|
25
|
+
object.extend VkontakteApi::Authorization
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
26
29
|
describe "#authorization_url" do
|
27
30
|
before(:each) do
|
28
|
-
|
31
|
+
auth.stub(:client).and_return(@client)
|
29
32
|
end
|
30
33
|
|
31
34
|
context "with a site type" do
|
32
35
|
it "returns a valid authorization url" do
|
33
|
-
@auth_code.
|
34
|
-
|
36
|
+
expect(@auth_code).to receive(:authorize_url).with(redirect_uri: @redirect_uri)
|
37
|
+
expect(auth.authorization_url(type: :site)).to eq(@url)
|
35
38
|
end
|
36
39
|
end
|
37
40
|
|
38
41
|
context "with a client type" do
|
39
42
|
it "returns a valid authorization url" do
|
40
|
-
@implicit.
|
41
|
-
|
43
|
+
expect(@implicit).to receive(:authorize_url).with(redirect_uri: @redirect_uri)
|
44
|
+
expect(auth.authorization_url(type: :client)).to eq(@url)
|
42
45
|
end
|
43
46
|
end
|
44
47
|
|
45
48
|
context "given a redirect_uri" do
|
46
49
|
it "prefers the given uri over VkontakteApi.redirect_uri" do
|
47
50
|
redirect_uri = 'http://example.com/oauth/callback'
|
48
|
-
@auth_code.
|
49
|
-
|
51
|
+
expect(@auth_code).to receive(:authorize_url).with(redirect_uri: redirect_uri)
|
52
|
+
auth.authorization_url(redirect_uri: redirect_uri)
|
50
53
|
end
|
51
54
|
end
|
52
55
|
|
@@ -55,9 +58,9 @@ describe VkontakteApi::Authorization do
|
|
55
58
|
scope = double("Scope")
|
56
59
|
flat_scope = double("Flat scope")
|
57
60
|
|
58
|
-
VkontakteApi::Utils.
|
59
|
-
@auth_code.
|
60
|
-
|
61
|
+
expect(VkontakteApi::Utils).to receive(:flatten_argument).with(scope).and_return(flat_scope)
|
62
|
+
expect(@auth_code).to receive(:authorize_url).with(redirect_uri: @redirect_uri, scope: flat_scope)
|
63
|
+
auth.authorization_url(scope: scope)
|
61
64
|
end
|
62
65
|
end
|
63
66
|
end
|
@@ -70,42 +73,42 @@ describe VkontakteApi::Authorization do
|
|
70
73
|
end
|
71
74
|
|
72
75
|
it "gets the token" do
|
73
|
-
@auth_code.
|
74
|
-
|
76
|
+
expect(@auth_code).to receive(:get_token).with(@code, redirect_uri: @redirect_uri)
|
77
|
+
auth.authorize(type: :site, code: @code)
|
75
78
|
end
|
76
79
|
end
|
77
80
|
|
78
81
|
context "with an app_server type" do
|
79
82
|
it "gets the token" do
|
80
|
-
@client_credentials.
|
81
|
-
|
83
|
+
expect(@client_credentials).to receive(:get_token).with({ redirect_uri: @redirect_uri }, subject::OPTIONS[:client_credentials])
|
84
|
+
auth.authorize(type: :app_server)
|
82
85
|
end
|
83
86
|
end
|
84
87
|
|
85
88
|
context "with an unknown type" do
|
86
89
|
it "raises an ArgumentError" do
|
87
90
|
expect {
|
88
|
-
|
91
|
+
auth.authorize(type: :unknown)
|
89
92
|
}.to raise_error(ArgumentError)
|
90
93
|
end
|
91
94
|
end
|
92
95
|
|
93
96
|
it "builds a VkontakteApi::Client instance with the received token" do
|
94
97
|
client = double("VkontakteApi::Client instance")
|
95
|
-
VkontakteApi::Client.
|
96
|
-
|
98
|
+
expect(VkontakteApi::Client).to receive(:new).with(@token).and_return(client)
|
99
|
+
expect(auth.authorize).to eq(client)
|
97
100
|
end
|
98
101
|
end
|
99
102
|
|
100
103
|
describe "#client" do
|
101
104
|
it "creates and returns an OAuth2::Client instance" do
|
102
|
-
OAuth2::Client.
|
103
|
-
|
105
|
+
expect(OAuth2::Client).to receive(:new).with(@app_id, @app_secret, subject::OPTIONS[:client])
|
106
|
+
expect(auth.send(:client)).to eq(@client)
|
104
107
|
end
|
105
108
|
|
106
109
|
it "caches the result" do
|
107
|
-
OAuth2::Client.
|
108
|
-
5.times {
|
110
|
+
expect(OAuth2::Client).to receive(:new).once
|
111
|
+
5.times { auth.send(:client) }
|
109
112
|
end
|
110
113
|
end
|
111
114
|
end
|