gds-sso 0.4.3 → 0.5.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/Gemfile +1 -1
- data/README.md +13 -2
- data/Rakefile +16 -3
- data/app/controllers/authentications_controller.rb +1 -1
- data/lib/gds-sso/omniauth_strategy.rb +2 -2
- data/lib/gds-sso/version.rb +1 -1
- data/lib/gds-sso.rb +2 -1
- data/spec/fixtures/integration/signonotron2.sql +9 -0
- data/spec/fixtures/integration/signonotron2_database.yml +8 -0
- data/spec/internal/app/controllers/application_controller.rb +3 -0
- data/spec/internal/app/controllers/example_controller.rb +12 -0
- data/spec/internal/app/models/user.rb +18 -0
- data/spec/internal/config/database.yml +3 -0
- data/spec/internal/config/initializers/gds-sso.rb +8 -0
- data/spec/internal/config/routes.rb +4 -0
- data/spec/internal/db/schema.rb +3 -0
- data/spec/internal/log/test.log +2518 -0
- data/{test/test_http_strategy.rb → spec/internal/public/favicon.ico} +0 -0
- data/spec/requests/end_to_end_spec.rb +77 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/signonotron2_integration_helpers.rb +35 -0
- data/spec/tasks/signonotron_tasks.rake +41 -0
- data/test/{test_gds_sso_strategy.rb → gds_sso_strategy_test.rb} +0 -0
- data/test/{test_omniauth_strategy.rb → omniauth_strategy_test.rb} +6 -8
- data/test/{test_user.rb → user_test.rb} +0 -0
- metadata +92 -38
- data/lib/gds-sso/routes.rb +0 -20
File without changes
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Integration of client using GDS-SSO with signonotron" do
|
4
|
+
include Signonotron2IntegrationHelpers
|
5
|
+
|
6
|
+
before :all do
|
7
|
+
wait_for_signonotron_to_start
|
8
|
+
end
|
9
|
+
before :each do
|
10
|
+
@client_host = 'www.example-client.com'
|
11
|
+
Capybara.current_driver = :mechanize
|
12
|
+
Capybara::Mechanize.local_hosts << @client_host
|
13
|
+
|
14
|
+
load_signonotron_fixture
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "Web client accesses" do
|
18
|
+
before :each do
|
19
|
+
page.driver.header 'accept', 'text/html'
|
20
|
+
end
|
21
|
+
|
22
|
+
specify "a non-restricted page can be accessed without authentication" do
|
23
|
+
visit "http://#{@client_host}/"
|
24
|
+
page.should have_content('jabberwocky')
|
25
|
+
end
|
26
|
+
|
27
|
+
specify "first access to a restricted page requires authentication and application approval" do
|
28
|
+
visit "http://#{@client_host}/restricted"
|
29
|
+
page.should have_content("Sign in")
|
30
|
+
fill_in "Email", :with => "test@example-client.com"
|
31
|
+
fill_in "Password", :with => "q1w2e3r4t5y6u7i8o9p0"
|
32
|
+
click_on "Sign in"
|
33
|
+
|
34
|
+
click_on "Authorize"
|
35
|
+
|
36
|
+
page.should have_content('restricted kablooie')
|
37
|
+
end
|
38
|
+
|
39
|
+
specify "access to a restricted page for an approved application requires only authentication" do
|
40
|
+
# First we login to authorise the app
|
41
|
+
visit "http://#{@client_host}/restricted"
|
42
|
+
fill_in "Email", :with => "test@example-client.com"
|
43
|
+
fill_in "Password", :with => "q1w2e3r4t5y6u7i8o9p0"
|
44
|
+
click_on "Sign in"
|
45
|
+
click_on "Authorize"
|
46
|
+
|
47
|
+
# At this point the app should be authorised, we reset the session to simulate a new browser visit.
|
48
|
+
reset_session!
|
49
|
+
page.driver.header 'accept', 'text/html'
|
50
|
+
|
51
|
+
visit "http://#{@client_host}/restricted"
|
52
|
+
page.should have_content("Sign in")
|
53
|
+
fill_in "Email", :with => "test@example-client.com"
|
54
|
+
fill_in "Password", :with => "q1w2e3r4t5y6u7i8o9p0"
|
55
|
+
click_on "Sign in"
|
56
|
+
|
57
|
+
page.should have_content('restricted kablooie')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "API client accesses" do
|
62
|
+
before :each do
|
63
|
+
page.driver.header 'accept', 'application/json'
|
64
|
+
end
|
65
|
+
|
66
|
+
specify "access to a restricted page for an api client requires basic auth" do
|
67
|
+
visit "http://#{@client_host}/restricted"
|
68
|
+
page.driver.response.status.should == 401
|
69
|
+
page.driver.response.headers["WWW-Authenticate"].should == 'Basic realm="API Access"'
|
70
|
+
|
71
|
+
page.driver.browser.authorize 'test_api_user', 'api_user_password'
|
72
|
+
visit "http://#{@client_host}/restricted"
|
73
|
+
|
74
|
+
page.should have_content('restricted kablooie')
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
# Yes, we really do want to turn off the test environment check here.
|
5
|
+
# Bad things happen if we don't ;-)
|
6
|
+
ENV['GDS_SSO_STRATEGY'] = 'real'
|
7
|
+
|
8
|
+
Bundler.require :default, :development
|
9
|
+
|
10
|
+
require 'capybara/rspec'
|
11
|
+
|
12
|
+
Combustion.initialize! :action_controller
|
13
|
+
|
14
|
+
require 'rspec/rails'
|
15
|
+
require 'capybara/rails'
|
16
|
+
|
17
|
+
require 'mechanize'
|
18
|
+
require 'capybara/mechanize'
|
19
|
+
|
20
|
+
include Warden::Test::Helpers
|
21
|
+
|
22
|
+
Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
module Signonotron2IntegrationHelpers
|
4
|
+
def wait_for_signonotron_to_start
|
5
|
+
retries = 0
|
6
|
+
url = GDS::SSO::Config.oauth_root_url
|
7
|
+
puts "Waiting for signonotron to start at #{url}"
|
8
|
+
while ! signonotron_started?(url)
|
9
|
+
print '.'
|
10
|
+
if retries > 10
|
11
|
+
raise "Signonotron is not running at #{url}. Please start with 'bundle exec rake signonotron:start'. Under jenkins this should have been run automatically"
|
12
|
+
end
|
13
|
+
retries += 1
|
14
|
+
sleep 1
|
15
|
+
end
|
16
|
+
puts "Signonotron is now running at #{url}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def signonotron_started?(url)
|
20
|
+
uri = URI.parse(url)
|
21
|
+
conn = Net::HTTP.start(uri.host, uri.port)
|
22
|
+
true
|
23
|
+
rescue Errno::ECONNREFUSED
|
24
|
+
false
|
25
|
+
ensure
|
26
|
+
conn.try(:finish)
|
27
|
+
end
|
28
|
+
|
29
|
+
def load_signonotron_fixture
|
30
|
+
fixtures_path = Pathname.new(File.join(File.dirname(__FILE__), '../fixtures/integration'))
|
31
|
+
db = YAML.load_file(fixtures_path + 'signonotron2_database.yml')['test']
|
32
|
+
cmd = "mysql -u#{db['username']} -p#{db['password']} #{db['database']} < #{fixtures_path + 'signonotron2.sql'}"
|
33
|
+
system cmd or raise "Error loading signonotron fixture"
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
namespace :signonotron do
|
2
|
+
desc "Start signonotron (for integration tests)"
|
3
|
+
task :start => :stop do
|
4
|
+
gem_root = Pathname.new(File.dirname(__FILE__)) + '..' + '..'
|
5
|
+
FileUtils.mkdir_p(gem_root + 'tmp')
|
6
|
+
Dir.chdir gem_root + 'tmp' do
|
7
|
+
if File.exist? "signonotron2"
|
8
|
+
Dir.chdir "signonotron2" do
|
9
|
+
puts `git clean -fdx`
|
10
|
+
puts `git fetch origin`
|
11
|
+
puts `git reset --hard origin/master`
|
12
|
+
end
|
13
|
+
else
|
14
|
+
puts `git clone git@github.com:alphagov/signonotron2`
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Dir.chdir gem_root + 'tmp' + 'signonotron2' do
|
19
|
+
env_stuff = '/usr/bin/env -u BUNDLE_GEMFILE -u BUNDLE_BIN_PATH -u RUBYOPT -u GEM_HOME -u GEM_PATH RAILS_ENV=test'
|
20
|
+
puts `#{env_stuff} bundle install --path=#{gem_root + 'tmp' + 'signonotron2_bundle'}`
|
21
|
+
FileUtils.cp gem_root.join('spec', 'fixtures', 'integration', 'signonotron2_database.yml'), File.join('config', 'database.yml')
|
22
|
+
puts `#{env_stuff} bundle exec rake db:drop db:create db:schema:load`
|
23
|
+
|
24
|
+
puts "Starting signonotron instance in the background"
|
25
|
+
fork do
|
26
|
+
Process.daemon(true)
|
27
|
+
exec "#{env_stuff} bundle exec rails s -p 4567"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
desc "Stop running signonotron (for integration tests)"
|
33
|
+
task :stop do
|
34
|
+
pid_output = `lsof -Fp -i :4567`.chomp
|
35
|
+
if pid_output =~ /\Ap(\d+)\z/
|
36
|
+
puts "Stopping running instance of Signonotron (pid #{$1})"
|
37
|
+
Process.kill(:INT, $1.to_i)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
File without changes
|
@@ -8,14 +8,12 @@ class TestOmniAuthStrategy < Test::Unit::TestCase
|
|
8
8
|
@app = stub("app")
|
9
9
|
@strategy = OmniAuth::Strategies::Gds.new(@app, :gds, 'client_id', 'client_secret')
|
10
10
|
@strategy.stubs(:fetch_user_data).returns({
|
11
|
-
'
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
'twitter' => 'fidothe'
|
18
|
-
}
|
11
|
+
'uid' => 'abcde',
|
12
|
+
'version' => 1,
|
13
|
+
'name' => 'Matt Patterson',
|
14
|
+
'email' => 'matt@alphagov.co.uk',
|
15
|
+
'github' => 'fidothe',
|
16
|
+
'twitter' => 'fidothe'
|
19
17
|
}.to_json)
|
20
18
|
end
|
21
19
|
|
File without changes
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: gds-sso
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 0.5.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Matt Patterson
|
@@ -11,10 +11,11 @@ autorequire:
|
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
13
|
|
14
|
-
date: 2012-
|
14
|
+
date: 2012-04-20 00:00:00 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: rails
|
18
|
+
prerelease: false
|
18
19
|
requirement: &id001 !ruby/object:Gem::Requirement
|
19
20
|
none: false
|
20
21
|
requirements:
|
@@ -22,10 +23,10 @@ dependencies:
|
|
22
23
|
- !ruby/object:Gem::Version
|
23
24
|
version: 3.0.0
|
24
25
|
type: :runtime
|
25
|
-
prerelease: false
|
26
26
|
version_requirements: *id001
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: warden
|
29
|
+
prerelease: false
|
29
30
|
requirement: &id002 !ruby/object:Gem::Requirement
|
30
31
|
none: false
|
31
32
|
requirements:
|
@@ -33,43 +34,43 @@ dependencies:
|
|
33
34
|
- !ruby/object:Gem::Version
|
34
35
|
version: 1.0.6
|
35
36
|
type: :runtime
|
36
|
-
prerelease: false
|
37
37
|
version_requirements: *id002
|
38
38
|
- !ruby/object:Gem::Dependency
|
39
39
|
name: oauth2
|
40
|
+
prerelease: false
|
40
41
|
requirement: &id003 !ruby/object:Gem::Requirement
|
41
42
|
none: false
|
42
43
|
requirements:
|
43
44
|
- - "="
|
44
45
|
- !ruby/object:Gem::Version
|
45
|
-
version: 0.
|
46
|
+
version: 0.5.2
|
46
47
|
type: :runtime
|
47
|
-
prerelease: false
|
48
48
|
version_requirements: *id003
|
49
49
|
- !ruby/object:Gem::Dependency
|
50
50
|
name: oa-oauth
|
51
|
+
prerelease: false
|
51
52
|
requirement: &id004 !ruby/object:Gem::Requirement
|
52
53
|
none: false
|
53
54
|
requirements:
|
54
|
-
- -
|
55
|
+
- - ~>
|
55
56
|
- !ruby/object:Gem::Version
|
56
|
-
version: 0.2
|
57
|
+
version: 0.3.2
|
57
58
|
type: :runtime
|
58
|
-
prerelease: false
|
59
59
|
version_requirements: *id004
|
60
60
|
- !ruby/object:Gem::Dependency
|
61
61
|
name: oa-core
|
62
|
+
prerelease: false
|
62
63
|
requirement: &id005 !ruby/object:Gem::Requirement
|
63
64
|
none: false
|
64
65
|
requirements:
|
65
|
-
- -
|
66
|
+
- - ~>
|
66
67
|
- !ruby/object:Gem::Version
|
67
|
-
version: 0.2
|
68
|
+
version: 0.3.2
|
68
69
|
type: :runtime
|
69
|
-
prerelease: false
|
70
70
|
version_requirements: *id005
|
71
71
|
- !ruby/object:Gem::Dependency
|
72
72
|
name: rack-accept
|
73
|
+
prerelease: false
|
73
74
|
requirement: &id006 !ruby/object:Gem::Requirement
|
74
75
|
none: false
|
75
76
|
requirements:
|
@@ -77,21 +78,21 @@ dependencies:
|
|
77
78
|
- !ruby/object:Gem::Version
|
78
79
|
version: 0.4.4
|
79
80
|
type: :runtime
|
80
|
-
prerelease: false
|
81
81
|
version_requirements: *id006
|
82
82
|
- !ruby/object:Gem::Dependency
|
83
|
-
name:
|
83
|
+
name: rack
|
84
|
+
prerelease: false
|
84
85
|
requirement: &id007 !ruby/object:Gem::Requirement
|
85
86
|
none: false
|
86
87
|
requirements:
|
87
|
-
- - "
|
88
|
+
- - "="
|
88
89
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
90
|
+
version: 1.3.5
|
90
91
|
type: :runtime
|
91
|
-
prerelease: false
|
92
92
|
version_requirements: *id007
|
93
93
|
- !ruby/object:Gem::Dependency
|
94
94
|
name: rake
|
95
|
+
prerelease: false
|
95
96
|
requirement: &id008 !ruby/object:Gem::Requirement
|
96
97
|
none: false
|
97
98
|
requirements:
|
@@ -99,10 +100,10 @@ dependencies:
|
|
99
100
|
- !ruby/object:Gem::Version
|
100
101
|
version: 0.9.2
|
101
102
|
type: :development
|
102
|
-
prerelease: false
|
103
103
|
version_requirements: *id008
|
104
104
|
- !ruby/object:Gem::Dependency
|
105
105
|
name: mocha
|
106
|
+
prerelease: false
|
106
107
|
requirement: &id009 !ruby/object:Gem::Requirement
|
107
108
|
none: false
|
108
109
|
requirements:
|
@@ -110,19 +111,51 @@ dependencies:
|
|
110
111
|
- !ruby/object:Gem::Version
|
111
112
|
version: 0.9.0
|
112
113
|
type: :development
|
113
|
-
prerelease: false
|
114
114
|
version_requirements: *id009
|
115
115
|
- !ruby/object:Gem::Dependency
|
116
116
|
name: capybara
|
117
|
+
prerelease: false
|
117
118
|
requirement: &id010 !ruby/object:Gem::Requirement
|
118
119
|
none: false
|
119
120
|
requirements:
|
120
|
-
- -
|
121
|
+
- - ~>
|
121
122
|
- !ruby/object:Gem::Version
|
122
|
-
version:
|
123
|
+
version: 1.1.2
|
123
124
|
type: :development
|
124
|
-
prerelease: false
|
125
125
|
version_requirements: *id010
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rspec-rails
|
128
|
+
prerelease: false
|
129
|
+
requirement: &id011 !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ~>
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: 2.9.0
|
135
|
+
type: :development
|
136
|
+
version_requirements: *id011
|
137
|
+
- !ruby/object:Gem::Dependency
|
138
|
+
name: capybara-mechanize
|
139
|
+
prerelease: false
|
140
|
+
requirement: &id012 !ruby/object:Gem::Requirement
|
141
|
+
none: false
|
142
|
+
requirements:
|
143
|
+
- - ~>
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 0.3.0
|
146
|
+
type: :development
|
147
|
+
version_requirements: *id012
|
148
|
+
- !ruby/object:Gem::Dependency
|
149
|
+
name: combustion
|
150
|
+
prerelease: false
|
151
|
+
requirement: &id013 !ruby/object:Gem::Requirement
|
152
|
+
none: false
|
153
|
+
requirements:
|
154
|
+
- - ~>
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: 0.3.1
|
157
|
+
type: :development
|
158
|
+
version_requirements: *id013
|
126
159
|
description: Client for GDS' OAuth 2-based SSO
|
127
160
|
email:
|
128
161
|
- matt@constituentparts.com
|
@@ -141,7 +174,6 @@ files:
|
|
141
174
|
- lib/gds-sso/controller_methods.rb
|
142
175
|
- lib/gds-sso/failure_app.rb
|
143
176
|
- lib/gds-sso/omniauth_strategy.rb
|
144
|
-
- lib/gds-sso/routes.rb
|
145
177
|
- lib/gds-sso/user.rb
|
146
178
|
- lib/gds-sso/version.rb
|
147
179
|
- lib/gds-sso/warden_config.rb
|
@@ -149,11 +181,25 @@ files:
|
|
149
181
|
- README.md
|
150
182
|
- Gemfile
|
151
183
|
- Rakefile
|
152
|
-
- test/
|
184
|
+
- test/gds_sso_strategy_test.rb
|
185
|
+
- test/omniauth_strategy_test.rb
|
153
186
|
- test/test_helper.rb
|
154
|
-
- test/
|
155
|
-
-
|
156
|
-
-
|
187
|
+
- test/user_test.rb
|
188
|
+
- spec/fixtures/integration/signonotron2.sql
|
189
|
+
- spec/fixtures/integration/signonotron2_database.yml
|
190
|
+
- spec/internal/app/controllers/application_controller.rb
|
191
|
+
- spec/internal/app/controllers/example_controller.rb
|
192
|
+
- spec/internal/app/models/user.rb
|
193
|
+
- spec/internal/config/database.yml
|
194
|
+
- spec/internal/config/initializers/gds-sso.rb
|
195
|
+
- spec/internal/config/routes.rb
|
196
|
+
- spec/internal/db/schema.rb
|
197
|
+
- spec/internal/log/test.log
|
198
|
+
- spec/internal/public/favicon.ico
|
199
|
+
- spec/requests/end_to_end_spec.rb
|
200
|
+
- spec/spec_helper.rb
|
201
|
+
- spec/support/signonotron2_integration_helpers.rb
|
202
|
+
- spec/tasks/signonotron_tasks.rake
|
157
203
|
homepage: https://github.com/alphagov/gds-sso
|
158
204
|
licenses: []
|
159
205
|
|
@@ -167,29 +213,37 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
167
213
|
requirements:
|
168
214
|
- - ">="
|
169
215
|
- !ruby/object:Gem::Version
|
170
|
-
hash: -535713269746465567
|
171
|
-
segments:
|
172
|
-
- 0
|
173
216
|
version: "0"
|
174
217
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
175
218
|
none: false
|
176
219
|
requirements:
|
177
220
|
- - ">="
|
178
221
|
- !ruby/object:Gem::Version
|
179
|
-
hash: -535713269746465567
|
180
|
-
segments:
|
181
|
-
- 0
|
182
222
|
version: "0"
|
183
223
|
requirements: []
|
184
224
|
|
185
225
|
rubyforge_project: gds-sso
|
186
|
-
rubygems_version: 1.8.
|
226
|
+
rubygems_version: 1.8.12
|
187
227
|
signing_key:
|
188
228
|
specification_version: 3
|
189
229
|
summary: Client for GDS' OAuth 2-based SSO
|
190
230
|
test_files:
|
191
|
-
- test/
|
231
|
+
- test/gds_sso_strategy_test.rb
|
232
|
+
- test/omniauth_strategy_test.rb
|
192
233
|
- test/test_helper.rb
|
193
|
-
- test/
|
194
|
-
-
|
195
|
-
-
|
234
|
+
- test/user_test.rb
|
235
|
+
- spec/fixtures/integration/signonotron2.sql
|
236
|
+
- spec/fixtures/integration/signonotron2_database.yml
|
237
|
+
- spec/internal/app/controllers/application_controller.rb
|
238
|
+
- spec/internal/app/controllers/example_controller.rb
|
239
|
+
- spec/internal/app/models/user.rb
|
240
|
+
- spec/internal/config/database.yml
|
241
|
+
- spec/internal/config/initializers/gds-sso.rb
|
242
|
+
- spec/internal/config/routes.rb
|
243
|
+
- spec/internal/db/schema.rb
|
244
|
+
- spec/internal/log/test.log
|
245
|
+
- spec/internal/public/favicon.ico
|
246
|
+
- spec/requests/end_to_end_spec.rb
|
247
|
+
- spec/spec_helper.rb
|
248
|
+
- spec/support/signonotron2_integration_helpers.rb
|
249
|
+
- spec/tasks/signonotron_tasks.rake
|
data/lib/gds-sso/routes.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
module ActionDispatch::Routing
|
2
|
-
class Mapper
|
3
|
-
# Allow you to add authentication request from the router:
|
4
|
-
#
|
5
|
-
# authenticate(:user) do
|
6
|
-
# resources :post
|
7
|
-
# end
|
8
|
-
#
|
9
|
-
# Stolen from devise
|
10
|
-
def authenticate(scope)
|
11
|
-
constraint = lambda do |request|
|
12
|
-
request.env["warden"].authenticate!(:scope => scope)
|
13
|
-
end
|
14
|
-
|
15
|
-
constraints(constraint) do
|
16
|
-
yield
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|