doorkeeper 4.2.5 → 5.0.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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE.md +25 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +17 -0
- data/.gitignore +2 -1
- data/.gitlab-ci.yml +16 -0
- data/.hound.yml +2 -13
- data/.rubocop.yml +17 -0
- data/.travis.yml +19 -4
- data/Appraisals +8 -4
- data/CODE_OF_CONDUCT.md +46 -0
- data/Gemfile +1 -1
- data/NEWS.md +115 -0
- data/README.md +175 -39
- data/RELEASING.md +5 -12
- data/Rakefile +6 -0
- data/SECURITY.md +15 -0
- data/UPGRADE.md +2 -0
- data/app/assets/stylesheets/doorkeeper/admin/application.css +2 -2
- data/app/controllers/doorkeeper/application_controller.rb +2 -5
- data/app/controllers/doorkeeper/application_metal_controller.rb +4 -0
- data/app/controllers/doorkeeper/applications_controller.rb +47 -13
- data/app/controllers/doorkeeper/authorizations_controller.rb +55 -12
- data/app/controllers/doorkeeper/authorized_applications_controller.rb +19 -2
- data/app/controllers/doorkeeper/tokens_controller.rb +24 -17
- data/app/helpers/doorkeeper/dashboard_helper.rb +8 -6
- data/app/validators/redirect_uri_validator.rb +13 -2
- data/app/views/doorkeeper/applications/_delete_form.html.erb +3 -1
- data/app/views/doorkeeper/applications/_form.html.erb +31 -19
- data/app/views/doorkeeper/applications/edit.html.erb +1 -1
- data/app/views/doorkeeper/applications/index.html.erb +18 -6
- data/app/views/doorkeeper/applications/new.html.erb +1 -1
- data/app/views/doorkeeper/applications/show.html.erb +8 -5
- data/app/views/doorkeeper/authorizations/error.html.erb +1 -1
- data/app/views/doorkeeper/authorizations/new.html.erb +5 -1
- data/app/views/doorkeeper/authorized_applications/index.html.erb +0 -1
- data/app/views/layouts/doorkeeper/admin.html.erb +15 -15
- data/config/locales/en.yml +19 -6
- data/doorkeeper.gemspec +20 -20
- data/gemfiles/rails_4_2.gemfile +6 -4
- data/gemfiles/rails_5_0.gemfile +4 -4
- data/gemfiles/rails_5_1.gemfile +6 -7
- data/gemfiles/rails_5_2.gemfile +12 -0
- data/gemfiles/rails_master.gemfile +17 -0
- data/lib/doorkeeper/config.rb +123 -71
- data/lib/doorkeeper/engine.rb +7 -3
- data/lib/doorkeeper/errors.rb +20 -5
- data/lib/doorkeeper/grape/helpers.rb +14 -9
- data/lib/doorkeeper/helpers/controller.rb +16 -22
- data/lib/doorkeeper/models/access_grant_mixin.rb +67 -23
- data/lib/doorkeeper/models/access_token_mixin.rb +54 -55
- data/lib/doorkeeper/models/application_mixin.rb +16 -30
- data/lib/doorkeeper/models/concerns/expirable.rb +7 -5
- data/lib/doorkeeper/models/concerns/orderable.rb +13 -0
- data/lib/doorkeeper/models/concerns/scopes.rb +1 -1
- data/lib/doorkeeper/oauth/authorization/code.rb +31 -8
- data/lib/doorkeeper/oauth/authorization/context.rb +15 -0
- data/lib/doorkeeper/oauth/authorization/token.rb +46 -20
- data/lib/doorkeeper/oauth/authorization_code_request.rb +33 -3
- data/lib/doorkeeper/oauth/base_request.rb +23 -7
- data/lib/doorkeeper/oauth/client/credentials.rb +6 -4
- data/lib/doorkeeper/oauth/client.rb +2 -4
- data/lib/doorkeeper/oauth/client_credentials/creator.rb +2 -1
- data/lib/doorkeeper/oauth/client_credentials/issuer.rb +6 -3
- data/lib/doorkeeper/oauth/client_credentials/validation.rb +4 -6
- data/lib/doorkeeper/oauth/client_credentials_request.rb +0 -4
- data/lib/doorkeeper/oauth/error.rb +2 -2
- data/lib/doorkeeper/oauth/error_response.rb +11 -4
- data/lib/doorkeeper/oauth/forbidden_token_response.rb +1 -1
- data/lib/doorkeeper/oauth/helpers/scope_checker.rb +0 -8
- data/lib/doorkeeper/oauth/helpers/uri_checker.rb +15 -0
- data/lib/doorkeeper/oauth/invalid_token_response.rb +3 -4
- data/lib/doorkeeper/oauth/password_access_token_request.rb +8 -4
- data/lib/doorkeeper/oauth/pre_authorization.rb +45 -13
- data/lib/doorkeeper/oauth/refresh_token_request.rb +7 -1
- data/lib/doorkeeper/oauth/scopes.rb +19 -9
- data/lib/doorkeeper/oauth/token.rb +6 -3
- data/lib/doorkeeper/oauth/token_introspection.rb +128 -0
- data/lib/doorkeeper/oauth/token_response.rb +4 -2
- data/lib/doorkeeper/oauth.rb +13 -0
- data/lib/doorkeeper/orm/active_record/access_grant.rb +27 -0
- data/lib/doorkeeper/orm/active_record/access_token.rb +21 -20
- data/lib/doorkeeper/orm/active_record/application.rb +45 -0
- data/lib/doorkeeper/orm/active_record/stale_records_cleaner.rb +26 -0
- data/lib/doorkeeper/orm/active_record.rb +21 -8
- data/lib/doorkeeper/rails/helpers.rb +7 -10
- data/lib/doorkeeper/rails/routes.rb +21 -7
- data/lib/doorkeeper/rake/db.rake +40 -0
- data/lib/doorkeeper/rake/setup.rake +6 -0
- data/lib/doorkeeper/rake.rb +14 -0
- data/lib/doorkeeper/request/authorization_code.rb +0 -2
- data/lib/doorkeeper/request/client_credentials.rb +0 -2
- data/lib/doorkeeper/request/code.rb +0 -2
- data/lib/doorkeeper/request/password.rb +1 -13
- data/lib/doorkeeper/request/refresh_token.rb +0 -2
- data/lib/doorkeeper/request/token.rb +0 -2
- data/lib/doorkeeper/request.rb +29 -30
- data/lib/doorkeeper/validations.rb +3 -2
- data/lib/doorkeeper/version.rb +14 -1
- data/lib/doorkeeper.rb +21 -17
- data/lib/generators/doorkeeper/application_owner_generator.rb +26 -12
- data/lib/generators/doorkeeper/confidential_applications_generator.rb +32 -0
- data/lib/generators/doorkeeper/install_generator.rb +17 -9
- data/lib/generators/doorkeeper/migration_generator.rb +26 -9
- data/lib/generators/doorkeeper/pkce_generator.rb +32 -0
- data/lib/generators/doorkeeper/previous_refresh_token_generator.rb +31 -20
- data/lib/generators/doorkeeper/templates/add_confidential_to_applications.rb.erb +13 -0
- data/lib/generators/doorkeeper/templates/{add_owner_to_application_migration.rb → add_owner_to_application_migration.rb.erb} +1 -1
- data/lib/generators/doorkeeper/templates/{add_previous_refresh_token_to_access_tokens.rb → add_previous_refresh_token_to_access_tokens.rb.erb} +1 -1
- data/lib/generators/doorkeeper/templates/enable_pkce_migration.rb.erb +6 -0
- data/lib/generators/doorkeeper/templates/initializer.rb +104 -12
- data/lib/generators/doorkeeper/templates/{migration.rb → migration.rb.erb} +2 -1
- data/lib/generators/doorkeeper/views_generator.rb +3 -1
- data/spec/controllers/application_metal_controller_spec.rb +50 -0
- data/spec/controllers/applications_controller_spec.rb +141 -17
- data/spec/controllers/authorizations_controller_spec.rb +304 -27
- data/spec/controllers/protected_resources_controller_spec.rb +44 -35
- data/spec/controllers/token_info_controller_spec.rb +17 -21
- data/spec/controllers/tokens_controller_spec.rb +201 -17
- data/spec/dummy/app/assets/config/manifest.js +2 -0
- data/spec/dummy/config/environments/test.rb +4 -5
- data/spec/dummy/config/initializers/doorkeeper.rb +23 -2
- data/spec/dummy/config/initializers/{active_record_belongs_to_required_by_default.rb → new_framework_defaults.rb} +5 -1
- data/spec/dummy/config/initializers/secret_token.rb +0 -1
- data/spec/dummy/config/routes.rb +3 -42
- data/spec/dummy/db/migrate/20111122132257_create_users.rb +3 -1
- data/spec/dummy/db/migrate/20120312140401_add_password_to_users.rb +3 -1
- data/spec/dummy/db/migrate/20151223192035_create_doorkeeper_tables.rb +3 -1
- data/spec/dummy/db/migrate/20151223200000_add_owner_to_application.rb +3 -1
- data/spec/dummy/db/migrate/20160320211015_add_previous_refresh_token_to_access_tokens.rb +3 -1
- data/spec/dummy/db/migrate/20170822064514_enable_pkce.rb +6 -0
- data/spec/dummy/db/migrate/20180210183654_add_confidential_to_applications.rb +13 -0
- data/spec/dummy/db/schema.rb +38 -37
- data/spec/factories.rb +1 -1
- data/spec/generators/application_owner_generator_spec.rb +25 -6
- data/spec/generators/confidential_applications_generator_spec.rb +45 -0
- data/spec/generators/install_generator_spec.rb +1 -1
- data/spec/generators/migration_generator_spec.rb +25 -4
- data/spec/generators/pkce_generator_spec.rb +43 -0
- data/spec/generators/previous_refresh_token_generator_spec.rb +57 -0
- data/spec/generators/views_generator_spec.rb +1 -1
- data/spec/grape/grape_integration_spec.rb +135 -0
- data/spec/helpers/doorkeeper/dashboard_helper_spec.rb +2 -2
- data/spec/lib/config_spec.rb +199 -22
- data/spec/lib/doorkeeper_spec.rb +1 -126
- data/spec/lib/models/expirable_spec.rb +0 -4
- data/spec/lib/models/revocable_spec.rb +2 -4
- data/spec/lib/models/scopes_spec.rb +0 -4
- data/spec/lib/oauth/authorization/uri_builder_spec.rb +0 -4
- data/spec/lib/oauth/authorization_code_request_spec.rb +63 -13
- data/spec/lib/oauth/base_request_spec.rb +43 -10
- data/spec/lib/oauth/base_response_spec.rb +1 -1
- data/spec/lib/oauth/client/credentials_spec.rb +5 -5
- data/spec/lib/oauth/client_credentials/creator_spec.rb +6 -2
- data/spec/lib/oauth/client_credentials/issuer_spec.rb +26 -7
- data/spec/lib/oauth/client_credentials/validation_spec.rb +2 -3
- data/spec/lib/oauth/client_credentials_integration_spec.rb +2 -2
- data/spec/lib/oauth/client_credentials_request_spec.rb +4 -5
- data/spec/lib/oauth/client_spec.rb +0 -3
- data/spec/lib/oauth/code_request_spec.rb +5 -5
- data/spec/lib/oauth/error_response_spec.rb +0 -3
- data/spec/lib/oauth/error_spec.rb +1 -3
- data/spec/lib/oauth/forbidden_token_response_spec.rb +1 -4
- data/spec/lib/oauth/helpers/scope_checker_spec.rb +0 -3
- data/spec/lib/oauth/helpers/unique_token_spec.rb +0 -1
- data/spec/lib/oauth/helpers/uri_checker_spec.rb +115 -3
- data/spec/lib/oauth/invalid_token_response_spec.rb +2 -5
- data/spec/lib/oauth/password_access_token_request_spec.rb +46 -5
- data/spec/lib/oauth/pre_authorization_spec.rb +40 -6
- data/spec/lib/oauth/refresh_token_request_spec.rb +30 -14
- data/spec/lib/oauth/scopes_spec.rb +28 -4
- data/spec/lib/oauth/token_request_spec.rb +10 -13
- data/spec/lib/oauth/token_response_spec.rb +0 -1
- data/spec/lib/oauth/token_spec.rb +37 -14
- data/spec/lib/orm/active_record/stale_records_cleaner_spec.rb +79 -0
- data/spec/lib/request/strategy_spec.rb +0 -1
- data/spec/lib/server_spec.rb +10 -0
- data/spec/models/doorkeeper/access_grant_spec.rb +45 -2
- data/spec/models/doorkeeper/access_token_spec.rb +132 -64
- data/spec/models/doorkeeper/application_spec.rb +114 -23
- data/spec/requests/applications/applications_request_spec.rb +139 -6
- data/spec/requests/applications/authorized_applications_spec.rb +1 -1
- data/spec/requests/endpoints/authorization_spec.rb +1 -1
- data/spec/requests/endpoints/token_spec.rb +15 -6
- data/spec/requests/flows/authorization_code_errors_spec.rb +12 -2
- data/spec/requests/flows/authorization_code_spec.rb +199 -2
- data/spec/requests/flows/client_credentials_spec.rb +73 -5
- data/spec/requests/flows/implicit_grant_errors_spec.rb +3 -3
- data/spec/requests/flows/implicit_grant_spec.rb +38 -11
- data/spec/requests/flows/password_spec.rb +160 -24
- data/spec/requests/flows/refresh_token_spec.rb +6 -6
- data/spec/requests/flows/revoke_token_spec.rb +26 -26
- data/spec/requests/flows/skip_authorization_spec.rb +16 -11
- data/spec/requests/protected_resources/metal_spec.rb +2 -2
- data/spec/requests/protected_resources/private_api_spec.rb +2 -2
- data/spec/routing/custom_controller_routes_spec.rb +63 -7
- data/spec/routing/default_routes_spec.rb +6 -2
- data/spec/routing/scoped_routes_spec.rb +16 -2
- data/spec/spec_helper.rb +54 -3
- data/spec/spec_helper_integration.rb +2 -64
- data/spec/support/dependencies/factory_bot.rb +2 -0
- data/spec/support/doorkeeper_rspec.rb +19 -0
- data/spec/support/helpers/access_token_request_helper.rb +1 -1
- data/spec/support/helpers/authorization_request_helper.rb +4 -4
- data/spec/support/helpers/model_helper.rb +9 -4
- data/spec/support/helpers/request_spec_helper.rb +18 -6
- data/spec/support/helpers/url_helper.rb +15 -10
- data/spec/support/http_method_shim.rb +12 -16
- data/spec/support/shared/controllers_shared_context.rb +2 -6
- data/spec/support/shared/models_shared_examples.rb +4 -4
- data/spec/validators/redirect_uri_validator_spec.rb +58 -7
- data/spec/version/version_spec.rb +15 -0
- data/vendor/assets/stylesheets/doorkeeper/bootstrap.min.css +4 -5
- metadata +75 -33
- data/spec/controllers/application_metal_controller.rb +0 -10
- data/spec/support/dependencies/factory_girl.rb +0 -2
|
@@ -1,7 +1,99 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'spec_helper'
|
|
2
2
|
|
|
3
3
|
module Doorkeeper
|
|
4
4
|
describe ApplicationsController do
|
|
5
|
+
context 'JSON API' do
|
|
6
|
+
render_views
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
allow(Doorkeeper.configuration).to receive(:api_only).and_return(true)
|
|
10
|
+
allow(Doorkeeper.configuration).to receive(:authenticate_admin).and_return(->(*) { true })
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'creates an application' do
|
|
14
|
+
expect do
|
|
15
|
+
post :create, params: {
|
|
16
|
+
doorkeeper_application: {
|
|
17
|
+
name: 'Example',
|
|
18
|
+
redirect_uri: 'https://example.com'
|
|
19
|
+
}, format: :json
|
|
20
|
+
}
|
|
21
|
+
end.to change { Doorkeeper::Application.count }
|
|
22
|
+
|
|
23
|
+
expect(response).to be_successful
|
|
24
|
+
|
|
25
|
+
expect(json_response).to include('id', 'name', 'uid', 'secret', 'redirect_uri', 'scopes')
|
|
26
|
+
|
|
27
|
+
expect(json_response['name']).to eq('Example')
|
|
28
|
+
expect(json_response['redirect_uri']).to eq('https://example.com')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'returns validation errors on wrong create params' do
|
|
32
|
+
expect do
|
|
33
|
+
post :create, params: {
|
|
34
|
+
doorkeeper_application: {
|
|
35
|
+
name: 'Example'
|
|
36
|
+
}, format: :json
|
|
37
|
+
}
|
|
38
|
+
end.not_to change { Doorkeeper::Application.count }
|
|
39
|
+
|
|
40
|
+
expect(response).to have_http_status(422)
|
|
41
|
+
|
|
42
|
+
expect(json_response).to include('errors')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'returns application info' do
|
|
46
|
+
application = FactoryBot.create(:application, name: 'Change me')
|
|
47
|
+
|
|
48
|
+
get :show, params: { id: application.id, format: :json }
|
|
49
|
+
|
|
50
|
+
expect(response).to be_successful
|
|
51
|
+
|
|
52
|
+
expect(json_response).to include('id', 'name', 'uid', 'secret', 'redirect_uri', 'scopes')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'updates application' do
|
|
56
|
+
application = FactoryBot.create(:application, name: 'Change me')
|
|
57
|
+
|
|
58
|
+
put :update, params: {
|
|
59
|
+
id: application.id,
|
|
60
|
+
doorkeeper_application: {
|
|
61
|
+
name: 'Example App',
|
|
62
|
+
redirect_uri: 'https://example.com'
|
|
63
|
+
}, format: :json
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
expect(application.reload.name).to eq 'Example App'
|
|
67
|
+
|
|
68
|
+
expect(json_response).to include('id', 'name', 'uid', 'secret', 'redirect_uri', 'scopes')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'returns validation errors on wrong update params' do
|
|
72
|
+
application = FactoryBot.create(:application, name: 'Change me')
|
|
73
|
+
|
|
74
|
+
put :update, params: {
|
|
75
|
+
id: application.id,
|
|
76
|
+
doorkeeper_application: {
|
|
77
|
+
name: 'Example App',
|
|
78
|
+
redirect_uri: 'localhost:3000'
|
|
79
|
+
}, format: :json
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
expect(response).to have_http_status(422)
|
|
83
|
+
|
|
84
|
+
expect(json_response).to include('errors')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'destroys an application' do
|
|
88
|
+
application = FactoryBot.create(:application)
|
|
89
|
+
|
|
90
|
+
delete :destroy, params: { id: application.id, format: :json }
|
|
91
|
+
|
|
92
|
+
expect(response).to have_http_status(204)
|
|
93
|
+
expect(Application.count).to be_zero
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
5
97
|
context 'when admin is not authenticated' do
|
|
6
98
|
before do
|
|
7
99
|
allow(Doorkeeper.configuration).to receive(:authenticate_admin).and_return(proc do
|
|
@@ -16,41 +108,73 @@ module Doorkeeper
|
|
|
16
108
|
|
|
17
109
|
it 'does not create application' do
|
|
18
110
|
expect do
|
|
19
|
-
post :create,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
111
|
+
post :create,
|
|
112
|
+
params: {
|
|
113
|
+
doorkeeper_application: {
|
|
114
|
+
name: 'Example',
|
|
115
|
+
redirect_uri: 'https://example.com'
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
end.not_to change { Doorkeeper::Application.count }
|
|
23
119
|
end
|
|
24
120
|
end
|
|
25
121
|
|
|
26
122
|
context 'when admin is authenticated' do
|
|
123
|
+
render_views
|
|
124
|
+
|
|
27
125
|
before do
|
|
28
|
-
allow(Doorkeeper.configuration).to receive(:authenticate_admin).and_return(->(
|
|
126
|
+
allow(Doorkeeper.configuration).to receive(:authenticate_admin).and_return(->(*) { true })
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'sorts applications by created_at' do
|
|
130
|
+
first_application = FactoryBot.create(:application)
|
|
131
|
+
second_application = FactoryBot.create(:application)
|
|
132
|
+
expect(Doorkeeper::Application).to receive(:ordered_by).and_call_original
|
|
133
|
+
|
|
134
|
+
get :index
|
|
135
|
+
|
|
136
|
+
expect(response.body).to have_selector("tbody tr:first-child#application_#{first_application.id}")
|
|
137
|
+
expect(response.body).to have_selector("tbody tr:last-child#application_#{second_application.id}")
|
|
29
138
|
end
|
|
30
139
|
|
|
31
140
|
it 'creates application' do
|
|
32
141
|
expect do
|
|
33
|
-
post :create,
|
|
34
|
-
|
|
35
|
-
|
|
142
|
+
post :create,
|
|
143
|
+
params: {
|
|
144
|
+
doorkeeper_application: {
|
|
145
|
+
name: 'Example',
|
|
146
|
+
redirect_uri: 'https://example.com'
|
|
147
|
+
}
|
|
148
|
+
}
|
|
36
149
|
end.to change { Doorkeeper::Application.count }.by(1)
|
|
150
|
+
|
|
37
151
|
expect(response).to be_redirect
|
|
38
152
|
end
|
|
39
153
|
|
|
40
154
|
it 'does not allow mass assignment of uid or secret' do
|
|
41
|
-
application =
|
|
42
|
-
put :update,
|
|
43
|
-
|
|
44
|
-
|
|
155
|
+
application = FactoryBot.create(:application)
|
|
156
|
+
put :update,
|
|
157
|
+
params: {
|
|
158
|
+
id: application.id,
|
|
159
|
+
doorkeeper_application: {
|
|
160
|
+
uid: '1A2B3C4D',
|
|
161
|
+
secret: '1A2B3C4D'
|
|
162
|
+
}
|
|
163
|
+
}
|
|
45
164
|
|
|
46
165
|
expect(application.reload.uid).not_to eq '1A2B3C4D'
|
|
47
166
|
end
|
|
48
167
|
|
|
49
168
|
it 'updates application' do
|
|
50
|
-
application =
|
|
51
|
-
put :update,
|
|
52
|
-
|
|
53
|
-
|
|
169
|
+
application = FactoryBot.create(:application)
|
|
170
|
+
put :update,
|
|
171
|
+
params: {
|
|
172
|
+
id: application.id, doorkeeper_application: {
|
|
173
|
+
name: 'Example',
|
|
174
|
+
redirect_uri: 'https://example.com'
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
54
178
|
expect(application.reload.name).to eq 'Example'
|
|
55
179
|
end
|
|
56
180
|
end
|
|
@@ -1,29 +1,47 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'spec_helper'
|
|
2
2
|
|
|
3
3
|
describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
4
4
|
include AuthorizationRequestHelper
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
if Rails::VERSION::MAJOR >= 5
|
|
7
|
+
class ActionDispatch::TestResponse
|
|
8
|
+
def query_params
|
|
9
|
+
@query_params ||= begin
|
|
10
|
+
fragment = URI.parse(location).fragment
|
|
11
|
+
Rack::Utils.parse_query(fragment)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
else
|
|
16
|
+
class ActionController::TestResponse
|
|
17
|
+
def query_params
|
|
18
|
+
@query_params ||= begin
|
|
19
|
+
fragment = URI.parse(location).fragment
|
|
20
|
+
Rack::Utils.parse_query(fragment)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
9
24
|
end
|
|
10
25
|
|
|
11
26
|
def translated_error_message(key)
|
|
12
|
-
I18n.translate key, scope: [
|
|
27
|
+
I18n.translate key, scope: %i[doorkeeper errors messages]
|
|
13
28
|
end
|
|
14
29
|
|
|
15
|
-
let(:client) {
|
|
30
|
+
let(:client) { FactoryBot.create :application }
|
|
16
31
|
let(:user) { User.create!(name: 'Joe', password: 'sekret') }
|
|
17
|
-
let(:access_token) {
|
|
32
|
+
let(:access_token) { FactoryBot.build :access_token, resource_owner_id: user.id, application_id: client.id }
|
|
18
33
|
|
|
19
34
|
before do
|
|
20
35
|
allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(["implicit"])
|
|
21
36
|
allow(controller).to receive(:current_resource_owner).and_return(user)
|
|
37
|
+
allow(Doorkeeper.configuration).to receive(:custom_access_token_expires_in).and_return(proc { |context|
|
|
38
|
+
context.grant_type == Doorkeeper::OAuth::IMPLICIT ? 1234 : nil
|
|
39
|
+
})
|
|
22
40
|
end
|
|
23
41
|
|
|
24
42
|
describe 'POST #create' do
|
|
25
43
|
before do
|
|
26
|
-
post :create, client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri
|
|
44
|
+
post :create, params: { client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri }
|
|
27
45
|
end
|
|
28
46
|
|
|
29
47
|
it 'redirects after authorization' do
|
|
@@ -35,15 +53,15 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
|
35
53
|
end
|
|
36
54
|
|
|
37
55
|
it 'includes access token in fragment' do
|
|
38
|
-
expect(
|
|
56
|
+
expect(response.query_params['access_token']).to eq(Doorkeeper::AccessToken.first.token)
|
|
39
57
|
end
|
|
40
58
|
|
|
41
59
|
it 'includes token type in fragment' do
|
|
42
|
-
expect(
|
|
60
|
+
expect(response.query_params['token_type']).to eq('Bearer')
|
|
43
61
|
end
|
|
44
62
|
|
|
45
63
|
it 'includes token expiration in fragment' do
|
|
46
|
-
expect(
|
|
64
|
+
expect(response.query_params['expires_in'].to_i).to eq(1234)
|
|
47
65
|
end
|
|
48
66
|
|
|
49
67
|
it 'issues the token for the current client' do
|
|
@@ -55,10 +73,48 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
|
55
73
|
end
|
|
56
74
|
end
|
|
57
75
|
|
|
76
|
+
describe "POST #create in API mode" do
|
|
77
|
+
before do
|
|
78
|
+
allow(Doorkeeper.configuration).to receive(:api_only).and_return(true)
|
|
79
|
+
post :create, params: { client_id: client.uid, response_type: "token", redirect_uri: client.redirect_uri }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
let(:response_json_body) { JSON.parse(response.body) }
|
|
83
|
+
let(:redirect_uri) { response_json_body["redirect_uri"] }
|
|
84
|
+
|
|
85
|
+
it "renders success after authorization" do
|
|
86
|
+
expect(response).to be_successful
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "renders correct redirect uri" do
|
|
90
|
+
expect(redirect_uri).to match(/^#{client.redirect_uri}/)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "includes access token in fragment" do
|
|
94
|
+
expect(redirect_uri.match(/access_token=([a-f0-9]+)&?/)[1]).to eq(Doorkeeper::AccessToken.first.token)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "includes token type in fragment" do
|
|
98
|
+
expect(redirect_uri.match(/token_type=(\w+)&?/)[1]).to eq "Bearer"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "includes token expiration in fragment" do
|
|
102
|
+
expect(redirect_uri.match(/expires_in=(\d+)&?/)[1].to_i).to eq 1234
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "issues the token for the current client" do
|
|
106
|
+
expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "issues the token for the current resource owner" do
|
|
110
|
+
expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
58
114
|
describe 'POST #create with errors' do
|
|
59
115
|
before do
|
|
60
116
|
default_scopes_exist :public
|
|
61
|
-
post :create, client_id: client.uid, response_type: 'token', scope: 'invalid', redirect_uri: client.redirect_uri
|
|
117
|
+
post :create, params: { client_id: client.uid, response_type: 'token', scope: 'invalid', redirect_uri: client.redirect_uri }
|
|
62
118
|
end
|
|
63
119
|
|
|
64
120
|
it 'redirects after authorization' do
|
|
@@ -66,19 +122,54 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
|
66
122
|
end
|
|
67
123
|
|
|
68
124
|
it 'redirects to client redirect uri' do
|
|
69
|
-
expect(response.location).to match(
|
|
125
|
+
expect(response.location).to match(/^#{client.redirect_uri}/)
|
|
70
126
|
end
|
|
71
127
|
|
|
72
128
|
it 'does not include access token in fragment' do
|
|
73
|
-
expect(
|
|
129
|
+
expect(response.query_params['access_token']).to be_nil
|
|
74
130
|
end
|
|
75
131
|
|
|
76
132
|
it 'includes error in fragment' do
|
|
77
|
-
expect(
|
|
133
|
+
expect(response.query_params['error']).to eq('invalid_scope')
|
|
78
134
|
end
|
|
79
135
|
|
|
80
136
|
it 'includes error description in fragment' do
|
|
81
|
-
expect(
|
|
137
|
+
expect(response.query_params['error_description']).to eq(translated_error_message(:invalid_scope))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'does not issue any access token' do
|
|
141
|
+
expect(Doorkeeper::AccessToken.all).to be_empty
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
describe 'POST #create in API mode with errors' do
|
|
146
|
+
before do
|
|
147
|
+
allow(Doorkeeper.configuration).to receive(:api_only).and_return(true)
|
|
148
|
+
default_scopes_exist :public
|
|
149
|
+
post :create, params: { client_id: client.uid, response_type: 'token', scope: 'invalid', redirect_uri: client.redirect_uri }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
let(:response_json_body) { JSON.parse(response.body) }
|
|
153
|
+
let(:redirect_uri) { response_json_body['redirect_uri'] }
|
|
154
|
+
|
|
155
|
+
it 'renders 400 error' do
|
|
156
|
+
expect(response.status).to eq 401
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'includes correct redirect URI' do
|
|
160
|
+
expect(redirect_uri).to match(/^#{client.redirect_uri}/)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'does not include access token in fragment' do
|
|
164
|
+
expect(redirect_uri.match(/access_token=([a-f0-9]+)&?/)).to be_nil
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'includes error in redirect uri' do
|
|
168
|
+
expect(redirect_uri.match(/error=([a-z_]+)&?/)[1]).to eq 'invalid_scope'
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it 'includes error description in redirect uri' do
|
|
172
|
+
expect(redirect_uri.match(/error_description=(.+)&?/)[1]).to_not be_nil
|
|
82
173
|
end
|
|
83
174
|
|
|
84
175
|
it 'does not issue any access token' do
|
|
@@ -91,11 +182,11 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
|
91
182
|
allow(Doorkeeper.configuration).to receive(:reuse_access_token).and_return(true)
|
|
92
183
|
|
|
93
184
|
access_token.save!
|
|
94
|
-
post :create, client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri
|
|
185
|
+
post :create, params: { client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri }
|
|
95
186
|
end
|
|
96
187
|
|
|
97
188
|
it 'returns the existing access token in a fragment' do
|
|
98
|
-
expect(
|
|
189
|
+
expect(response.query_params['access_token']).to eq(access_token.token)
|
|
99
190
|
end
|
|
100
191
|
|
|
101
192
|
it 'does not creates a new access token' do
|
|
@@ -103,13 +194,47 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
|
103
194
|
end
|
|
104
195
|
end
|
|
105
196
|
|
|
197
|
+
describe 'POST #create with callbacks' do
|
|
198
|
+
after do
|
|
199
|
+
client.update_attribute :redirect_uri, 'urn:ietf:wg:oauth:2.0:oob'
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
describe 'when successful' do
|
|
203
|
+
after do
|
|
204
|
+
post :create, params: { client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'should call :before_successful_authorization callback' do
|
|
208
|
+
expect(Doorkeeper.configuration).to receive_message_chain(:before_successful_authorization, :call).with(instance_of(described_class))
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it 'should call :after_successful_authorization callback' do
|
|
212
|
+
expect(Doorkeeper.configuration).to receive_message_chain(:after_successful_authorization, :call).with(instance_of(described_class))
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
describe 'with errors' do
|
|
217
|
+
after do
|
|
218
|
+
post :create, params: { client_id: client.uid, response_type: 'token', redirect_uri: 'bad_uri' }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it 'should not call :before_successful_authorization callback' do
|
|
222
|
+
expect(Doorkeeper.configuration).not_to receive(:before_successful_authorization)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it 'should not call :after_successful_authorization callback' do
|
|
226
|
+
expect(Doorkeeper.configuration).not_to receive(:after_successful_authorization)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
106
231
|
describe 'GET #new token request with native url and skip_authorization true' do
|
|
107
232
|
before do
|
|
108
233
|
allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc do
|
|
109
234
|
true
|
|
110
235
|
end)
|
|
111
236
|
client.update_attribute :redirect_uri, 'urn:ietf:wg:oauth:2.0:oob'
|
|
112
|
-
get :new, client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri
|
|
237
|
+
get :new, params: { client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri }
|
|
113
238
|
end
|
|
114
239
|
|
|
115
240
|
it 'should redirect immediately' do
|
|
@@ -128,18 +253,17 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
|
128
253
|
|
|
129
254
|
describe 'GET #new code request with native url and skip_authorization true' do
|
|
130
255
|
before do
|
|
131
|
-
allow(Doorkeeper.configuration).to receive(:grant_flows).
|
|
132
|
-
and_return(%w(authorization_code))
|
|
256
|
+
allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(%w[authorization_code])
|
|
133
257
|
allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc do
|
|
134
258
|
true
|
|
135
259
|
end)
|
|
136
260
|
client.update_attribute :redirect_uri, 'urn:ietf:wg:oauth:2.0:oob'
|
|
137
|
-
get :new, client_id: client.uid, response_type: 'code', redirect_uri: client.redirect_uri
|
|
261
|
+
get :new, params: { client_id: client.uid, response_type: 'code', redirect_uri: client.redirect_uri }
|
|
138
262
|
end
|
|
139
263
|
|
|
140
264
|
it 'should redirect immediately' do
|
|
141
265
|
expect(response).to be_redirect
|
|
142
|
-
expect(response.location).to match(/oauth\/authorize
|
|
266
|
+
expect(response.location).to match(/oauth\/authorize\/native\?code=#{Doorkeeper::AccessGrant.first.token}/)
|
|
143
267
|
end
|
|
144
268
|
|
|
145
269
|
it 'should issue a grant' do
|
|
@@ -156,7 +280,7 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
|
156
280
|
allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc do
|
|
157
281
|
true
|
|
158
282
|
end)
|
|
159
|
-
get :new, client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri
|
|
283
|
+
get :new, params: { client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri }
|
|
160
284
|
end
|
|
161
285
|
|
|
162
286
|
it 'should redirect immediately' do
|
|
@@ -169,11 +293,11 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
|
169
293
|
end
|
|
170
294
|
|
|
171
295
|
it 'includes token type in fragment' do
|
|
172
|
-
expect(
|
|
296
|
+
expect(response.query_params['token_type']).to eq('Bearer')
|
|
173
297
|
end
|
|
174
298
|
|
|
175
299
|
it 'includes token expiration in fragment' do
|
|
176
|
-
expect(
|
|
300
|
+
expect(response.query_params['expires_in'].to_i).to eq(1234)
|
|
177
301
|
end
|
|
178
302
|
|
|
179
303
|
it 'issues the token for the current client' do
|
|
@@ -185,10 +309,72 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
|
185
309
|
end
|
|
186
310
|
end
|
|
187
311
|
|
|
312
|
+
describe 'GET #new in API mode' do
|
|
313
|
+
before do
|
|
314
|
+
allow(Doorkeeper.configuration).to receive(:api_only).and_return(true)
|
|
315
|
+
get :new, params: { client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri }
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
it 'should render success' do
|
|
319
|
+
expect(response).to be_successful
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it "sets status to pre-authorization" do
|
|
323
|
+
expect(json_response["status"]).to eq(I18n.t('doorkeeper.pre_authorization.status'))
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it "sets correct values" do
|
|
327
|
+
expect(json_response['client_id']).to eq(client.uid)
|
|
328
|
+
expect(json_response['redirect_uri']).to eq(client.redirect_uri)
|
|
329
|
+
expect(json_response['state']).to be_nil
|
|
330
|
+
expect(json_response['response_type']).to eq('token')
|
|
331
|
+
expect(json_response['scope']).to eq('')
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
describe 'GET #new in API mode with skip_authorization true' do
|
|
336
|
+
before do
|
|
337
|
+
allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { true })
|
|
338
|
+
allow(Doorkeeper.configuration).to receive(:api_only).and_return(true)
|
|
339
|
+
|
|
340
|
+
get :new, params: { client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri }
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it 'should render success' do
|
|
344
|
+
expect(response).to be_successful
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
it 'should issue a token' do
|
|
348
|
+
expect(Doorkeeper::AccessToken.count).to be 1
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
it "sets status to redirect" do
|
|
352
|
+
expect(JSON.parse(response.body)["status"]).to eq("redirect")
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it "sets redirect_uri to correct value" do
|
|
356
|
+
redirect_uri = JSON.parse(response.body)["redirect_uri"]
|
|
357
|
+
expect(redirect_uri).to_not be_nil
|
|
358
|
+
expect(redirect_uri.match(/token_type=(\w+)&?/)[1]).to eq "Bearer"
|
|
359
|
+
expect(redirect_uri.match(/expires_in=(\d+)&?/)[1].to_i).to eq 1234
|
|
360
|
+
expect(
|
|
361
|
+
redirect_uri.match(/access_token=([a-f0-9]+)&?/)[1]
|
|
362
|
+
).to eq Doorkeeper::AccessToken.first.token
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
it "issues the token for the current client" do
|
|
366
|
+
expect(Doorkeeper::AccessToken.first.application_id).to eq(client.id)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
it "issues the token for the current resource owner" do
|
|
370
|
+
expect(Doorkeeper::AccessToken.first.resource_owner_id).to eq(user.id)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
188
374
|
describe 'GET #new with errors' do
|
|
189
375
|
before do
|
|
190
376
|
default_scopes_exist :public
|
|
191
|
-
get :new, an_invalid: 'request'
|
|
377
|
+
get :new, params: { an_invalid: 'request' }
|
|
192
378
|
end
|
|
193
379
|
|
|
194
380
|
it 'does not redirect' do
|
|
@@ -200,4 +386,95 @@ describe Doorkeeper::AuthorizationsController, 'implicit grant flow' do
|
|
|
200
386
|
expect(Doorkeeper::AccessToken.count).to eq 0
|
|
201
387
|
end
|
|
202
388
|
end
|
|
389
|
+
|
|
390
|
+
describe 'GET #new in API mode with errors' do
|
|
391
|
+
let(:response_json_body) { JSON.parse(response.body) }
|
|
392
|
+
|
|
393
|
+
before do
|
|
394
|
+
default_scopes_exist :public
|
|
395
|
+
allow(Doorkeeper.configuration).to receive(:api_only).and_return(true)
|
|
396
|
+
get :new, params: { an_invalid: 'request' }
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
it 'should render bad request' do
|
|
400
|
+
expect(response).to have_http_status(:bad_request)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
it 'includes error in body' do
|
|
404
|
+
expect(response_json_body['error']).to eq('unsupported_response_type')
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
it 'includes error description in body' do
|
|
408
|
+
expect(response_json_body['error_description']).to eq(translated_error_message(:unsupported_response_type))
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
it 'does not issue any token' do
|
|
412
|
+
expect(Doorkeeper::AccessGrant.count).to eq 0
|
|
413
|
+
expect(Doorkeeper::AccessToken.count).to eq 0
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
describe 'GET #new with callbacks' do
|
|
418
|
+
after do
|
|
419
|
+
client.update_attribute :redirect_uri, 'urn:ietf:wg:oauth:2.0:oob'
|
|
420
|
+
get :new, params: { client_id: client.uid, response_type: 'token', redirect_uri: client.redirect_uri }
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
describe 'when authorizing' do
|
|
424
|
+
before do
|
|
425
|
+
allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { true })
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
it 'should call :before_successful_authorization callback' do
|
|
429
|
+
expect(Doorkeeper.configuration).to receive_message_chain(:before_successful_authorization, :call).with(instance_of(described_class))
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
it 'should call :after_successful_authorization callback' do
|
|
433
|
+
expect(Doorkeeper.configuration).to receive_message_chain(:after_successful_authorization, :call).with(instance_of(described_class))
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
describe 'when not authorizing' do
|
|
438
|
+
before do
|
|
439
|
+
allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { false })
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
it 'should not call :before_successful_authorization callback' do
|
|
443
|
+
expect(Doorkeeper.configuration).not_to receive(:before_successful_authorization)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it 'should not call :after_successful_authorization callback' do
|
|
447
|
+
expect(Doorkeeper.configuration).not_to receive(:after_successful_authorization)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
describe 'when not authorizing in api mode' do
|
|
452
|
+
before do
|
|
453
|
+
allow(Doorkeeper.configuration).to receive(:skip_authorization).and_return(proc { false })
|
|
454
|
+
allow(Doorkeeper.configuration).to receive(:api_only).and_return(true)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it 'should not call :before_successful_authorization callback' do
|
|
458
|
+
expect(Doorkeeper.configuration).not_to receive(:before_successful_authorization)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
it 'should not call :after_successful_authorization callback' do
|
|
462
|
+
expect(Doorkeeper.configuration).not_to receive(:after_successful_authorization)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
describe 'authorize response memoization' do
|
|
468
|
+
it 'memoizes the result of the authorization' do
|
|
469
|
+
strategy = double(:strategy, authorize: true)
|
|
470
|
+
expect(strategy).to receive(:authorize).once
|
|
471
|
+
allow(controller).to receive(:strategy) { strategy }
|
|
472
|
+
allow(controller).to receive(:create) do
|
|
473
|
+
2.times { controller.send :authorize_response }
|
|
474
|
+
controller.render json: {}, status: :ok
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
post :create
|
|
478
|
+
end
|
|
479
|
+
end
|
|
203
480
|
end
|