uservoice-ruby 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|