rack-oauth2-server 1.2.2 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +14 -0
- data/Gemfile +3 -0
- data/README.rdoc +26 -7
- data/Rakefile +1 -1
- data/VERSION +1 -0
- data/lib/rack/oauth2/admin/css/screen.css +233 -0
- data/lib/rack/oauth2/admin/images/loading.gif +0 -0
- data/lib/rack/oauth2/admin/js/application.js +154 -0
- data/lib/rack/oauth2/admin/js/jquery.js +166 -0
- data/lib/rack/oauth2/admin/js/jquery.tmpl.js +414 -0
- data/lib/rack/oauth2/admin/js/sammy.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.json.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.storage.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.title.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.tmpl.js +5 -0
- data/lib/rack/oauth2/admin/js/underscore.js +722 -0
- data/lib/rack/oauth2/admin/views/client.tmpl +48 -0
- data/lib/rack/oauth2/admin/views/clients.tmpl +36 -0
- data/lib/rack/oauth2/admin/views/edit.tmpl +57 -0
- data/lib/rack/oauth2/admin/views/index.html +26 -0
- data/lib/rack/oauth2/models/access_grant.rb +6 -4
- data/lib/rack/oauth2/models/access_token.rb +36 -4
- data/lib/rack/oauth2/models/auth_request.rb +4 -3
- data/lib/rack/oauth2/models/client.rb +15 -2
- data/lib/rack/oauth2/server.rb +71 -58
- data/lib/rack/oauth2/server/admin.rb +216 -0
- data/lib/rack/oauth2/server/helper.rb +4 -4
- data/lib/rack/oauth2/sinatra.rb +2 -2
- data/rack-oauth2-server.gemspec +2 -3
- data/test/admin/api_test.rb +196 -0
- data/test/admin_test_.rb +49 -0
- data/test/{access_grant_test.rb → oauth/access_grant_test.rb} +1 -1
- data/test/{access_token_test.rb → oauth/access_token_test.rb} +83 -12
- data/test/{authorization_test.rb → oauth/authorization_test.rb} +1 -1
- data/test/rails/config/environment.rb +2 -0
- data/test/rails/log/test.log +72938 -0
- data/test/setup.rb +17 -1
- data/test/sinatra/my_app.rb +1 -1
- metadata +27 -9
- data/lib/rack/oauth2/server/version.rb +0 -9
@@ -0,0 +1,216 @@
|
|
1
|
+
require "sinatra/base"
|
2
|
+
require "json"
|
3
|
+
require "rack/oauth2/sinatra"
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module OAuth2
|
7
|
+
class Server
|
8
|
+
class Admin < ::Sinatra::Base
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
# Rack module that mounts the specified class on the specified path,
|
13
|
+
# and passes all other request to the application.
|
14
|
+
class Mount
|
15
|
+
class << self
|
16
|
+
def mount(klass, path)
|
17
|
+
@klass = klass
|
18
|
+
@path = path
|
19
|
+
@match = /^#{Regexp.escape(path)}\/(.*)$/
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :klass, :path, :match
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(app)
|
26
|
+
@pass = app
|
27
|
+
@admin = self.class.klass.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def call(env)
|
31
|
+
path = env["PATH_INFO"].to_s
|
32
|
+
script_name = env['SCRIPT_NAME']
|
33
|
+
if path =~ self.class.match && rest = $1
|
34
|
+
env.merge! "SCRIPT_NAME"=>(script_name + self.class.path), "PATH_INFO"=>"/#{rest}"
|
35
|
+
return @admin.call(env)
|
36
|
+
else
|
37
|
+
return @pass.call(env)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns Rack handle that mounts Admin on the specified path, and
|
43
|
+
# forwards all other requests back to the application.
|
44
|
+
#
|
45
|
+
# @param [String, nil] path The path to mount on, defaults to
|
46
|
+
# /oauth/admin
|
47
|
+
# @return [Object] Rack module
|
48
|
+
#
|
49
|
+
# @example To include admin console in Rails 2.x app
|
50
|
+
# config.middleware.use Rack::OAuth2::Server::Admin.mount
|
51
|
+
def mount(path = "/oauth/admin")
|
52
|
+
mount = Class.new(Mount)
|
53
|
+
mount.mount Admin, "/oauth/admin"
|
54
|
+
mount
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
# Need client ID to get access token to access this console.
|
60
|
+
set :client_id, nil
|
61
|
+
# Need client secret to get access token to access this console.
|
62
|
+
set :client_secret, nil
|
63
|
+
# Use this URL to authorize access to this console. If not set, goes to
|
64
|
+
# /oauth/authorize.
|
65
|
+
set :authorize, nil
|
66
|
+
|
67
|
+
# Number of tokens to return in each page.
|
68
|
+
set :tokens_per_page, 100
|
69
|
+
set :public, ::File.dirname(__FILE__) + "/../admin"
|
70
|
+
mime_type :js, "text/javascript"
|
71
|
+
mime_type :tmpl, "text/x-jquery-template"
|
72
|
+
|
73
|
+
|
74
|
+
helpers Rack::OAuth2::Sinatra::Helpers
|
75
|
+
extend Rack::OAuth2::Sinatra
|
76
|
+
use Rack::OAuth2::Server
|
77
|
+
|
78
|
+
# Force HTTPS except for development environment.
|
79
|
+
before do
|
80
|
+
redirect request.url.sub(/^http:/, "https:") unless request.scheme == "https"
|
81
|
+
end unless development?
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
# -- Static content --
|
86
|
+
|
87
|
+
# It's a single-page app, this is that single page.
|
88
|
+
get "/" do
|
89
|
+
send_file settings.public + "/views/index.html"
|
90
|
+
end
|
91
|
+
|
92
|
+
# Service JavaScript, CSS and jQuery templates from the gem.
|
93
|
+
%w{js css views}.each do |path|
|
94
|
+
get "/#{path}/:name" do
|
95
|
+
send_file settings.public + "/#{path}/" + params[:name]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
# -- Getting an access token --
|
101
|
+
|
102
|
+
# To get an OAuth token, you need client ID and secret, two values we
|
103
|
+
# didn't pass on to the JavaScript code, so it has no way to request
|
104
|
+
# authorization directly. Instead, it redirects to this URL which in turn
|
105
|
+
# redirects to the authorization endpoint. This redirect does accept the
|
106
|
+
# state parameter, which will be returned after authorization.
|
107
|
+
get "/authorize" do
|
108
|
+
redirect_uri = "#{request.scheme}://#{request.host}:#{request.port}#{request.script_name}"
|
109
|
+
query = { :client_id=>settings.client_id, :client_secret=>settings.client_secret, :state=>params[:state],
|
110
|
+
:response_type=>"token", :scope=>"oauth-admin", :redirect_uri=>redirect_uri }
|
111
|
+
auth_url = settings.authorize || "#{request.scheme}://#{request.host}:#{request.port}/oauth/authorize"
|
112
|
+
redirect "#{auth_url}?#{Rack::Utils.build_query(query)}"
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
# -- API --
|
117
|
+
|
118
|
+
oauth_required "/api/clients", "/api/client/:id", "/api/client/:id/revoke", "/api/token/:token/revoke", :scope=>"oauth-admin"
|
119
|
+
|
120
|
+
get "/api/clients" do
|
121
|
+
content_type "application/json"
|
122
|
+
json = { :list=>Server::Client.all.map { |client| client_as_json(client) },
|
123
|
+
:tokens=>{ :total=>Server::AccessToken.count, :week=>Server::AccessToken.count(:days=>7),
|
124
|
+
:revoked=>Server::AccessToken.count(:days=>7, :revoked=>true) } }
|
125
|
+
json.to_json
|
126
|
+
end
|
127
|
+
|
128
|
+
post "/api/clients" do
|
129
|
+
begin
|
130
|
+
client = Server::Client.create(validate_params(params))
|
131
|
+
redirect "#{request.script_name}/api/client/#{client.id}"
|
132
|
+
rescue
|
133
|
+
halt 400, $!.message
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
get "/api/client/:id" do
|
138
|
+
content_type "application/json"
|
139
|
+
client = Server::Client.find(params[:id])
|
140
|
+
json = client_as_json(client, true)
|
141
|
+
|
142
|
+
page = [params[:page].to_i, 1].max
|
143
|
+
offset = (page - 1) * settings.tokens_per_page
|
144
|
+
total = Server::AccessToken.count(:client_id=>client.id)
|
145
|
+
tokens = Server::AccessToken.for_client(params[:id], offset, settings.tokens_per_page)
|
146
|
+
json[:tokens] = { :list=>tokens.map { |token| token_as_json(token) } }
|
147
|
+
json[:tokens][:total] = total
|
148
|
+
json[:tokens][:page] = page
|
149
|
+
json[:tokens][:next] = "#{request.script_name}/client/#{params[:id]}?page=#{page + 1}" if total > page * settings.tokens_per_page
|
150
|
+
json[:tokens][:previous] = "#{request.script_name}/client/#{params[:id]}?page=#{page - 1}" if page > 1
|
151
|
+
json[:tokens][:total] = Server::AccessToken.count(:client_id=>client.id)
|
152
|
+
json[:tokens][:week] = Server::AccessToken.count(:client_id=>client.id, :days=>7)
|
153
|
+
json[:tokens][:revoked] = Server::AccessToken.count(:client_id=>client.id, :days=>7, :revoked=>true)
|
154
|
+
|
155
|
+
json.to_json
|
156
|
+
end
|
157
|
+
|
158
|
+
put "/api/client/:id" do
|
159
|
+
client = Server::Client.find(params[:id])
|
160
|
+
begin
|
161
|
+
client.update validate_params(params)
|
162
|
+
redirect "#{request.script_name}/api/client/#{client.id}"
|
163
|
+
rescue
|
164
|
+
halt 400, $!.message
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
post "/api/client/:id/revoke" do
|
169
|
+
client = Server::Client.find(params[:id])
|
170
|
+
client.revoke!
|
171
|
+
200
|
172
|
+
end
|
173
|
+
|
174
|
+
post "/api/token/:token/revoke" do
|
175
|
+
token = Server::AccessToken.from_token(params[:token])
|
176
|
+
token.revoke!
|
177
|
+
200
|
178
|
+
end
|
179
|
+
|
180
|
+
helpers do
|
181
|
+
def validate_params(params)
|
182
|
+
display_name = params[:displayName].to_s.strip
|
183
|
+
halt 400, "Missing display name" if display_name.empty?
|
184
|
+
link = URI.parse(params[:link].to_s.strip).normalize rescue nil
|
185
|
+
halt 400, "Link is not a URL (must be http://....)" unless link
|
186
|
+
halt 400, "Link must be an absolute URL with HTTP/S scheme" unless link.absolute? && %{http https}.include?(link.scheme)
|
187
|
+
redirect_uri = URI.parse(params[:redirectUri].to_s.strip).normalize rescue nil
|
188
|
+
halt 400, "Redirect URL is not a URL (must be http://....)" unless redirect_uri
|
189
|
+
halt 400, "Redirect URL must be an absolute URL with HTTP/S scheme" unless
|
190
|
+
redirect_uri.absolute? && %{http https}.include?(redirect_uri.scheme)
|
191
|
+
if image_url = URI.parse(params[:imageUrl].to_s.strip).normalize rescue nil
|
192
|
+
halt 400, "Image URL must be an absolute URL with HTTP/S scheme" unless
|
193
|
+
image_url.absolute? && %{http https}.include?(image_url.scheme)
|
194
|
+
end
|
195
|
+
{ :display_name=>display_name, :link=>link.to_s, :image_url=>image_url.to_s, :redirect_uri=>redirect_uri.to_s }
|
196
|
+
end
|
197
|
+
|
198
|
+
def client_as_json(client, with_stats = false)
|
199
|
+
{ "id"=>client.id.to_s, "secret"=>client.secret, :redirectUri=>client.redirect_uri,
|
200
|
+
:displayName=>client.display_name, :link=>client.link, :imageUrl=>client.image_url,
|
201
|
+
:url=>"#{request.script_name}/api/client/#{client.id}",
|
202
|
+
:revoke=>"#{request.script_name}/api/client/#{client.id}/revoke",
|
203
|
+
:created=>client.created_at, :revoked=>client.revoked }
|
204
|
+
end
|
205
|
+
|
206
|
+
def token_as_json(token)
|
207
|
+
{ :token=>token.token, :identity=>token.identity, :scope=>token.scope, :created=>token.created_at,
|
208
|
+
:expired=>token.expires_at, :revoked=>token.revoked,
|
209
|
+
:revoke=>"#{request.script_name}/api/token/#{token.token}/revoke" }
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
@@ -66,7 +66,7 @@ module Rack
|
|
66
66
|
#
|
67
67
|
# @return 401
|
68
68
|
def no_access!
|
69
|
-
@response["oauth.no_access"] = true
|
69
|
+
@response["oauth.no_access"] = "true"
|
70
70
|
@response.status = 401
|
71
71
|
end
|
72
72
|
|
@@ -77,7 +77,7 @@ module Rack
|
|
77
77
|
# @param [String] scope The missing scope, e.g. "read"
|
78
78
|
# @return 403
|
79
79
|
def no_scope!(scope)
|
80
|
-
@response["oauth.no_scope"] = scope
|
80
|
+
@response["oauth.no_scope"] = scope.to_s
|
81
81
|
@response.status = 403
|
82
82
|
end
|
83
83
|
|
@@ -106,7 +106,7 @@ module Rack
|
|
106
106
|
# @param [String] identity Identity string
|
107
107
|
# @return 200
|
108
108
|
def grant!(authorization, identity)
|
109
|
-
@response["oauth.authorization"] = authorization
|
109
|
+
@response["oauth.authorization"] = authorization.to_s
|
110
110
|
@response["oauth.identity"] = identity.to_s
|
111
111
|
@response.status = 200
|
112
112
|
end
|
@@ -118,7 +118,7 @@ module Rack
|
|
118
118
|
# @param [String] authorization Authorization handle
|
119
119
|
# @return 401
|
120
120
|
def deny!(authorization)
|
121
|
-
@response["oauth.authorization"] = authorization
|
121
|
+
@response["oauth.authorization"] = authorization.to_s
|
122
122
|
@response.status = 401
|
123
123
|
end
|
124
124
|
|
data/lib/rack/oauth2/sinatra.rb
CHANGED
data/rack-oauth2-server.gemspec
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
$: << File.dirname(__FILE__) + "/lib"
|
2
|
-
require "rack/oauth2/server/version"
|
3
2
|
|
4
3
|
Gem::Specification.new do |spec|
|
5
4
|
spec.name = "rack-oauth2-server"
|
6
|
-
spec.version =
|
5
|
+
spec.version = IO.read("VERSION")
|
7
6
|
spec.author = "Assaf Arkin"
|
8
7
|
spec.email = "assaf@labnotes.org"
|
9
8
|
spec.homepage = "http://github.com/assaf/#{spec.name}"
|
@@ -11,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
11
10
|
spec.description = "Because you don't allow strangers into your app, and OAuth 2.0 is the new awesome."
|
12
11
|
spec.post_install_message = ""
|
13
12
|
|
14
|
-
spec.files = Dir["{bin,lib,rails,test}/**/*", "CHANGELOG", "MIT-LICENSE", "README.rdoc", "Rakefile", "Gemfile", "*.gemspec"]
|
13
|
+
spec.files = Dir["{bin,lib,rails,test}/**/*", "CHANGELOG", "VERSION", "MIT-LICENSE", "README.rdoc", "Rakefile", "Gemfile", "*.gemspec"]
|
15
14
|
|
16
15
|
spec.has_rdoc = true
|
17
16
|
spec.extra_rdoc_files = "README.rdoc", "CHANGELOG"
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require "test/setup"
|
2
|
+
|
3
|
+
class AdminApiTest < Test::Unit::TestCase
|
4
|
+
module Helpers
|
5
|
+
def should_fail_authentication
|
6
|
+
should "respond with status 401 (Unauthorized)" do
|
7
|
+
assert_equal 401, last_response.status
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def should_forbid_access
|
12
|
+
should "respond with status 403 (Forbidden)" do
|
13
|
+
assert_equal 403, last_response.status
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
extend Helpers
|
18
|
+
|
19
|
+
|
20
|
+
def setup
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def without_scope
|
25
|
+
token = Rack::OAuth2::Server::AccessToken.get_token_for("Superman", "nobody", client.id)
|
26
|
+
header "Authorization", "OAuth #{token.token}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_scope
|
30
|
+
token = Rack::OAuth2::Server::AccessToken.get_token_for("Superman", "oauth-admin", client.id)
|
31
|
+
header "Authorization", "OAuth #{token.token}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def json
|
35
|
+
JSON.parse(last_response.body)
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# -- /oauth/admin/api/clients
|
40
|
+
|
41
|
+
context "all clients" do
|
42
|
+
context "without authentication" do
|
43
|
+
setup { get "/oauth/admin/api/clients" }
|
44
|
+
should_fail_authentication
|
45
|
+
end
|
46
|
+
|
47
|
+
context "without scope" do
|
48
|
+
setup { without_scope ; get "/oauth/admin/api/clients" }
|
49
|
+
should_forbid_access
|
50
|
+
end
|
51
|
+
|
52
|
+
context "with scope" do
|
53
|
+
setup { with_scope ; get "/oauth/admin/api/clients" }
|
54
|
+
should "return OK" do
|
55
|
+
assert_equal 200, last_response.status
|
56
|
+
end
|
57
|
+
should "return JSON document" do
|
58
|
+
assert_equal "application/json;charset=utf-8", last_response.content_type
|
59
|
+
end
|
60
|
+
should "return list of clients" do
|
61
|
+
assert Array === json["list"]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "client" do
|
66
|
+
setup do
|
67
|
+
with_scope
|
68
|
+
get "/oauth/admin/api/clients"
|
69
|
+
@first = json["list"].first
|
70
|
+
end
|
71
|
+
|
72
|
+
should "provide client identifier" do
|
73
|
+
assert_equal client.id.to_s, @first["id"]
|
74
|
+
end
|
75
|
+
should "provide client secret" do
|
76
|
+
assert_equal client.secret, @first["secret"]
|
77
|
+
end
|
78
|
+
should "provide redirect URI" do
|
79
|
+
assert_equal client.redirect_uri, @first["redirectUri"]
|
80
|
+
end
|
81
|
+
should "provide display name" do
|
82
|
+
assert_equal client.display_name, @first["displayName"]
|
83
|
+
end
|
84
|
+
should "provide site URL" do
|
85
|
+
assert_equal client.link, @first["link"]
|
86
|
+
end
|
87
|
+
should "provide image URL" do
|
88
|
+
assert_equal client.image_url, @first["imageUrl"]
|
89
|
+
end
|
90
|
+
should "provide created timestamp" do
|
91
|
+
assert_equal client.created_at.to_i, @first["created"]
|
92
|
+
end
|
93
|
+
should "provide link to client resource"do
|
94
|
+
assert_equal ["/oauth/admin/api/client", client.id].join("/"), @first["url"]
|
95
|
+
end
|
96
|
+
should "provide link to revoke resource"do
|
97
|
+
assert_equal ["/oauth/admin/api/client", client.id, "revoke"].join("/"), @first["revoke"]
|
98
|
+
end
|
99
|
+
should "tell if not revoked" do
|
100
|
+
assert @first["revoked"].nil?
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "revoked client" do
|
105
|
+
setup do
|
106
|
+
client.revoke!
|
107
|
+
with_scope
|
108
|
+
get "/oauth/admin/api/clients"
|
109
|
+
@first = json["list"].first
|
110
|
+
end
|
111
|
+
|
112
|
+
should "provide revoked timestamp" do
|
113
|
+
assert_equal client.revoked.to_i, @first["revoked"]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "tokens" do
|
118
|
+
setup do
|
119
|
+
tokens = []
|
120
|
+
1.upto(10).map do |days|
|
121
|
+
Timecop.travel -days*86400 do
|
122
|
+
tokens << Rack::OAuth2::Server::AccessToken.get_token_for("Superman", days.to_s, client.id)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
# Revoke one token today (within past 7 days), one 10 days ago (beyond)
|
126
|
+
tokens.first.revoke!
|
127
|
+
tokens.last.revoke!
|
128
|
+
with_scope ; get "/oauth/admin/api/clients"
|
129
|
+
end
|
130
|
+
|
131
|
+
should "return total number of tokens" do
|
132
|
+
assert_equal 11, json["tokens"]["total"]
|
133
|
+
end
|
134
|
+
should "return number of tokens created past week" do
|
135
|
+
assert_equal 7, json["tokens"]["week"]
|
136
|
+
end
|
137
|
+
should "return number of revoked token past week" do
|
138
|
+
assert_equal 1, json["tokens"]["revoked"]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
# -- /oauth/admin/api/client/:id
|
145
|
+
|
146
|
+
context "single client" do
|
147
|
+
context "without authentication" do
|
148
|
+
setup { get "/oauth/admin/api/client/#{client.id}" }
|
149
|
+
should_fail_authentication
|
150
|
+
end
|
151
|
+
|
152
|
+
context "without scope" do
|
153
|
+
setup { without_scope ; get "/oauth/admin/api/client/#{client.id}" }
|
154
|
+
should_forbid_access
|
155
|
+
end
|
156
|
+
|
157
|
+
context "with scope" do
|
158
|
+
setup { with_scope ; get "/oauth/admin/api/client/#{client.id}" }
|
159
|
+
|
160
|
+
should "return OK" do
|
161
|
+
assert_equal 200, last_response.status
|
162
|
+
end
|
163
|
+
should "return JSON document" do
|
164
|
+
assert_equal "application/json;charset=utf-8", last_response.content_type
|
165
|
+
end
|
166
|
+
should "provide client identifier" do
|
167
|
+
assert_equal client.id.to_s, json["id"]
|
168
|
+
end
|
169
|
+
should "provide client secret" do
|
170
|
+
assert_equal client.secret, json["secret"]
|
171
|
+
end
|
172
|
+
should "provide redirect URI" do
|
173
|
+
assert_equal client.redirect_uri, json["redirectUri"]
|
174
|
+
end
|
175
|
+
should "provide display name" do
|
176
|
+
assert_equal client.display_name, json["displayName"]
|
177
|
+
end
|
178
|
+
should "provide site URL" do
|
179
|
+
assert_equal client.link, json["link"]
|
180
|
+
end
|
181
|
+
should "provide image URL" do
|
182
|
+
assert_equal client.image_url, json["imageUrl"]
|
183
|
+
end
|
184
|
+
should "provide created timestamp" do
|
185
|
+
assert_equal client.created_at.to_i, json["created"]
|
186
|
+
end
|
187
|
+
should "provide link to client resource"do
|
188
|
+
assert_equal ["/oauth/admin/api/client", client.id].join("/"), json["url"]
|
189
|
+
end
|
190
|
+
should "provide link to revoke resource"do
|
191
|
+
assert_equal ["/oauth/admin/api/client", client.id, "revoke"].join("/"), json["revoke"]
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|