rack-oauth2-server 1.2.2 → 1.3.0
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/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
|