uservoice-ruby 0.0.5 → 0.0.6
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/uservoice/client.rb +132 -0
- data/lib/uservoice/collection.rb +63 -0
- data/lib/uservoice/user_voice.rb +2 -127
- data/lib/uservoice/version.rb +1 -1
- data/spec/lib/user_voice_spec.rb +0 -171
- data/spec/lib/uservoice/client_spec.rb +202 -0
- data/spec/lib/uservoice/collection_spec.rb +126 -0
- metadata +37 -11
@@ -0,0 +1,132 @@
|
|
1
|
+
module UserVoice
|
2
|
+
class Client
|
3
|
+
|
4
|
+
def initialize(*args)
|
5
|
+
case args.size
|
6
|
+
when 3,4
|
7
|
+
init_subdomain_and_api_keys(*args)
|
8
|
+
when 1,2
|
9
|
+
init_consumer_and_access_token(*args)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def init_subdomain_and_api_keys(subdomain_name, api_key, api_secret, attrs={})
|
14
|
+
consumer = OAuth::Consumer.new(api_key, api_secret, {
|
15
|
+
:site => "#{attrs[:protocol] || 'https'}://#{subdomain_name}.#{attrs[:uservoice_domain] || 'uservoice.com'}"
|
16
|
+
})
|
17
|
+
init_consumer_and_access_token(consumer, attrs)
|
18
|
+
end
|
19
|
+
|
20
|
+
def init_consumer_and_access_token(consumer, attrs={})
|
21
|
+
@consumer = consumer
|
22
|
+
@token = OAuth::AccessToken.new(@consumer, attrs[:oauth_token] || '', attrs[:oauth_token_secret] || '')
|
23
|
+
@response_format = attrs[:response_format] || :hash
|
24
|
+
@callback = attrs[:callback]
|
25
|
+
end
|
26
|
+
|
27
|
+
def authorize_url
|
28
|
+
request_token.authorize_url
|
29
|
+
end
|
30
|
+
|
31
|
+
def login_with_verifier(oauth_verifier)
|
32
|
+
raise Unauthorized.new('Call request token first') if @request_token.nil?
|
33
|
+
token = @request_token.get_access_token(:oauth_verifier => oauth_verifier)
|
34
|
+
Client.new(@consumer, :oauth_token => token.token, :oauth_token_secret => token.secret)
|
35
|
+
end
|
36
|
+
|
37
|
+
def login_with_access_token(oauth_token, oauth_token_secret, &block)
|
38
|
+
token = Client.new(@consumer, :oauth_token => oauth_token, :oauth_token_secret => oauth_token_secret)
|
39
|
+
if block_given?
|
40
|
+
yield token
|
41
|
+
else
|
42
|
+
return token
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def token
|
47
|
+
@token.token
|
48
|
+
end
|
49
|
+
|
50
|
+
def secret
|
51
|
+
@token.secret
|
52
|
+
end
|
53
|
+
|
54
|
+
def request_token
|
55
|
+
@request_token = @consumer.get_request_token(:oauth_callback => @callback)
|
56
|
+
end
|
57
|
+
|
58
|
+
def login_as_owner(&block)
|
59
|
+
token = post('/api/v1/users/login_as_owner.json', {
|
60
|
+
'request_token' => request_token.token
|
61
|
+
})['token']
|
62
|
+
if token
|
63
|
+
login_with_access_token(token['oauth_token'], token['oauth_token_secret'], &block)
|
64
|
+
else
|
65
|
+
raise Unauthorized.new("Could not get Access Token")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def login_as(email, &block)
|
70
|
+
unless email.to_s.match(EMAIL_FORMAT)
|
71
|
+
raise Unauthorized.new("'#{email}' is not a valid email address")
|
72
|
+
end
|
73
|
+
token = post('/api/v1/users/login_as.json', {
|
74
|
+
:user => { :email => email },
|
75
|
+
:request_token => request_token.token
|
76
|
+
})['token']
|
77
|
+
|
78
|
+
if token
|
79
|
+
login_with_access_token(token['oauth_token'], token['oauth_token_secret'], &block)
|
80
|
+
else
|
81
|
+
raise Unauthorized.new("Could not get Access Token")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def request(method, uri, request_body={}, headers={})
|
86
|
+
headers = DEFAULT_HEADERS.merge(headers)
|
87
|
+
|
88
|
+
if headers['Content-Type'] == 'application/json' && request_body.is_a?(Hash)
|
89
|
+
request_body = request_body.to_json
|
90
|
+
end
|
91
|
+
|
92
|
+
response = case method.to_sym
|
93
|
+
when :post, :put
|
94
|
+
@token.request(method, uri, request_body, headers)
|
95
|
+
when :head, :delete, :get
|
96
|
+
@token.request(method, uri, headers)
|
97
|
+
else
|
98
|
+
raise RuntimeError.new("Invalid HTTP method #{method}")
|
99
|
+
end
|
100
|
+
|
101
|
+
return case @response_format.to_s
|
102
|
+
when 'raw'
|
103
|
+
response
|
104
|
+
else
|
105
|
+
attrs = JSON.parse(response.body)
|
106
|
+
if attrs && attrs['errors']
|
107
|
+
case attrs['errors']['type']
|
108
|
+
when 'unauthorized'
|
109
|
+
raise Unauthorized.new(attrs)
|
110
|
+
when 'record_not_found'
|
111
|
+
raise NotFound.new(attrs)
|
112
|
+
when 'application_error'
|
113
|
+
raise ApplicationError.new(attrs)
|
114
|
+
else
|
115
|
+
raise APIError.new(attrs)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
attrs
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
%w(get post delete put).each do |method|
|
123
|
+
define_method(method) do |*args|
|
124
|
+
request(method, *args)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def get_collection(uri, opts={})
|
129
|
+
UserVoice::Collection.new(self, uri, opts)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module UserVoice
|
2
|
+
class Collection
|
3
|
+
def initialize(client, query, opts={})
|
4
|
+
@client = client
|
5
|
+
@query = query
|
6
|
+
@limit = opts[:limit] || 2**60
|
7
|
+
@per_page = [@limit, 500].min
|
8
|
+
@pages = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def first
|
12
|
+
load_record(0)
|
13
|
+
end
|
14
|
+
|
15
|
+
def last
|
16
|
+
load_record(size() - 1)
|
17
|
+
end
|
18
|
+
|
19
|
+
def size
|
20
|
+
if @response_data.nil?
|
21
|
+
load_record(0)
|
22
|
+
end
|
23
|
+
@response_data['total_records']
|
24
|
+
end
|
25
|
+
|
26
|
+
def map
|
27
|
+
index = 0
|
28
|
+
records = []
|
29
|
+
while record = load_record(index)
|
30
|
+
records.push(yield record)
|
31
|
+
index += 1
|
32
|
+
end
|
33
|
+
return records
|
34
|
+
end
|
35
|
+
alias collect map
|
36
|
+
|
37
|
+
def each
|
38
|
+
map do |value|
|
39
|
+
yield value
|
40
|
+
value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def load_record(i)
|
47
|
+
load_page((i/500.0).floor + 1)[i%500]
|
48
|
+
end
|
49
|
+
|
50
|
+
def load_page(i)
|
51
|
+
if @pages[i].nil?
|
52
|
+
result = @client.get("#{@query}#{@query.include?('?') ? '&' : '?'}per_page=#{@per_page}&page=#{i}")
|
53
|
+
|
54
|
+
if @response_data = result.delete('response_data')
|
55
|
+
@pages[i] = result.shift.last if result.first
|
56
|
+
else
|
57
|
+
raise UserVoice::NotFound.new('The resource you requested is not a collection')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
return @pages[i]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/uservoice/user_voice.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require "uservoice/version"
|
2
|
+
require 'uservoice/collection'
|
3
|
+
require 'uservoice/client'
|
2
4
|
require 'rubygems'
|
3
5
|
require 'ezcrypto'
|
4
6
|
require 'json'
|
@@ -31,131 +33,4 @@ module UserVoice
|
|
31
33
|
|
32
34
|
return CGI.escape(encoded)
|
33
35
|
end
|
34
|
-
|
35
|
-
class Client
|
36
|
-
|
37
|
-
def initialize(*args)
|
38
|
-
case args.size
|
39
|
-
when 3,4
|
40
|
-
init_subdomain_and_api_keys(*args)
|
41
|
-
when 1,2
|
42
|
-
init_consumer_and_access_token(*args)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def init_subdomain_and_api_keys(subdomain_name, api_key, api_secret, attrs={})
|
47
|
-
consumer = OAuth::Consumer.new(api_key, api_secret, {
|
48
|
-
:site => "#{attrs[:protocol] || 'https'}://#{subdomain_name}.#{attrs[:uservoice_domain] || 'uservoice.com'}"
|
49
|
-
})
|
50
|
-
init_consumer_and_access_token(consumer, attrs)
|
51
|
-
end
|
52
|
-
|
53
|
-
def init_consumer_and_access_token(consumer, attrs={})
|
54
|
-
@consumer = consumer
|
55
|
-
@token = OAuth::AccessToken.new(@consumer, attrs[:oauth_token] || '', attrs[:oauth_token_secret] || '')
|
56
|
-
@response_format = attrs[:response_format] || :hash
|
57
|
-
@callback = attrs[:callback]
|
58
|
-
end
|
59
|
-
|
60
|
-
def authorize_url
|
61
|
-
request_token.authorize_url
|
62
|
-
end
|
63
|
-
|
64
|
-
def login_with_verifier(oauth_verifier)
|
65
|
-
raise Unauthorized.new('Call request token first') if @request_token.nil?
|
66
|
-
token = @request_token.get_access_token(:oauth_verifier => oauth_verifier)
|
67
|
-
Client.new(@consumer, :oauth_token => token.token, :oauth_token_secret => token.secret)
|
68
|
-
end
|
69
|
-
|
70
|
-
def login_with_access_token(oauth_token, oauth_token_secret, &block)
|
71
|
-
token = Client.new(@consumer, :oauth_token => oauth_token, :oauth_token_secret => oauth_token_secret)
|
72
|
-
if block_given?
|
73
|
-
yield token
|
74
|
-
else
|
75
|
-
return token
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def token
|
80
|
-
@token.token
|
81
|
-
end
|
82
|
-
|
83
|
-
def secret
|
84
|
-
@token.secret
|
85
|
-
end
|
86
|
-
|
87
|
-
def request_token
|
88
|
-
@request_token = @consumer.get_request_token(:oauth_callback => @callback)
|
89
|
-
end
|
90
|
-
|
91
|
-
def login_as_owner(&block)
|
92
|
-
token = post('/api/v1/users/login_as_owner.json', {
|
93
|
-
'request_token' => request_token.token
|
94
|
-
})['token']
|
95
|
-
if token
|
96
|
-
login_with_access_token(token['oauth_token'], token['oauth_token_secret'], &block)
|
97
|
-
else
|
98
|
-
raise Unauthorized.new("Could not get Access Token")
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def login_as(email, &block)
|
103
|
-
unless email.to_s.match(EMAIL_FORMAT)
|
104
|
-
raise Unauthorized.new("'#{email}' is not a valid email address")
|
105
|
-
end
|
106
|
-
token = post('/api/v1/users/login_as.json', {
|
107
|
-
:user => { :email => email },
|
108
|
-
:request_token => request_token.token
|
109
|
-
})['token']
|
110
|
-
|
111
|
-
if token
|
112
|
-
login_with_access_token(token['oauth_token'], token['oauth_token_secret'], &block)
|
113
|
-
else
|
114
|
-
raise Unauthorized.new("Could not get Access Token")
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
def request(method, uri, request_body={}, headers={})
|
119
|
-
headers = DEFAULT_HEADERS.merge(headers)
|
120
|
-
|
121
|
-
if headers['Content-Type'] == 'application/json' && request_body.is_a?(Hash)
|
122
|
-
request_body = request_body.to_json
|
123
|
-
end
|
124
|
-
|
125
|
-
response = case method.to_sym
|
126
|
-
when :post, :put
|
127
|
-
@token.request(method, uri, request_body, headers)
|
128
|
-
when :head, :delete, :get
|
129
|
-
@token.request(method, uri, headers)
|
130
|
-
else
|
131
|
-
raise RuntimeError.new("Invalid HTTP method #{method}")
|
132
|
-
end
|
133
|
-
|
134
|
-
return case @response_format.to_s
|
135
|
-
when 'raw'
|
136
|
-
response
|
137
|
-
else
|
138
|
-
attrs = JSON.parse(response.body)
|
139
|
-
if attrs && attrs['errors']
|
140
|
-
case attrs['errors']['type']
|
141
|
-
when 'unauthorized'
|
142
|
-
raise Unauthorized.new(attrs)
|
143
|
-
when 'record_not_found'
|
144
|
-
raise NotFound.new(attrs)
|
145
|
-
when 'application_error'
|
146
|
-
raise ApplicationError.new(attrs)
|
147
|
-
else
|
148
|
-
raise APIError.new(attrs)
|
149
|
-
end
|
150
|
-
end
|
151
|
-
attrs
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
%w(get post delete put).each do |method|
|
156
|
-
define_method(method) do |*args|
|
157
|
-
request(method, *args)
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
36
|
end
|
data/lib/uservoice/version.rb
CHANGED
data/spec/lib/user_voice_spec.rb
CHANGED
@@ -12,175 +12,4 @@ describe UserVoice do
|
|
12
12
|
key = EzCrypto::Key.with_password(config['subdomain_name'], config['sso_key'])
|
13
13
|
key.decrypt(encrypted_raw_data).should match('mailaddress@example.com')
|
14
14
|
end
|
15
|
-
|
16
|
-
describe UserVoice::Client do
|
17
|
-
subject { UserVoice::Client.new(config['subdomain_name'],
|
18
|
-
config['api_key'],
|
19
|
-
config['api_secret'],
|
20
|
-
:uservoice_domain => config['uservoice_domain'],
|
21
|
-
:protocol => config['protocol']) }
|
22
|
-
|
23
|
-
it "should get user names from the API" do
|
24
|
-
users = subject.get("/api/v1/users.json?per_page=3")
|
25
|
-
user_names = users['users'].map { |user| user['name'] }
|
26
|
-
user_names.all?.should == true
|
27
|
-
user_names.size.should == 3
|
28
|
-
end
|
29
|
-
|
30
|
-
it "should not get current user without logged in user" do
|
31
|
-
lambda do
|
32
|
-
user = subject.get("/api/v1/users/current.json")
|
33
|
-
end.should raise_error(UserVoice::Unauthorized)
|
34
|
-
end
|
35
|
-
|
36
|
-
it "should be able to get access token as owner" do
|
37
|
-
subject.login_as_owner do |owner|
|
38
|
-
owner.get("/api/v1/users/current.json")['user']['roles']['owner'].should == true
|
39
|
-
|
40
|
-
owner.login_as('regular@example.com') do |regular|
|
41
|
-
owner.get("/api/v1/users/current.json")['user']['roles']['owner'].should == true
|
42
|
-
@user = regular.get("/api/v1/users/current.json")['user']
|
43
|
-
@user['roles']['owner'].should == false
|
44
|
-
end
|
45
|
-
|
46
|
-
owner.get("/api/v1/users/current.json")['user']['roles']['owner'].should == true
|
47
|
-
end
|
48
|
-
# ensure blocks got run
|
49
|
-
@user['email'].should == 'regular@example.com'
|
50
|
-
end
|
51
|
-
|
52
|
-
it "should not be able to create KB article as nobody" do
|
53
|
-
lambda do
|
54
|
-
result = subject.post("/api/v1/articles.json", :article => {
|
55
|
-
:title => 'good morning'
|
56
|
-
})
|
57
|
-
end.should raise_error(UserVoice::Unauthorized)
|
58
|
-
end
|
59
|
-
|
60
|
-
it "should be able to create and delete a forum as the owner" do
|
61
|
-
owner = subject.login_as_owner
|
62
|
-
forum = owner.post("/api/v1/forums.json", :forum => {
|
63
|
-
:name => 'Test forum from RSpec',
|
64
|
-
'private' => true,
|
65
|
-
'allow_by_email_domain' => true,
|
66
|
-
'allowed_email_domains' => [{'domain' => 'raimo.rspec.example.com'}]
|
67
|
-
})['forum']
|
68
|
-
|
69
|
-
forum['id'].should be_a(Integer)
|
70
|
-
|
71
|
-
deleted_forum = owner.delete("/api/v1/forums/#{forum['id']}.json")['forum']
|
72
|
-
deleted_forum['id'].should == forum['id']
|
73
|
-
end
|
74
|
-
|
75
|
-
it "should get current user with 2-legged call" do
|
76
|
-
user = subject.login_as('mailaddress@example.com') do |token|
|
77
|
-
token.get("/api/v1/users/current.json")['user']
|
78
|
-
end
|
79
|
-
|
80
|
-
user['email'].should == 'mailaddress@example.com'
|
81
|
-
end
|
82
|
-
|
83
|
-
it "should get current user with copied access token" do
|
84
|
-
original_token = subject.login_as('mailaddress@example.com')
|
85
|
-
|
86
|
-
client = UserVoice::Client.new(config['subdomain_name'],
|
87
|
-
config['api_key'],
|
88
|
-
config['api_secret'],
|
89
|
-
:uservoice_domain => config['uservoice_domain'],
|
90
|
-
:protocol => config['protocol'],
|
91
|
-
:oauth_token => original_token.token,
|
92
|
-
:oauth_token_secret => original_token.secret)
|
93
|
-
# Also this works but creates an extra object:
|
94
|
-
# client = client.login_with_access_token(original_token.token, original_token.secret)
|
95
|
-
|
96
|
-
user = client.get("/api/v1/users/current.json")['user']
|
97
|
-
|
98
|
-
user['email'].should == 'mailaddress@example.com'
|
99
|
-
end
|
100
|
-
|
101
|
-
it "should login as an owner" do
|
102
|
-
me = subject.login_as_owner
|
103
|
-
|
104
|
-
owner = me.get("/api/v1/users/current.json")['user']
|
105
|
-
owner['roles']['owner'].should == true
|
106
|
-
end
|
107
|
-
|
108
|
-
it "should not be able to delete when not deleting on behalf of anyone" do
|
109
|
-
lambda {
|
110
|
-
result = subject.delete("/api/v1/users/#{234}.json")
|
111
|
-
}.should raise_error(UserVoice::Unauthorized, /user required/i)
|
112
|
-
end
|
113
|
-
|
114
|
-
it "should not be able to delete owner" do
|
115
|
-
owner_access_token = subject.login_as_owner
|
116
|
-
|
117
|
-
owner = owner_access_token.get("/api/v1/users/current.json")['user']
|
118
|
-
|
119
|
-
lambda {
|
120
|
-
result = owner_access_token.delete("/api/v1/users/#{owner['id']}.json")
|
121
|
-
}.should raise_error(UserVoice::Unauthorized, /last owner/i)
|
122
|
-
end
|
123
|
-
|
124
|
-
it "should not be able to delete user without login" do
|
125
|
-
regular_user = subject.login_as('somebodythere@example.com').get("/api/v1/users/current.json")['user']
|
126
|
-
|
127
|
-
lambda {
|
128
|
-
subject.delete("/api/v1/users/#{regular_user['id']}.json")
|
129
|
-
}.should raise_error(UserVoice::Unauthorized)
|
130
|
-
end
|
131
|
-
|
132
|
-
it "should be able to identify suggestions" do
|
133
|
-
owner_token = subject.login_as_owner
|
134
|
-
external_scope='sync_to_moon'
|
135
|
-
suggestions = owner_token.get("/api/v1/suggestions.json?filter=with_external_id&external_scope=#{external_scope}&manual_action=#{external_scope}")['suggestions']
|
136
|
-
|
137
|
-
identifications = suggestions.map {|s| { :id => s['id'], :external_id => s['id'].to_i*10 } }
|
138
|
-
|
139
|
-
ids = owner_token.put("/api/v1/suggestions/identify.json",
|
140
|
-
:external_scope => external_scope,
|
141
|
-
:identifications => identifications)['identifications']['ids']
|
142
|
-
ids.should == identifications.map { |s| s[:id] }.sort
|
143
|
-
end
|
144
|
-
|
145
|
-
it "should be able to delete itself" do
|
146
|
-
my_token = subject.login_as('somebodythere@example.com')
|
147
|
-
|
148
|
-
# whoami
|
149
|
-
my_id = my_token.get("/api/v1/users/current.json")['user']['id']
|
150
|
-
|
151
|
-
# Delete myself!
|
152
|
-
my_token.delete("/api/v1/users/#{my_id}.json")['user']['id'].should == my_id
|
153
|
-
|
154
|
-
# I don't exist anymore
|
155
|
-
lambda {
|
156
|
-
my_token.get("/api/v1/users/current.json")
|
157
|
-
}.should raise_error(UserVoice::NotFound)
|
158
|
-
end
|
159
|
-
|
160
|
-
it "should/be able to delete random user and login as him after that" do
|
161
|
-
somebody = subject.login_as('somebodythere@example.com')
|
162
|
-
owner = subject.login_as_owner
|
163
|
-
|
164
|
-
# somebody is still there...
|
165
|
-
regular_user = somebody.get("/api/v1/users/current.json")['user']
|
166
|
-
regular_user['email'].should == 'somebodythere@example.com'
|
167
|
-
|
168
|
-
# delete somebody!
|
169
|
-
owner.delete("/api/v1/users/#{regular_user['id']}.json")['user']['id'].should == regular_user['id']
|
170
|
-
|
171
|
-
# not found anymore!
|
172
|
-
lambda {
|
173
|
-
somebody.get("/api/v1/users/current.json")['errors']['type']
|
174
|
-
}.should raise_error(UserVoice::NotFound)
|
175
|
-
|
176
|
-
# this recreates somebody
|
177
|
-
somebody = subject.login_as('somebodythere@example.com')
|
178
|
-
somebody.get("/api/v1/users/current.json")['user']['id'].should_not == regular_user['id']
|
179
|
-
end
|
180
|
-
|
181
|
-
it "should raise error with invalid email parameter" do
|
182
|
-
expect { subject.login_as('ma') }.to raise_error(UserVoice::Unauthorized)
|
183
|
-
expect { subject.login_as(nil) }.to raise_error(UserVoice::Unauthorized)
|
184
|
-
end
|
185
|
-
end
|
186
15
|
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe UserVoice::Client do
|
3
|
+
subject { UserVoice::Client.new(config['subdomain_name'],
|
4
|
+
config['api_key'],
|
5
|
+
config['api_secret'],
|
6
|
+
:uservoice_domain => config['uservoice_domain'],
|
7
|
+
:protocol => config['protocol']) }
|
8
|
+
let(:external_scope) { 'external_system_name' }
|
9
|
+
|
10
|
+
it "should get user names from the API" do
|
11
|
+
users = subject.get("/api/v1/users.json?per_page=3")
|
12
|
+
user_names = users['users'].map { |user| user['name'] }
|
13
|
+
user_names.all?.should == true
|
14
|
+
user_names.size.should == 3
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should not get current user without logged in user" do
|
18
|
+
lambda do
|
19
|
+
user = subject.get("/api/v1/users/current.json")
|
20
|
+
end.should raise_error(UserVoice::Unauthorized)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should be able to get access token as owner" do
|
24
|
+
subject.login_as_owner do |owner|
|
25
|
+
owner.get("/api/v1/users/current.json")['user']['roles']['owner'].should == true
|
26
|
+
|
27
|
+
owner.login_as('regular@example.com') do |regular|
|
28
|
+
owner.get("/api/v1/users/current.json")['user']['roles']['owner'].should == true
|
29
|
+
@user = regular.get("/api/v1/users/current.json")['user']
|
30
|
+
@user['roles']['owner'].should == false
|
31
|
+
end
|
32
|
+
|
33
|
+
owner.get("/api/v1/users/current.json")['user']['roles']['owner'].should == true
|
34
|
+
end
|
35
|
+
# ensure blocks got run
|
36
|
+
@user['email'].should == 'regular@example.com'
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should not be able to create KB article as nobody" do
|
40
|
+
lambda do
|
41
|
+
result = subject.post("/api/v1/articles.json", :article => {
|
42
|
+
:title => 'good morning'
|
43
|
+
})
|
44
|
+
end.should raise_error(UserVoice::Unauthorized)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should be able to create and delete a forum as the owner" do
|
48
|
+
owner = subject.login_as_owner
|
49
|
+
forum = owner.post("/api/v1/forums.json", :forum => {
|
50
|
+
:name => 'Test forum from RSpec',
|
51
|
+
'private' => true,
|
52
|
+
'allow_by_email_domain' => true,
|
53
|
+
'allowed_email_domains' => [{'domain' => 'raimo.rspec.example.com'}]
|
54
|
+
})['forum']
|
55
|
+
|
56
|
+
forum['id'].should be_a(Integer)
|
57
|
+
|
58
|
+
deleted_forum = owner.delete("/api/v1/forums/#{forum['id']}.json")['forum']
|
59
|
+
deleted_forum['id'].should == forum['id']
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should get current user with 2-legged call" do
|
63
|
+
user = subject.login_as('mailaddress@example.com') do |token|
|
64
|
+
token.get("/api/v1/users/current.json")['user']
|
65
|
+
end
|
66
|
+
|
67
|
+
user['email'].should == 'mailaddress@example.com'
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should get current user with copied access token" do
|
71
|
+
original_token = subject.login_as('mailaddress@example.com')
|
72
|
+
|
73
|
+
client = UserVoice::Client.new(config['subdomain_name'],
|
74
|
+
config['api_key'],
|
75
|
+
config['api_secret'],
|
76
|
+
:uservoice_domain => config['uservoice_domain'],
|
77
|
+
:protocol => config['protocol'],
|
78
|
+
:oauth_token => original_token.token,
|
79
|
+
:oauth_token_secret => original_token.secret)
|
80
|
+
# Also this works but creates an extra object:
|
81
|
+
# client = client.login_with_access_token(original_token.token, original_token.secret)
|
82
|
+
|
83
|
+
user = client.get("/api/v1/users/current.json")['user']
|
84
|
+
|
85
|
+
user['email'].should == 'mailaddress@example.com'
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should login as an owner" do
|
89
|
+
me = subject.login_as_owner
|
90
|
+
|
91
|
+
owner = me.get("/api/v1/users/current.json")['user']
|
92
|
+
owner['roles']['owner'].should == true
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should not be able to delete when not deleting on behalf of anyone" do
|
96
|
+
lambda {
|
97
|
+
result = subject.delete("/api/v1/users/#{234}.json")
|
98
|
+
}.should raise_error(UserVoice::Unauthorized, /user required/i)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should not be able to delete owner" do
|
102
|
+
owner_access_token = subject.login_as_owner
|
103
|
+
|
104
|
+
owner = owner_access_token.get("/api/v1/users/current.json")['user']
|
105
|
+
|
106
|
+
lambda {
|
107
|
+
result = owner_access_token.delete("/api/v1/users/#{owner['id']}.json")
|
108
|
+
}.should raise_error(UserVoice::Unauthorized, /last owner/i)
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should not be able to delete user without login" do
|
112
|
+
regular_user = subject.login_as('somebodythere@example.com').get("/api/v1/users/current.json")['user']
|
113
|
+
|
114
|
+
lambda {
|
115
|
+
subject.delete("/api/v1/users/#{regular_user['id']}.json")
|
116
|
+
}.should raise_error(UserVoice::Unauthorized)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should get all suggestions using a collection enumerator' do
|
120
|
+
subject.should_receive(:get).once.and_return({
|
121
|
+
"response_data"=>{"page"=>1, "per_page"=>10, "total_records"=>1, "filter"=>"all", "sort"=>"votes"},
|
122
|
+
"suggestions"=>[ {
|
123
|
+
"url"=>"http://uservoice-subdomain.uservoice.com/forums/1-a/suggestions/1-i",
|
124
|
+
"id"=>1,
|
125
|
+
"state"=>"published",
|
126
|
+
"title"=>"a",
|
127
|
+
"text"=>"b",
|
128
|
+
"formatted_text"=>"b",
|
129
|
+
"forum"=>{"id"=>"1", "name"=>"General"}
|
130
|
+
}
|
131
|
+
]})
|
132
|
+
suggestions = subject.get_collection("/api/v1/suggestions.json")
|
133
|
+
count = 0
|
134
|
+
suggestions.each do |suggestion|
|
135
|
+
count += 1
|
136
|
+
end
|
137
|
+
count.should == suggestions.size
|
138
|
+
count.should == 1
|
139
|
+
end
|
140
|
+
|
141
|
+
it "should get an error when trying to query suggestions with an unexistant manual action" do
|
142
|
+
lambda {
|
143
|
+
subject.login_as_owner do |owner_token|
|
144
|
+
owner_token.get("/api/v1/suggestions.json?filter=with_external_id&external_scope=#{external_scope}&manual_action=#{external_scope}")['suggestions']
|
145
|
+
end
|
146
|
+
}.should raise_error(UserVoice::NotFound)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should identify a suggestion" do
|
150
|
+
owner_token = subject.login_as_owner
|
151
|
+
|
152
|
+
suggestions = owner_token.get("/api/v1/suggestions.json?filter=without_external_id&external_scope=#{external_scope}&per_page=1")['suggestions']
|
153
|
+
identifications = suggestions.map {|s| { :id => s['id'], :external_id => s['id'].to_i*10, :url => 'http://url.example.com' } }
|
154
|
+
|
155
|
+
ids = owner_token.put("/api/v1/suggestions/identify.json",
|
156
|
+
:upsert => true,
|
157
|
+
:external_scope => external_scope,
|
158
|
+
:identifications => identifications)['identifications']['ids']
|
159
|
+
ids.should == identifications.map { |s| s[:id] }.sort
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should be able to delete itself" do
|
163
|
+
my_token = subject.login_as('somebodythere@example.com')
|
164
|
+
|
165
|
+
# whoami
|
166
|
+
my_id = my_token.get("/api/v1/users/current.json")['user']['id']
|
167
|
+
|
168
|
+
# Delete myself!
|
169
|
+
my_token.delete("/api/v1/users/#{my_id}.json")['user']['id'].should == my_id
|
170
|
+
|
171
|
+
# I don't exist anymore
|
172
|
+
lambda {
|
173
|
+
my_token.get("/api/v1/users/current.json")
|
174
|
+
}.should raise_error(UserVoice::NotFound)
|
175
|
+
end
|
176
|
+
|
177
|
+
it "should/be able to delete random user and login as him after that" do
|
178
|
+
somebody = subject.login_as('somebodythere@example.com')
|
179
|
+
owner = subject.login_as_owner
|
180
|
+
|
181
|
+
# somebody is still there...
|
182
|
+
regular_user = somebody.get("/api/v1/users/current.json")['user']
|
183
|
+
regular_user['email'].should == 'somebodythere@example.com'
|
184
|
+
|
185
|
+
# delete somebody!
|
186
|
+
owner.delete("/api/v1/users/#{regular_user['id']}.json")['user']['id'].should == regular_user['id']
|
187
|
+
|
188
|
+
# not found anymore!
|
189
|
+
lambda {
|
190
|
+
somebody.get("/api/v1/users/current.json")['errors']['type']
|
191
|
+
}.should raise_error(UserVoice::NotFound)
|
192
|
+
|
193
|
+
# this recreates somebody
|
194
|
+
somebody = subject.login_as('somebodythere@example.com')
|
195
|
+
somebody.get("/api/v1/users/current.json")['user']['id'].should_not == regular_user['id']
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should raise error with invalid email parameter" do
|
199
|
+
expect { subject.login_as('ma') }.to raise_error(UserVoice::Unauthorized)
|
200
|
+
expect { subject.login_as(nil) }.to raise_error(UserVoice::Unauthorized)
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe UserVoice::Collection do
|
4
|
+
PER_PAGE = 500
|
5
|
+
ELEMENTS = 1501 # 4 pages, one record in the last page
|
6
|
+
|
7
|
+
context 'having an empty result set' do
|
8
|
+
let(:client) do
|
9
|
+
client = mock(
|
10
|
+
:get => {"response_data"=>{"page"=>1, "per_page"=>10, "total_records"=>0, "filter"=>"all", "sort"=>"votes"}, "suggestions"=>[]}
|
11
|
+
)
|
12
|
+
end
|
13
|
+
before do
|
14
|
+
@collection = UserVoice::Collection.new(client, '/api/v1/suggestions')
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should return size of zero" do
|
18
|
+
@collection.size.should == 0
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should have zero entries with #each' do
|
22
|
+
@collection.each do |suggestion|
|
23
|
+
raise RuntimeError.new('should be empty')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'having a list with one element' do
|
29
|
+
before do
|
30
|
+
@client = mock(
|
31
|
+
:get => {"response_data"=>{"page"=>1, "per_page"=>10, "total_records"=>1, "filter"=>"all", "sort"=>"votes"}, "suggestions"=>[ {
|
32
|
+
"url"=>"http://uservoice-subdomain.uservoice.com/forums/1-general/suggestions/1-idea",
|
33
|
+
"id"=>1,
|
34
|
+
"state"=>"published",
|
35
|
+
"title"=>"a",
|
36
|
+
"text"=>"b",
|
37
|
+
"formatted_text"=>"b",
|
38
|
+
"forum"=>{"id"=>"1", "name"=>"General"}
|
39
|
+
}
|
40
|
+
]})
|
41
|
+
@collection = UserVoice::Collection.new(@client, '/api/v1/suggestions')
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should have correct size' do
|
45
|
+
@collection.size.should == 1
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should yield correct records ids' do
|
49
|
+
ids = []
|
50
|
+
@collection.each do |val|
|
51
|
+
ids.push(val['id'])
|
52
|
+
end
|
53
|
+
ids.should == [1]
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should map ids' do
|
57
|
+
@collection.map do |val|
|
58
|
+
val['id']
|
59
|
+
end.should == [1]
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'should collect ids' do
|
63
|
+
@client.should_receive(:get).with("/api/v1/suggestions?per_page=#{PER_PAGE}&page=1").once
|
64
|
+
|
65
|
+
@collection.collect do |val|
|
66
|
+
val['id']
|
67
|
+
end.should == [1]
|
68
|
+
|
69
|
+
@collection.map do |val|
|
70
|
+
val['id']
|
71
|
+
end.should == [1]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'having a list with 1501 elements' do
|
76
|
+
|
77
|
+
before do
|
78
|
+
@client = mock()
|
79
|
+
|
80
|
+
4.times.map do |page_index|
|
81
|
+
@client.stub(:get).with("/api/v1/suggestions?per_page=#{PER_PAGE}&page=#{page_index+1}") do
|
82
|
+
{
|
83
|
+
"response_data" => {"page"=> page_index+1, "per_page" => PER_PAGE, "total_records" => ELEMENTS, "filter"=>"all", "sort"=>"votes"},
|
84
|
+
"suggestions"=> (PER_PAGE * page_index + 1).upto([PER_PAGE * (page_index + 1), ELEMENTS].min).map do |idea_index|
|
85
|
+
{
|
86
|
+
"url"=>"http://uservoice-subdomain.uservoice.com/forums/1-general/suggestions/#{idea_index}-idea",
|
87
|
+
"id"=> idea_index,
|
88
|
+
"state"=>"published",
|
89
|
+
"title"=>"Idea ##{idea_index}",
|
90
|
+
"text"=>"Idea ##{idea_index}",
|
91
|
+
"formatted_text"=>"Idea ##{idea_index}",
|
92
|
+
"forum"=>{"id"=>"1", "name"=>"General"}
|
93
|
+
}
|
94
|
+
end
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
@collection = UserVoice::Collection.new(@client, '/api/v1/suggestions')
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should have correct size' do
|
102
|
+
@client.should_receive(:get).with("/api/v1/suggestions?per_page=#{PER_PAGE}&page=1").once
|
103
|
+
@collection.size.should == ELEMENTS
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'should get last element and array size with two api calls' do
|
107
|
+
@collection.last['id'].should == ELEMENTS
|
108
|
+
@collection.first['id'].should == 1
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should yield correct records ids' do
|
112
|
+
ids = []
|
113
|
+
@collection.each do |val|
|
114
|
+
ids.push(val['id'])
|
115
|
+
end
|
116
|
+
ids.size.should == ELEMENTS
|
117
|
+
ids.should == 1.upto(ELEMENTS).to_a
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should map ids' do
|
121
|
+
@collection.map do |val|
|
122
|
+
val['id']
|
123
|
+
end.should == 1.upto(ELEMENTS).to_a
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: uservoice-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-09
|
12
|
+
date: 2012-10-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
16
|
-
requirement:
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,15 @@ dependencies:
|
|
21
21
|
version: 1.0.5
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements:
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.5
|
25
30
|
- !ruby/object:Gem::Dependency
|
26
31
|
name: ezcrypto
|
27
|
-
requirement:
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
28
33
|
none: false
|
29
34
|
requirements:
|
30
35
|
- - ! '>='
|
@@ -32,10 +37,15 @@ dependencies:
|
|
32
37
|
version: 0.7.2
|
33
38
|
type: :runtime
|
34
39
|
prerelease: false
|
35
|
-
version_requirements:
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.7.2
|
36
46
|
- !ruby/object:Gem::Dependency
|
37
47
|
name: json
|
38
|
-
requirement:
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
39
49
|
none: false
|
40
50
|
requirements:
|
41
51
|
- - ! '>='
|
@@ -43,10 +53,15 @@ dependencies:
|
|
43
53
|
version: 1.7.5
|
44
54
|
type: :runtime
|
45
55
|
prerelease: false
|
46
|
-
version_requirements:
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.7.5
|
47
62
|
- !ruby/object:Gem::Dependency
|
48
63
|
name: oauth
|
49
|
-
requirement:
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
50
65
|
none: false
|
51
66
|
requirements:
|
52
67
|
- - ! '>='
|
@@ -54,7 +69,12 @@ dependencies:
|
|
54
69
|
version: 0.4.7
|
55
70
|
type: :runtime
|
56
71
|
prerelease: false
|
57
|
-
version_requirements:
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.4.7
|
58
78
|
description: The gem provides Ruby-bindings to UserVoice API and helps generating
|
59
79
|
Single-Sign-On tokens.
|
60
80
|
email:
|
@@ -68,10 +88,14 @@ files:
|
|
68
88
|
- README.md
|
69
89
|
- Rakefile
|
70
90
|
- lib/uservoice-ruby.rb
|
91
|
+
- lib/uservoice/client.rb
|
92
|
+
- lib/uservoice/collection.rb
|
71
93
|
- lib/uservoice/user_voice.rb
|
72
94
|
- lib/uservoice/version.rb
|
73
95
|
- spec/config.yml.templ
|
74
96
|
- spec/lib/user_voice_spec.rb
|
97
|
+
- spec/lib/uservoice/client_spec.rb
|
98
|
+
- spec/lib/uservoice/collection_spec.rb
|
75
99
|
- spec/spec_helper.rb
|
76
100
|
- uservoice-ruby.gemspec
|
77
101
|
homepage: http://developer.uservoice.com
|
@@ -94,11 +118,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
118
|
version: '0'
|
95
119
|
requirements: []
|
96
120
|
rubyforge_project: uservoice-ruby
|
97
|
-
rubygems_version: 1.8.
|
121
|
+
rubygems_version: 1.8.23
|
98
122
|
signing_key:
|
99
123
|
specification_version: 3
|
100
124
|
summary: Client library for UserVoice API
|
101
125
|
test_files:
|
102
126
|
- spec/config.yml.templ
|
103
127
|
- spec/lib/user_voice_spec.rb
|
128
|
+
- spec/lib/uservoice/client_spec.rb
|
129
|
+
- spec/lib/uservoice/collection_spec.rb
|
104
130
|
- spec/spec_helper.rb
|