forest_liana 5.3.0 → 6.0.0.pre.beta.2

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/forest_liana/actions_controller.rb +0 -82
  3. data/app/controllers/forest_liana/application_controller.rb +1 -7
  4. data/app/controllers/forest_liana/authentication_controller.rb +122 -0
  5. data/app/controllers/forest_liana/base_controller.rb +4 -0
  6. data/app/controllers/forest_liana/router.rb +2 -2
  7. data/app/controllers/forest_liana/sessions_controller.rb +1 -1
  8. data/app/controllers/forest_liana/stats_controller.rb +5 -5
  9. data/app/helpers/forest_liana/adapter_helper.rb +1 -1
  10. data/app/models/forest_liana/model/action.rb +1 -2
  11. data/app/serializers/forest_liana/schema_serializer.rb +2 -2
  12. data/app/services/forest_liana/apimap_sorter.rb +1 -2
  13. data/app/services/forest_liana/authentication.rb +59 -0
  14. data/app/services/forest_liana/authorization_getter.rb +12 -20
  15. data/app/services/forest_liana/forest_api_requester.rb +14 -5
  16. data/app/services/forest_liana/ip_whitelist_checker.rb +1 -1
  17. data/app/services/forest_liana/login_handler.rb +3 -11
  18. data/app/services/forest_liana/oidc_client_manager.rb +34 -0
  19. data/app/services/forest_liana/oidc_configuration_retriever.rb +12 -0
  20. data/app/services/forest_liana/oidc_dynamic_client_registrator.rb +67 -0
  21. data/app/services/forest_liana/permissions_checker.rb +1 -1
  22. data/app/services/forest_liana/query_stat_getter.rb +5 -5
  23. data/app/services/forest_liana/resources_getter.rb +3 -3
  24. data/app/services/forest_liana/token.rb +27 -0
  25. data/config/initializers/error-messages.rb +20 -0
  26. data/config/routes.rb +5 -2
  27. data/lib/forest_liana.rb +1 -0
  28. data/lib/forest_liana/bootstrapper.rb +1 -20
  29. data/lib/forest_liana/collection.rb +2 -2
  30. data/lib/forest_liana/engine.rb +9 -0
  31. data/lib/forest_liana/json_printer.rb +1 -1
  32. data/lib/forest_liana/schema_file_updater.rb +0 -1
  33. data/lib/forest_liana/version.rb +1 -1
  34. data/spec/dummy/config/initializers/forest_liana.rb +1 -0
  35. data/spec/requests/authentications_spec.rb +107 -0
  36. data/spec/requests/sessions_spec.rb +55 -0
  37. data/spec/services/forest_liana/apimap_sorter_spec.rb +4 -6
  38. metadata +57 -9
  39. data/app/helpers/forest_liana/is_same_data_structure_helper.rb +0 -44
  40. data/spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb +0 -87
  41. data/spec/requests/actions_controller_spec.rb +0 -136
@@ -0,0 +1,55 @@
1
+ require 'rails_helper'
2
+ require 'openid_connect'
3
+ require 'json'
4
+
5
+ RSpec.describe "Authentications", type: :request do
6
+ before() do
7
+ allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
8
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
9
+ allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
10
+
11
+ body = '{"data":{"id":"654","type":"users","attributes":{"email":"user@email.com","first_name":"FirstName","last_name":"LastName","teams":["Operations"]}},"relationships":{"renderings":{"data":[{"id":1,"type":"renderings"}]}}}'
12
+ allow(ForestLiana::ForestApiRequester).to receive(:get).with(
13
+ "/liana/v2/renderings/42/authorization", { :headers => { "forest-token" => "google-access-token" }, :query=> {} }
14
+ ).and_return(
15
+ instance_double(HTTParty::Response, :body => body, :code => 200)
16
+ )
17
+ end
18
+
19
+ after() do
20
+ Rails.cache.delete(URI.join(ForestLiana.application_url, ForestLiana::Engine.routes.url_helpers.authentication_callback_path).to_s)
21
+ end
22
+
23
+ headers = {
24
+ 'Accept' => 'application/json',
25
+ 'Content-Type' => 'application/json',
26
+ }
27
+
28
+ describe "POST /forest/sessions-google" do
29
+ before() do
30
+ post ForestLiana::Engine.routes.url_helpers.sessions_google_path, { :renderingId => 42, :forestToken => "google-access-token" }, :headers => headers
31
+ end
32
+
33
+ it "should respond with a 200 code" do
34
+ expect(response).to have_http_status(200)
35
+ end
36
+
37
+ it "should return a valid authentication token" do
38
+ response_body = JSON.parse(response.body, :symbolize_names => true)
39
+ expect(response_body).to have_key(:token)
40
+
41
+ token = response_body[:token]
42
+ decoded = JWT.decode(token, ForestLiana.auth_secret, true, { algorithm: 'HS256' })[0]
43
+
44
+ expected_token_data = {
45
+ "id" => '654',
46
+ "email" => 'user@email.com',
47
+ "first_name" => 'FirstName',
48
+ "last_name" => 'LastName',
49
+ "rendering_id" => "42",
50
+ "team" => 'Operations'
51
+ }
52
+ expect(decoded).to include(expected_token_data);
53
+ end
54
+ end
55
+ end
@@ -75,8 +75,7 @@ module ForestLiana
75
75
  type: 'File',
76
76
  field: 'File'
77
77
  }],
78
- http_method: nil,
79
- hooks: nil,
78
+ http_method: nil
80
79
  }
81
80
  }, {
82
81
  attributes: {
@@ -96,8 +95,7 @@ module ForestLiana
96
95
  download: nil,
97
96
  endpoint: nil,
98
97
  redirect: nil,
99
- 'http_method': nil,
100
- hooks: nil,
98
+ 'http_method': nil
101
99
  }
102
100
  }]
103
101
  }
@@ -150,8 +148,8 @@ module ForestLiana
150
148
  end
151
149
 
152
150
  it 'should sort the included actions and segments objects attributes values' do
153
- expect(apimap_sorted['included'][0]['attributes'].keys).to eq(['name', 'endpoint', 'http_method', 'redirect', 'download', 'hooks'])
154
- expect(apimap_sorted['included'][1]['attributes'].keys).to eq(['name', 'http_method', 'fields', 'hooks'])
151
+ expect(apimap_sorted['included'][0]['attributes'].keys).to eq(['name', 'endpoint', 'http_method', 'redirect', 'download'])
152
+ expect(apimap_sorted['included'][1]['attributes'].keys).to eq(['name', 'http_method', 'fields'])
155
153
  expect(apimap_sorted['included'][2]['attributes'].keys).to eq(['name'])
156
154
  expect(apimap_sorted['included'][3]['attributes'].keys).to eq(['name'])
157
155
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_liana
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.3.0
4
+ version: 6.0.0.pre.beta.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Munda
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-07 00:00:00.000000000 Z
11
+ date: 2020-12-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -178,6 +178,48 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: json
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: json-jwt
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :runtime
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: openid_connect
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
181
223
  description: Forest is a modern admin interface that works on all major web frameworks.
182
224
  forest_liana is the gem that makes Forest admin work on any Rails application (Rails
183
225
  >= 4.0).
@@ -197,6 +239,7 @@ files:
197
239
  - app/controllers/forest_liana/apimaps_controller.rb
198
240
  - app/controllers/forest_liana/application_controller.rb
199
241
  - app/controllers/forest_liana/associations_controller.rb
242
+ - app/controllers/forest_liana/authentication_controller.rb
200
243
  - app/controllers/forest_liana/base_controller.rb
201
244
  - app/controllers/forest_liana/devise_controller.rb
202
245
  - app/controllers/forest_liana/intercom_controller.rb
@@ -211,7 +254,6 @@ files:
211
254
  - app/helpers/forest_liana/adapter_helper.rb
212
255
  - app/helpers/forest_liana/application_helper.rb
213
256
  - app/helpers/forest_liana/decoration_helper.rb
214
- - app/helpers/forest_liana/is_same_data_structure_helper.rb
215
257
  - app/helpers/forest_liana/query_helper.rb
216
258
  - app/helpers/forest_liana/schema_helper.rb
217
259
  - app/models/forest_liana/model/action.rb
@@ -231,6 +273,7 @@ files:
231
273
  - app/serializers/forest_liana/stripe_payment_serializer.rb
232
274
  - app/serializers/forest_liana/stripe_subscription_serializer.rb
233
275
  - app/services/forest_liana/apimap_sorter.rb
276
+ - app/services/forest_liana/authentication.rb
234
277
  - app/services/forest_liana/authorization_getter.rb
235
278
  - app/services/forest_liana/base_getter.rb
236
279
  - app/services/forest_liana/belongs_to_updater.rb
@@ -252,6 +295,9 @@ files:
252
295
  - app/services/forest_liana/login_handler.rb
253
296
  - app/services/forest_liana/mixpanel_last_events_getter.rb
254
297
  - app/services/forest_liana/objective_stat_getter.rb
298
+ - app/services/forest_liana/oidc_client_manager.rb
299
+ - app/services/forest_liana/oidc_configuration_retriever.rb
300
+ - app/services/forest_liana/oidc_dynamic_client_registrator.rb
255
301
  - app/services/forest_liana/operator_date_interval_parser.rb
256
302
  - app/services/forest_liana/permissions_checker.rb
257
303
  - app/services/forest_liana/permissions_getter.rb
@@ -276,11 +322,13 @@ files:
276
322
  - app/services/forest_liana/stripe_sources_getter.rb
277
323
  - app/services/forest_liana/stripe_subscription_getter.rb
278
324
  - app/services/forest_liana/stripe_subscriptions_getter.rb
325
+ - app/services/forest_liana/token.rb
279
326
  - app/services/forest_liana/two_factor_registration_confirmer.rb
280
327
  - app/services/forest_liana/user_secret_creator.rb
281
328
  - app/services/forest_liana/value_stat_getter.rb
282
329
  - app/views/layouts/forest_liana/application.html.erb
283
330
  - config/initializers/arel-helpers.rb
331
+ - config/initializers/error-messages.rb
284
332
  - config/initializers/errors.rb
285
333
  - config/initializers/logger.rb
286
334
  - config/initializers/time_formats.rb
@@ -337,12 +385,12 @@ files:
337
385
  - spec/dummy/db/migrate/20190716130830_add_age_to_tree.rb
338
386
  - spec/dummy/db/migrate/20190716135241_add_type_to_user.rb
339
387
  - spec/dummy/db/schema.rb
340
- - spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb
341
388
  - spec/helpers/forest_liana/query_helper_spec.rb
342
389
  - spec/helpers/forest_liana/schema_helper_spec.rb
343
390
  - spec/rails_helper.rb
344
- - spec/requests/actions_controller_spec.rb
391
+ - spec/requests/authentications_spec.rb
345
392
  - spec/requests/resources_spec.rb
393
+ - spec/requests/sessions_spec.rb
346
394
  - spec/services/forest_liana/apimap_sorter_spec.rb
347
395
  - spec/services/forest_liana/filters_parser_spec.rb
348
396
  - spec/services/forest_liana/ip_whitelist_checker_spec.rb
@@ -454,9 +502,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
454
502
  version: '0'
455
503
  required_rubygems_version: !ruby/object:Gem::Requirement
456
504
  requirements:
457
- - - ">="
505
+ - - ">"
458
506
  - !ruby/object:Gem::Version
459
- version: '0'
507
+ version: 1.3.1
460
508
  requirements: []
461
509
  rubygems_version: 3.0.8
462
510
  signing_key:
@@ -559,7 +607,8 @@ test_files:
559
607
  - spec/services/forest_liana/apimap_sorter_spec.rb
560
608
  - spec/services/forest_liana/filters_parser_spec.rb
561
609
  - spec/spec_helper.rb
562
- - spec/requests/actions_controller_spec.rb
610
+ - spec/requests/sessions_spec.rb
611
+ - spec/requests/authentications_spec.rb
563
612
  - spec/requests/resources_spec.rb
564
613
  - spec/dummy/README.rdoc
565
614
  - spec/dummy/app/views/layouts/application.html.erb
@@ -602,6 +651,5 @@ test_files:
602
651
  - spec/dummy/config/initializers/backtrace_silencers.rb
603
652
  - spec/dummy/config/database.yml
604
653
  - spec/helpers/forest_liana/schema_helper_spec.rb
605
- - spec/helpers/forest_liana/is_same_data_structure_helper_spec.rb
606
654
  - spec/helpers/forest_liana/query_helper_spec.rb
607
655
  - spec/rails_helper.rb
@@ -1,44 +0,0 @@
1
- require 'set'
2
-
3
- module ForestLiana
4
- module IsSameDataStructureHelper
5
- class Analyser
6
- def initialize(object, other, deep = 0)
7
- @object = object
8
- @other = other
9
- @deep = deep
10
- end
11
-
12
- def are_objects(object, other)
13
- object && other && object.is_a?(Hash) && other.is_a?(Hash)
14
- end
15
-
16
- def check_keys(object, other, step = 0)
17
- unless are_objects(object, other)
18
- return false
19
- end
20
-
21
- object_keys = object.keys
22
- other_keys = other.keys
23
-
24
- if object_keys.length != other_keys.length
25
- return false
26
- end
27
-
28
- object_keys_set = object_keys.to_set
29
- other_keys.each { |key|
30
- if !object_keys_set.member?(key) || (step + 1 <= @deep && !check_keys(object[key], other[key], step + 1))
31
- return false
32
- end
33
- }
34
-
35
- return true
36
- end
37
-
38
- def perform
39
- check_keys(@object, @other)
40
- end
41
- end
42
- end
43
- end
44
-
@@ -1,87 +0,0 @@
1
- module ForestLiana
2
- context 'IsSameDataStructure class' do
3
- it 'should: be valid with simple data' do
4
- object = {:a => 'a', :b => 'b'}
5
- other = {:a => 'a', :b => 'b'}
6
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
7
- expect(result).to be true
8
- end
9
-
10
- it 'should: be invalid with simple data' do
11
- object = {:a => 'a', :b => 'b'}
12
- other = {:a => 'a', :c => 'c'}
13
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
14
- expect(result).to be false
15
- end
16
-
17
- it 'should: be invalid with not same hash' do
18
- object = {:a => 'a', :b => 'b'}
19
- other = {:a => 'a', :b => 'b', :c => 'c'}
20
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
21
- expect(result).to be false
22
- end
23
-
24
- it 'should: be invalid with nil' do
25
- object = nil
26
- other = {:a => 'a', :b => 'b', :c => 'c'}
27
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
28
- expect(result).to be false
29
- end
30
-
31
- it 'should: be invalid with not hash' do
32
- object = nil
33
- other = {:a => 'a', :b => 'b', :c => 'c'}
34
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
35
- expect(result).to be false
36
- end
37
-
38
- it 'should: be invalid with integer' do
39
- object = 1
40
- other = {:a => 'a', :b => 'b', :c => 'c'}
41
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
42
- expect(result).to be false
43
- end
44
-
45
- it 'should: be invalid with string' do
46
- object = 'a'
47
- other = {:a => 'a', :b => 'b', :c => 'c'}
48
- result = IsSameDataStructureHelper::Analyser.new(object, other).perform
49
- expect(result).to be false
50
- end
51
-
52
- it 'should: be valid with depth 1' do
53
- object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
54
- other = {:a => {:c => 'c'}, :b => {:d => 'd'}}
55
- result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
56
- expect(result).to be true
57
- end
58
-
59
- it 'should: be invalid with depth 1' do
60
- object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
61
- other = {:a => {:c => 'c'}, :b => {:e => 'e'}}
62
- result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
63
- expect(result).to be false
64
- end
65
-
66
- it 'should: be invalid with depth 1 and nil' do
67
- object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
68
- other = {:a => {:c => 'c'}, :b => nil}
69
- result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
70
- expect(result).to be false
71
- end
72
-
73
- it 'should: be invalid with depth 1 and integer' do
74
- object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
75
- other = {:a => {:c => 'c'}, :b => 1}
76
- result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
77
- expect(result).to be false
78
- end
79
-
80
- it 'should: be invalid with depth 1 and string' do
81
- object = {:a => {:c => 'c'}, :b => {:d => 'd'}}
82
- other = {:a => {:c => 'c'}, :b => 'b'}
83
- result = IsSameDataStructureHelper::Analyser.new(object, other, 1).perform
84
- expect(result).to be false
85
- end
86
- end
87
- end
@@ -1,136 +0,0 @@
1
- require 'rails_helper'
2
-
3
- describe 'Requesting Actions routes', :type => :request do
4
- before(:each) do
5
- allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
6
- allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
7
- Island.create(name: 'Corsica')
8
- end
9
-
10
- after(:each) do
11
- Island.destroy_all
12
- end
13
-
14
- describe 'call /values' do
15
- it 'should respond 200' do
16
- post '/forest/actions/foo/values', {}
17
- expect(response.status).to eq(200)
18
- expect(response.body).to be {}
19
- end
20
- end
21
-
22
- describe 'hooks' do
23
- foo = {
24
- field: 'foo',
25
- type: 'String',
26
- default_value: nil,
27
- enums: nil,
28
- is_required: false,
29
- reference: nil,
30
- description: nil,
31
- widget: nil,
32
- }
33
- action_definition = {
34
- name: 'my_action',
35
- fields: [foo],
36
- hooks: {
37
- :load => -> (context) {
38
- context[:fields]
39
- },
40
- :change => {
41
- 'foo' => -> (context) {
42
- fields = context[:fields]
43
- fields['foo'][:value] = 'baz'
44
- return fields
45
- }
46
- }
47
- }
48
- }
49
- fail_action_definition = {
50
- name: 'fail_action',
51
- fields: [foo],
52
- hooks: {
53
- :load => -> (context) {
54
- 1
55
- },
56
- :change => {
57
- 'foo' => -> (context) {
58
- 1
59
- }
60
- }
61
- }
62
- }
63
- cheat_action_definition = {
64
- name: 'cheat_action',
65
- fields: [foo],
66
- hooks: {
67
- :load => -> (context) {
68
- context[:fields]['baz'] = foo.clone.update({field: 'baz'})
69
- context[:fields]
70
- },
71
- :change => {
72
- 'foo' => -> (context) {
73
- context[:fields]['baz'] = foo.clone.update({field: 'baz'})
74
- context[:fields]
75
- }
76
- }
77
- }
78
- }
79
- action = ForestLiana::Model::Action.new(action_definition)
80
- fail_action = ForestLiana::Model::Action.new(fail_action_definition)
81
- cheat_action = ForestLiana::Model::Action.new(cheat_action_definition)
82
- island = ForestLiana.apimap.find {|collection| collection.name.to_s == ForestLiana.name_for(Island)}
83
- island.actions = [action, fail_action, cheat_action]
84
-
85
- describe 'call /load' do
86
- params = {recordIds: [1], collectionName: 'Island'}
87
-
88
- it 'should respond 200' do
89
- post '/forest/actions/my_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
90
- expect(response.status).to eq(200)
91
- expect(JSON.parse(response.body)).to eq({'fields' => [foo.merge({:value => nil}).stringify_keys]})
92
- end
93
-
94
- it 'should respond 500 with bad params' do
95
- post '/forest/actions/my_action/hooks/load', {}
96
- expect(response.status).to eq(500)
97
- end
98
-
99
- it 'should respond 500 with bad hook result type' do
100
- post '/forest/actions/fail_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
101
- expect(response.status).to eq(500)
102
- end
103
-
104
- it 'should respond 500 with bad hook result data structure' do
105
- post '/forest/actions/cheat_action/hooks/load', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
106
- expect(response.status).to eq(500)
107
- end
108
- end
109
-
110
- describe 'call /change' do
111
- updated_foo = foo.clone.merge({:previousValue => nil, :value => 'bar'})
112
- params = {recordIds: [1], fields: [updated_foo], collectionName: 'Island'}
113
-
114
- it 'should respond 200' do
115
- post '/forest/actions/my_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
116
- expect(response.status).to eq(200)
117
- expect(JSON.parse(response.body)).to eq({'fields' => [updated_foo.merge({:value => 'baz'}).stringify_keys]})
118
- end
119
-
120
- it 'should respond 500 with bad params' do
121
- post '/forest/actions/my_action/hooks/change', {}
122
- expect(response.status).to eq(500)
123
- end
124
-
125
- it 'should respond 500 with bad hook result type' do
126
- post '/forest/actions/fail_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
127
- expect(response.status).to eq(500)
128
- end
129
-
130
- it 'should respond 500 with bad hook result data structure' do
131
- post '/forest/actions/cheat_action/hooks/change', JSON.dump(params), 'CONTENT_TYPE' => 'application/json'
132
- expect(response.status).to eq(500)
133
- end
134
- end
135
- end
136
- end