sitehub 0.5.0.alpha5 → 0.5.0.alpha6

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.
@@ -0,0 +1,129 @@
1
+ describe 'middleware' do
2
+ include_context :middleware_test
3
+
4
+ let(:downstream_url) { 'http://localhost:12345' }
5
+ let(:experiment1_url) { "#{downstream_url}/experiment1" }
6
+ let(:experiment2_url) { "#{downstream_url}/experiment2" }
7
+
8
+ def middleware(name)
9
+ create_middleware.tap do |clazz|
10
+ clazz.class_eval do
11
+ define_method :call do |env|
12
+ callback = env['async.callback'] || env['async.orig_callback']
13
+ env['async.orig_callback'] = env['async.callback'] = proc do |status, headers, body|
14
+ body = body.body.join if body.is_a?(Rack::BodyProxy)
15
+
16
+ callback.call(status, headers, "#{name}, #{body}")
17
+ end
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ let(:app) { Async::Middleware.new(builder) }
25
+
26
+ before do
27
+ WebMock.enable!
28
+ stub_request(:get, downstream_url).to_return(body: 'hello')
29
+ end
30
+
31
+ context 'middleware added to top level' do
32
+ let(:builder) do
33
+ middleware = middleware(:middleware1)
34
+ downstream_url = downstream_url()
35
+
36
+ SiteHub.build do
37
+ access_logger StringIO.new
38
+ use middleware
39
+ proxy '/1' => downstream_url
40
+ proxy '/2' => downstream_url
41
+ end
42
+ end
43
+
44
+ it 'adds it to each route' do
45
+ get('/1')
46
+ expect(app.last_response.body.join).to eq('middleware1, hello')
47
+ get('/2')
48
+ expect(app.last_response.body.join).to eq('middleware1, hello')
49
+ end
50
+ end
51
+
52
+ context 'middleware added to specific route' do
53
+ let(:builder) do
54
+ middleware = middleware(:middleware1)
55
+ downstream_url = downstream_url()
56
+
57
+ SiteHub.build do
58
+ access_logger StringIO.new
59
+ proxy '/1' do
60
+ use middleware
61
+ route label: :with_middleware, url: downstream_url
62
+ end
63
+ proxy '/2' => downstream_url
64
+ end
65
+ end
66
+
67
+ it 'adds it to that route only' do
68
+ get('/1')
69
+ expect(app.last_response.body.join).to eq('middleware1, hello')
70
+ get('/2')
71
+ expect(app.last_response.body.join).to eq('hello')
72
+ end
73
+ end
74
+
75
+ context 'base inherited middleware' do
76
+ let(:builder) do
77
+ middleware1 = middleware(:middleware1)
78
+ middleware2 = middleware(:middleware2)
79
+ downstream_url = downstream_url()
80
+
81
+ SiteHub.build do
82
+ access_logger StringIO.new
83
+ use middleware1
84
+ proxy '/1' do
85
+ use middleware2
86
+ route label: :with_middleware, url: downstream_url
87
+ end
88
+ proxy '/2' => downstream_url
89
+ end
90
+ end
91
+
92
+ it 'adds it to that route only' do
93
+ get('/1')
94
+ expect(app.last_response.body.join).to eq('middleware1, middleware2, hello')
95
+ get('/2')
96
+ expect(app.last_response.body.join).to eq('middleware1, hello')
97
+ end
98
+ end
99
+
100
+ context 'nested inherited middleware' do
101
+ let(:builder) do
102
+ middleware1 = middleware(:middleware1)
103
+ middleware2 = middleware(:middleware2)
104
+ downstream_url = downstream_url()
105
+
106
+ SiteHub.build do
107
+ access_logger StringIO.new
108
+
109
+ proxy '/1' do
110
+ split percentage: 100, label: :experiment1 do
111
+ use middleware1
112
+ split percentage: 100, label: :with_middleware do
113
+ use middleware2
114
+ split percentage: 100, label: :with_nested_middleware, url: downstream_url
115
+ end
116
+ end
117
+ end
118
+ proxy '/2' => downstream_url
119
+ end
120
+ end
121
+
122
+ it 'adds it to that route only' do
123
+ get('/1')
124
+ expect(app.last_response.body.join).to eq('middleware1, middleware2, hello')
125
+ get('/2')
126
+ expect(app.last_response.body.join).to eq('hello')
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,55 @@
1
+ require 'stringio'
2
+
3
+ describe 'route affinity' do
4
+ let(:downstream_url) { 'http://localhost:12345' }
5
+ let(:experiment1_url) { "#{downstream_url}/experiment1" }
6
+ let(:experiment2_url) { "#{downstream_url}/experiment2" }
7
+
8
+ let(:experiment_body_1) { 'experiment1_body' }
9
+ let(:experiment_body_2) { 'experiment2_body' }
10
+
11
+ before do
12
+ WebMock.enable!
13
+ end
14
+
15
+ let(:app) do
16
+ experiment1_url = experiment1_url()
17
+ experiment2_url = experiment2_url()
18
+
19
+ sitehub = SiteHub.build do
20
+ access_logger StringIO.new
21
+ error_logger StringIO.new
22
+
23
+ proxy '/endpoint' do
24
+ split(label: :experiment1, percentage: 100) do
25
+ split percentage: 100, label: 'variant1', url: experiment1_url
26
+ end
27
+
28
+ split(label: :experiment2, percentage: 0) do
29
+ split percentage: 0, label: 'variant1', url: experiment2_url
30
+ split percentage: 100, label: 'variant2', url: :should_not_be_called
31
+ end
32
+ end
33
+ end
34
+ Async::Middleware.new(sitehub)
35
+ end
36
+
37
+ context 'requested route cookie not present' do
38
+ it 'drops a cookie to keep you on the same path' do
39
+ stub_request(:get, experiment1_url).to_return(body: experiment_body_1)
40
+ get('/endpoint')
41
+ expect(app.last_response.body).to eq([experiment_body_1])
42
+ expect(app.last_response.cookies[SiteHub::RECORDED_ROUTES_COOKIE][:value]).to eq('experiment1|variant1')
43
+ end
44
+ end
45
+
46
+ context 'requested route cookie present' do
47
+ it 'proxies to the preselected route' do
48
+ stub_request(:get, experiment2_url).to_return(body: experiment_body_2)
49
+
50
+ get('/endpoint', {}, 'HTTP_COOKIE' => "#{SiteHub::RECORDED_ROUTES_COOKIE}=experiment2|variant1")
51
+ expect(app.last_response.body).to eq([experiment_body_2])
52
+ expect(app.last_response.cookies[SiteHub::RECORDED_ROUTES_COOKIE][:value]).to eq('experiment2|variant1')
53
+ end
54
+ end
55
+ end
@@ -123,11 +123,11 @@ class SiteHub
123
123
  end
124
124
 
125
125
  it 'adds a forward proxies' do
126
- expect(subject.build).to be_using(Middleware::Routes)
126
+ expect(subject.build).to be_using(Middleware::CandidateRouteMappings)
127
127
  end
128
128
 
129
129
  it 'configures it with the sitehub_cookie_name' do
130
- forward_proxies = find_middleware(subject.build, Middleware::Routes)
130
+ forward_proxies = find_middleware(subject.build, Middleware::CandidateRouteMappings)
131
131
  expect(forward_proxies.sitehub_cookie_name).to eq(:custom_cookie_name)
132
132
  end
133
133
  end
@@ -173,7 +173,7 @@ class SiteHub
173
173
  Middleware::ErrorHandling,
174
174
  Middleware::TransactionId,
175
175
  Middleware::ReverseProxy,
176
- Middleware::Routes]
176
+ Middleware::CandidateRouteMappings]
177
177
 
178
178
  expect(middleware_stack).to eq(expected_middleware)
179
179
  end
@@ -1,18 +1,17 @@
1
1
  # rubocop:disable Metrics/ClassLength
2
- require 'sitehub/route_builder'
2
+ require 'sitehub/candidate_routes'
3
3
 
4
4
  class SiteHub
5
- describe RouteBuilder do
5
+ describe CandidateRoutes do
6
6
  include_context :middleware_test
7
7
 
8
8
  describe '::from_hash' do
9
9
  include_context :sitehub_json
10
10
 
11
- subject do
12
- described_class.from_hash(proxy_1, :expected).routes[Identifier.new(route_1[:label])]
13
- end
14
-
15
11
  context 'splits' do
12
+ subject do
13
+ described_class.from_hash(split_proxy, :expected)
14
+ end
16
15
  context 'sitehub_cookie_name' do
17
16
  it 'sets it' do
18
17
  expect(subject.sitehub_cookie_name).to eq(:expected)
@@ -21,26 +20,110 @@ class SiteHub
21
20
 
22
21
  context 'sitehub_cookie_path' do
23
22
  it 'sets it' do
24
- expect(subject.sitehub_cookie_path).to eq(proxy_1[:sitehub_cookie_path])
23
+ expect(subject.sitehub_cookie_path).to eq(split_proxy[:sitehub_cookie_path])
25
24
  end
26
25
  end
27
26
 
28
- pending 'returns core with splits'
27
+ it 'returns core with splits' do
28
+ split_1 = split_1()
29
+ split_2 = split_2()
30
+ expected = described_class.new(sitehub_cookie_name: :expected,
31
+ sitehub_cookie_path: subject.sitehub_cookie_path,
32
+ mapped_path: subject.mapped_path) do
33
+ split percentage: split_1[:percentage], label: split_1[:label], url: split_1[:url]
34
+ split percentage: split_2[:percentage], label: split_2[:label], url: split_2[:url]
35
+ end
36
+ expect(subject.candidates).to eq(expected.candidates)
37
+ end
38
+
39
+ context 'default' do
40
+ it 'sets it' do
41
+ expect(subject.default_route.app.mapped_url).to eq(split_proxy[:default])
42
+ end
43
+ end
29
44
  end
30
45
 
31
46
  context 'routes' do
47
+ subject do
48
+ described_class.from_hash(routes_proxy, :expected)
49
+ end
50
+
32
51
  context 'sitehub_cookie_name' do
33
- pending 'sets it'
52
+ it 'sets it' do
53
+ expect(subject.sitehub_cookie_name).to eq(:expected)
54
+ end
34
55
  end
35
56
 
36
57
  context 'sitehub_cookie_path' do
37
- pending 'sets it'
58
+ it 'sets it' do
59
+ expect(subject.sitehub_cookie_path).to eq(routes_proxy[:sitehub_cookie_path])
60
+ end
61
+ end
62
+
63
+ it 'returns core with routes' do
64
+ route_1 = route_1()
65
+ expected = described_class.new(sitehub_cookie_name: :expected,
66
+ sitehub_cookie_path: subject.sitehub_cookie_path,
67
+ mapped_path: subject.mapped_path) do
68
+ route label: route_1[:label], url: route_1[:url]
69
+ end
70
+ expect(subject.candidates).to eq(expected.candidates)
71
+ end
72
+
73
+ context 'default' do
74
+ it 'sets it' do
75
+ expect(subject.default_route.app.mapped_url).to eq(routes_proxy[:default])
76
+ end
77
+ end
78
+ end
79
+
80
+ context 'nested routes' do
81
+ context 'routes inside a split' do
82
+ subject do
83
+ described_class.from_hash(nested_route_proxy, :expected)
84
+ end
85
+
86
+ it 'creates them' do
87
+ route_1 = route_1()
88
+ nested_route = nested_route()
89
+
90
+ expected = described_class.new(sitehub_cookie_name: :expected,
91
+ sitehub_cookie_path: subject.sitehub_cookie_path,
92
+ mapped_path: subject.mapped_path) do
93
+ split(percentage: nested_route[:percentage], label: nested_route[:label]) do
94
+ route label: route_1[:label], url: route_1[:url]
95
+ end
96
+ end
97
+ expect(subject).to eq(expected)
98
+ end
99
+ end
100
+
101
+ context 'splits in a split' do
102
+ subject do
103
+ described_class.from_hash(nested_split_proxy, :expected)
104
+ end
105
+
106
+ it 'creates them' do
107
+ split_1 = split_1()
108
+ split_2 = split_2()
109
+ nested_split = nested_split()
110
+
111
+ expected = described_class.new(sitehub_cookie_name: :expected,
112
+ sitehub_cookie_path: subject.sitehub_cookie_path,
113
+ mapped_path: subject.mapped_path) do
114
+ split(percentage: nested_split[:percentage], label: nested_split[:label]) do
115
+ split percentage: split_1[:percentage], label: split_1[:label], url: split_1[:url]
116
+ split percentage: split_2[:percentage], label: split_2[:label], url: split_2[:url]
117
+ end
118
+ end
119
+ expect(subject).to eq(expected)
120
+ end
38
121
  end
39
- pending 'returns core with routes'
40
122
  end
41
123
 
42
124
  context 'default' do
43
- it 'sets the default'
125
+ it 'sets the default' do
126
+ end
44
127
  end
45
128
  end
46
129
 
@@ -51,26 +134,35 @@ class SiteHub
51
134
 
52
135
  describe '#routes' do
53
136
  it 'returns RouteCollection by default' do
54
- expect(subject.routes).to be_a(Collection::RouteCollection)
137
+ expect(subject.candidates).to be_a(Collection::RouteCollection)
55
138
  end
56
139
 
57
140
  it 'returns the same intance everytime' do
58
141
  collection = Collection::SplitRouteCollection.new
59
- subject.routes(collection)
60
- expect(subject.routes).to be(collection)
142
+ subject.candidates(collection)
143
+ expect(subject.candidates).to be(collection)
61
144
  end
62
145
 
63
146
  context 'endpoints already set' do
64
147
  context 'different object supplied' do
65
148
  it 'raises an error' do
66
- subject.routes(Collection::SplitRouteCollection.new)
67
- expect { subject.routes(Collection::RouteCollection.new) }
68
- .to raise_error(RouteBuilder::InvalidDefinitionException)
149
+ subject.candidates(Collection::SplitRouteCollection.new)
150
+ expect { subject.candidates(Collection::RouteCollection.new) }
151
+ .to raise_error(CandidateRoutes::InvalidDefinitionException)
69
152
  end
70
153
  end
71
154
  end
72
155
  end
73
156
 
157
+ describe '#[]' do
158
+ context 'id of existing route passed in' do
159
+ it 'returns it' do
160
+ subject.split(label: :current, percentage: 100, url: :url)
161
+ expect(subject[:current]).to eq(subject[:current])
162
+ end
163
+ end
164
+ end
165
+
74
166
  it 'supports middleware' do
75
167
  expect(described_class).to include(Middleware)
76
168
  end
@@ -141,20 +233,20 @@ class SiteHub
141
233
  describe '#split' do
142
234
  it 'setups up a splits collection' do
143
235
  subject.split percentage: 10, url: :url, label: :label
144
- expect(subject.routes).to be_a(Collection::SplitRouteCollection)
236
+ expect(subject.candidates).to be_a(Collection::SplitRouteCollection)
145
237
  end
146
238
  end
147
239
 
148
240
  describe '#route' do
149
241
  it 'sets up the routes collection' do
150
242
  subject.route url: :url, label: :current
151
- expect(subject.routes).to be_a(Collection::RouteCollection)
243
+ expect(subject.candidates).to be_a(Collection::RouteCollection)
152
244
  end
153
245
  end
154
246
 
155
- describe '#add_endpoint' do
247
+ describe '#add' do
156
248
  it 'stores the route against the given label' do
157
- subject.add_route url: :url, label: :current
249
+ subject.add url: :url, label: :current
158
250
 
159
251
  proxy = ForwardProxy.new(mapped_url: :url,
160
252
  mapped_path: subject.mapped_path)
@@ -164,17 +256,17 @@ class SiteHub
164
256
  sitehub_cookie_name: :cookie_name,
165
257
  sitehub_cookie_path: nil)
166
258
 
167
- expect(subject.routes[Identifier.new(:current)]).to eq(expected_route)
259
+ expect(subject[:current]).to eq(expected_route)
168
260
  end
169
261
 
170
262
  it 'accepts a rule' do
171
- endpoint = subject.add_route url: :url, label: :current, rule: :rule
263
+ endpoint = subject.add url: :url, label: :current, rule: :rule
172
264
  expect(endpoint.rule).to eq(:rule)
173
265
  end
174
266
 
175
267
  it 'accepts a percentage' do
176
- subject.routes(Collection::SplitRouteCollection.new)
177
- endpoint = subject.add_route url: :url, label: :current, percentage: 50
268
+ subject.candidates(Collection::SplitRouteCollection.new)
269
+ endpoint = subject.add url: :url, label: :current, percentage: 50
178
270
  expect(endpoint.upper).to eq(50)
179
271
  end
180
272
 
@@ -189,53 +281,65 @@ class SiteHub
189
281
 
190
282
  it 'stores the nested route_builder against the label' do
191
283
  rule = proc { true }
192
- subject.add_route(rule: rule, label: :label1, &block)
284
+ subject.add(rule: rule, label: :label1, &block)
193
285
  subject.use middleware
194
286
 
195
- expected_endpoints = RouteBuilder.new(rule: rule,
196
- id: :label1,
197
- sitehub_cookie_name: :cookie_name,
198
- mapped_path: '/path',
199
- &block).build
287
+ expected_endpoints = CandidateRoutes.new(rule: rule,
288
+ id: :label1,
289
+ sitehub_cookie_name: :cookie_name,
290
+ mapped_path: '/path',
291
+ &block).build
200
292
 
201
- expect(subject.routes[Identifier.new(:label1)]).to eq(expected_endpoints)
293
+ expect(subject[:label1]).to eq(expected_endpoints)
202
294
  subject.build
203
295
  end
204
296
 
205
297
  describe '#errors and warnings' do
206
298
  context 'precentage and rule not supplied' do
207
- it 'raise an error' do
208
- expected_message = described_class::RULE_NOT_SPECIFIED_MSG
209
- expect { subject.add_route(label: :label) {} }
210
- .to raise_exception described_class::InvalidDefinitionException, expected_message
299
+ context 'split required' do
300
+ it 'raise an error' do
301
+ subject.candidates(Collection::SplitRouteCollection.new)
302
+ expected_message = described_class::PERCENTAGE_NOT_SPECIFIED_MSG
303
+ expect { subject.add(label: :label) {} }
304
+ .to raise_exception described_class::InvalidDefinitionException, expected_message
305
+ end
306
+ end
307
+
308
+ context 'route required' do
309
+ it 'raise an error' do
310
+ subject.candidates(Collection::RouteCollection.new)
311
+ expected_message = described_class::RULE_NOT_SPECIFIED_MSG
312
+ expect { subject.add(label: :label) {} }
313
+ .to raise_exception described_class::InvalidDefinitionException, expected_message
314
+ end
211
315
  end
212
316
  end
213
317
 
214
318
  context 'url' do
215
319
  it 'gives a warning to say that the url will not be used' do
216
320
  expect(subject).to receive(:warn).with(described_class::IGNORING_URL_MSG)
217
- subject.add_route(rule: :rule, url: :url, label: :label, &block)
321
+ subject.add(rule: :rule, url: :url, label: :label, &block)
218
322
  end
219
323
  end
220
324
  end
221
325
 
222
326
  it 'stores a proxy builder' do
223
327
  rule = proc { true }
224
- subject.add_route(rule: rule, label: :label, &block)
328
+ subject.add(rule: rule, label: :label, &block)
225
329
 
226
330
  expected_endpoints = described_class.new(id: :label, sitehub_cookie_name: :cookie_name,
227
331
  rule: rule, mapped_path: subject.mapped_path, &block).tap do |builder|
228
332
  builder.sitehub_cookie_name subject.sitehub_cookie_name
229
333
  end.build
230
334
 
231
- expect(subject.routes.values).to eq([expected_endpoints])
335
+ expect(subject.candidates.values).to eq([expected_endpoints])
232
336
  end
233
337
 
234
338
  context 'invalid definitions inside block' do
235
339
  it 'raises an error' do
236
340
  rule = proc { true }
237
341
  expect do
238
- subject.add_route rule: rule, label: :label do
342
+ subject.add rule: rule, label: :label do
239
343
  split percentage: 20, url: :url, label: :label1
240
344
  end
241
345
  end.to raise_exception described_class::InvalidDefinitionException
@@ -250,9 +354,9 @@ class SiteHub
250
354
  context 'middleware not specified' do
251
355
  it 'leaves it the proxies alone' do
252
356
  subject.route url: :url, label: :current
253
- expect(subject.routes[Identifier.new(:current)]).to be_using_rack_stack(ForwardProxy)
357
+ expect(subject[:current]).to be_using_rack_stack(ForwardProxy)
254
358
  subject.build
255
- expect(subject.routes[Identifier.new(:current)]).to be_using_rack_stack(ForwardProxy)
359
+ expect(subject[:current]).to be_using_rack_stack(ForwardProxy)
256
360
  end
257
361
  end
258
362
 
@@ -264,7 +368,7 @@ class SiteHub
264
368
  it 'wraps the forward proxies in the middleware' do
265
369
  subject.route url: :url, label: :current
266
370
  subject.build
267
- expect(subject.routes[Identifier.new(:current)]).to be_using_rack_stack(middleware, ForwardProxy)
371
+ expect(subject[:current]).to be_using_rack_stack(middleware, ForwardProxy)
268
372
  end
269
373
 
270
374
  it 'wraps the default in the middleware' do
@@ -272,9 +376,18 @@ class SiteHub
272
376
  subject.build
273
377
  expect(subject.default_route).to be_using_rack_stack(middleware, ForwardProxy)
274
378
  end
379
+ end
275
380
 
276
- context 'nested routes' do
277
- pending 'what should it do?'
381
+ context 'middleware present on the parent route' do
382
+ it 'adds it to the list middleware to be added' do
383
+ middleware = middleware()
384
+ subject.split(percentage: 100, label: :parent) do
385
+ use middleware
386
+ split(percentage: 100, label: :child) do
387
+ default url: :url
388
+ end
389
+ end
390
+ expect(subject[:parent][:child].default_route).to be_using(middleware)
278
391
  end
279
392
  end
280
393
  end
@@ -284,11 +397,11 @@ class SiteHub
284
397
  context 'routes defined' do
285
398
  it 'returns that route' do
286
399
  subject.route url: :url, label: :current
287
- expect(subject.resolve(env: {})).to eq(subject.routes.values.first)
400
+ expect(subject.resolve(env: {})).to eq(subject.candidates.values.first)
288
401
  end
289
402
 
290
403
  it 'passes the env to the when resolving the correct route' do
291
- expect_any_instance_of(subject.routes.class).to receive(:resolve).with(env: :env).and_call_original
404
+ expect_any_instance_of(subject.candidates.class).to receive(:resolve).with(env: :env).and_call_original
292
405
  subject.resolve(env: :env)
293
406
  end
294
407
  end
@@ -343,15 +456,15 @@ class SiteHub
343
456
  context '#endpoints' do
344
457
  context 'called with a collection' do
345
458
  it 'sets endpoints to be that collection' do
346
- subject.routes(:collection)
347
- expect(subject.routes).to eq(:collection)
459
+ subject.candidates(:collection)
460
+ expect(subject.candidates).to eq(:collection)
348
461
  end
349
462
  end
350
463
 
351
464
  context 'already set with a different collection' do
352
465
  it 'raise an error' do
353
- subject.routes(:collection1)
354
- expect { subject.routes(:collection2) }.to raise_exception described_class::InvalidDefinitionException
466
+ subject.candidates(:collection1)
467
+ expect { subject.candidates(:collection2) }.to raise_exception described_class::InvalidDefinitionException
355
468
  end
356
469
  end
357
470
  end
@@ -374,5 +487,33 @@ class SiteHub
374
487
  expect(proxy.sitehub_cookie_name).to eq(:expected_cookie_name)
375
488
  end
376
489
  end
490
+
491
+ describe '#method_missing' do
492
+ context 'calling scope set' do
493
+ subject do
494
+ calling_scope = double(:calling_scope, parent_method: :called)
495
+ described_class.new(sitehub_cookie_name: :cookie_name,
496
+ mapped_path: '/path',
497
+ calling_scope: calling_scope)
498
+ end
499
+ context 'method on calling_context' do
500
+ it 'delegates to it' do
501
+ expect(subject.parent_method).to eq(:called)
502
+ end
503
+ end
504
+
505
+ context 'method on calling_context' do
506
+ it 'delegates to it' do
507
+ expect(subject.parent_method).to eq(:called)
508
+ end
509
+ end
510
+ end
511
+
512
+ context 'calling scope not set' do
513
+ it 'raises an error' do
514
+ expect { subject.parent_method }.to raise_error(NoMethodError)
515
+ end
516
+ end
517
+ end
377
518
  end
378
519
  end