qiita_team_services 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +36 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +27 -0
  5. data/.travis.yml +11 -0
  6. data/CHANGELOG.md +7 -0
  7. data/Gemfile +3 -0
  8. data/LICENSE +22 -0
  9. data/README.md +5 -0
  10. data/Rakefile +26 -0
  11. data/config/locales/en.yml +31 -0
  12. data/config/locales/ja.yml +32 -0
  13. data/lib/qiita/team/services/engine.rb +6 -0
  14. data/lib/qiita/team/services/errors.rb +7 -0
  15. data/lib/qiita/team/services/events/base.rb +47 -0
  16. data/lib/qiita/team/services/events/item_became_coediting.rb +10 -0
  17. data/lib/qiita/team/services/events/item_comment_created.rb +17 -0
  18. data/lib/qiita/team/services/events/item_comment_destroyed.rb +17 -0
  19. data/lib/qiita/team/services/events/item_comment_updated.rb +17 -0
  20. data/lib/qiita/team/services/events/item_created.rb +10 -0
  21. data/lib/qiita/team/services/events/item_destroyed.rb +10 -0
  22. data/lib/qiita/team/services/events/item_updated.rb +10 -0
  23. data/lib/qiita/team/services/events/project_activated.rb +10 -0
  24. data/lib/qiita/team/services/events/project_archived.rb +10 -0
  25. data/lib/qiita/team/services/events/project_comment_created.rb +19 -0
  26. data/lib/qiita/team/services/events/project_comment_destroyed.rb +19 -0
  27. data/lib/qiita/team/services/events/project_comment_updated.rb +19 -0
  28. data/lib/qiita/team/services/events/project_created.rb +10 -0
  29. data/lib/qiita/team/services/events/project_destroyed.rb +10 -0
  30. data/lib/qiita/team/services/events/project_updated.rb +10 -0
  31. data/lib/qiita/team/services/events/team_member_added.rb +12 -0
  32. data/lib/qiita/team/services/events/team_member_removed.rb +12 -0
  33. data/lib/qiita/team/services/events.rb +28 -0
  34. data/lib/qiita/team/services/hooks/base.rb +45 -0
  35. data/lib/qiita/team/services/hooks/chatwork_v1.rb +149 -0
  36. data/lib/qiita/team/services/hooks/concerns/event_handlable.rb +49 -0
  37. data/lib/qiita/team/services/hooks/concerns/http_client.rb +74 -0
  38. data/lib/qiita/team/services/hooks/concerns/persistable.rb +64 -0
  39. data/lib/qiita/team/services/hooks/concerns/service.rb +31 -0
  40. data/lib/qiita/team/services/hooks/concerns/slack.rb +338 -0
  41. data/lib/qiita/team/services/hooks/hipchat_v1.rb +143 -0
  42. data/lib/qiita/team/services/hooks/slack_v1.rb +25 -0
  43. data/lib/qiita/team/services/hooks/slack_v2.rb +19 -0
  44. data/lib/qiita/team/services/hooks/webhook.rb +193 -0
  45. data/lib/qiita/team/services/hooks.rb +25 -0
  46. data/lib/qiita/team/services/properties/base.rb +20 -0
  47. data/lib/qiita/team/services/properties/boolean_property.rb +12 -0
  48. data/lib/qiita/team/services/properties/string_property.rb +12 -0
  49. data/lib/qiita/team/services/properties.rb +21 -0
  50. data/lib/qiita/team/services/resources/README.md +177 -0
  51. data/lib/qiita/team/services/templates/chatwork_v1.html.erb +25 -0
  52. data/lib/qiita/team/services/templates/hipchat_v1.html.erb +58 -0
  53. data/lib/qiita/team/services/templates/slack_v1.html.erb +54 -0
  54. data/lib/qiita/team/services/templates/slack_v2.html.erb +44 -0
  55. data/lib/qiita/team/services/templates/webhook.html.erb +25 -0
  56. data/lib/qiita/team/services/version.rb +3 -0
  57. data/lib/qiita_team_services.rb +34 -0
  58. data/qiita_team_services.gemspec +42 -0
  59. data/spec/hooks/chatwork_v1_spec.rb +118 -0
  60. data/spec/hooks/hipchat_v1_spec.rb +139 -0
  61. data/spec/hooks/slack_v1_spec.rb +55 -0
  62. data/spec/hooks/slack_v2_spec.rb +50 -0
  63. data/spec/hooks/webhook_spec.rb +465 -0
  64. data/spec/spec_helper.rb +17 -0
  65. data/spec/support/factories/comments.rb +21 -0
  66. data/spec/support/factories/items.rb +26 -0
  67. data/spec/support/factories/projects.rb +17 -0
  68. data/spec/support/factories/taggings.rb +9 -0
  69. data/spec/support/factories/teams.rb +9 -0
  70. data/spec/support/factories/users.rb +11 -0
  71. data/spec/support/factory_girl.rb +13 -0
  72. data/spec/support/helpers/event_helper.rb +49 -0
  73. data/spec/support/helpers/hook_helper.rb +33 -0
  74. data/spec/support/helpers/http_client_stub_helper.rb +19 -0
  75. data/spec/support/helpers/slack_hook_helper.rb +104 -0
  76. data/spec/support/matchers/match_slack_attachments_request.rb +7 -0
  77. data/spec/support/resources/base.rb +23 -0
  78. data/spec/support/resources/comment.rb +29 -0
  79. data/spec/support/resources/item.rb +53 -0
  80. data/spec/support/resources/project.rb +45 -0
  81. data/spec/support/resources/tagging.rb +15 -0
  82. data/spec/support/resources/team.rb +11 -0
  83. data/spec/support/resources/user.rb +17 -0
  84. metadata +323 -0
@@ -0,0 +1,465 @@
1
+ describe Qiita::Team::Services::Hooks::Webhook, :versioning do
2
+ let(:hook) do
3
+ described_class.new(properties)
4
+ end
5
+
6
+ let(:properties) do
7
+ {
8
+ "url" => url,
9
+ "token" => token,
10
+ }
11
+ end
12
+
13
+ let(:url) do
14
+ FFaker::Internet.http_url
15
+ end
16
+
17
+ let(:token) do
18
+ described_class.generate_token
19
+ end
20
+
21
+ let(:event) do
22
+ Qiita::Team::Services::Events.create(event_name, resource, user, team)
23
+ end
24
+
25
+ let(:user) do
26
+ build(:user)
27
+ end
28
+
29
+ let(:team) do
30
+ build(:team)
31
+ end
32
+
33
+ describe "#item_created" do
34
+ subject do
35
+ hook.item_created(event)
36
+ end
37
+
38
+ let(:event_name) do
39
+ "item_created"
40
+ end
41
+
42
+ let(:resource) do
43
+ build(:item)
44
+ end
45
+
46
+ it "sends webhook" do
47
+ request = stub_request(:post, url).with(
48
+ body: {
49
+ action: "created",
50
+ model: "item",
51
+ item: hash_including(
52
+ "body" => kind_of(String),
53
+ "coediting" => false,
54
+ "comment_count" => kind_of(Integer),
55
+ "created_at_as_seconds" => kind_of(Integer),
56
+ "created_at_in_words" => kind_of(String),
57
+ "created_at" => kind_of(String),
58
+ "id" => kind_of(Integer),
59
+ "lgtm_count" => kind_of(Integer),
60
+ "raw_body" => kind_of(String),
61
+ "stock_count" => kind_of(Integer),
62
+ "stock_users" => [],
63
+ "tags" => [
64
+ {
65
+ "name" => kind_of(String),
66
+ "url_name" => kind_of(String),
67
+ "versions" => kind_of(Array),
68
+ },
69
+ ],
70
+ "title" => kind_of(String),
71
+ "updated_at" => kind_of(String),
72
+ "updated_at_in_words" => kind_of(String),
73
+ "url" => kind_of(String),
74
+ "user" => {
75
+ "id" => kind_of(Integer),
76
+ "profile_image_url" => kind_of(String),
77
+ "url_name" => kind_of(String),
78
+ },
79
+ "uuid" => kind_of(String),
80
+ ),
81
+ user: {
82
+ "id" => kind_of(Integer),
83
+ "profile_image_url" => kind_of(String),
84
+ "url_name" => kind_of(String),
85
+ },
86
+ },
87
+ headers: {
88
+ "Content-Type" => "application/json",
89
+ "User-Agent" => "Qiita:Team",
90
+ "X-Qiita-Event-Model" => "item",
91
+ "X-Qiita-Token" => token,
92
+ },
93
+ )
94
+ subject
95
+ expect(request).to have_been_made
96
+ end
97
+ end
98
+
99
+ describe "#item_updated" do
100
+ subject do
101
+ hook.item_updated(event)
102
+ end
103
+
104
+ let(:event_name) do
105
+ "item_updated"
106
+ end
107
+
108
+ let(:resource) do
109
+ build(:item)
110
+ end
111
+
112
+ it "sends webhook" do
113
+ request = stub_request(:post, url).with(
114
+ body: {
115
+ action: "updated",
116
+ message: kind_of(String),
117
+ model: "item",
118
+ item: kind_of(Hash),
119
+ user: kind_of(Hash),
120
+ },
121
+ )
122
+ subject
123
+ expect(request).to have_been_made
124
+ end
125
+ end
126
+
127
+ describe "#item_destroyed" do
128
+ subject do
129
+ hook.item_destroyed(event)
130
+ end
131
+
132
+ let(:event_name) do
133
+ "item_destroyed"
134
+ end
135
+
136
+ let(:resource) do
137
+ build(:item)
138
+ end
139
+
140
+ it "sends webhook" do
141
+ request = stub_request(:post, url).with(
142
+ body: {
143
+ action: "destroyed",
144
+ model: "item",
145
+ item: kind_of(Hash),
146
+ },
147
+ )
148
+ subject
149
+ expect(request).to have_been_made
150
+ end
151
+ end
152
+
153
+ describe "#item_comment_created" do
154
+ subject do
155
+ hook.item_comment_created(event)
156
+ end
157
+
158
+ let(:event_name) do
159
+ "item_comment_created"
160
+ end
161
+
162
+ let(:resource) do
163
+ build(:comment)
164
+ end
165
+
166
+ it "sends webhook" do
167
+ request = stub_request(:post, url).with(
168
+ body: {
169
+ action: "created",
170
+ model: "comment",
171
+ comment: kind_of(Hash),
172
+ item: kind_of(Hash),
173
+ },
174
+ )
175
+ subject
176
+ expect(request).to have_been_made
177
+ end
178
+ end
179
+
180
+ describe "#item_comment_updated" do
181
+ subject do
182
+ hook.item_comment_updated(event)
183
+ end
184
+
185
+ let(:event_name) do
186
+ "item_comment_updated"
187
+ end
188
+
189
+ let(:resource) do
190
+ build(:comment)
191
+ end
192
+
193
+ it "sends webhook" do
194
+ request = stub_request(:post, url).with(
195
+ body: {
196
+ action: "updated",
197
+ model: "comment",
198
+ comment: kind_of(Hash),
199
+ item: kind_of(Hash),
200
+ },
201
+ )
202
+ subject
203
+ expect(request).to have_been_made
204
+ end
205
+ end
206
+
207
+ describe "#item_comment_destroyed" do
208
+ subject do
209
+ hook.item_comment_destroyed(event)
210
+ end
211
+
212
+ let(:event_name) do
213
+ "item_comment_destroyed"
214
+ end
215
+
216
+ let(:resource) do
217
+ build(:comment)
218
+ end
219
+
220
+ it "sends webhook" do
221
+ request = stub_request(:post, url).with(
222
+ body: {
223
+ action: "destroyed",
224
+ model: "comment",
225
+ comment: kind_of(Hash),
226
+ item: kind_of(Hash),
227
+ },
228
+ )
229
+ subject
230
+ expect(request).to have_been_made
231
+ end
232
+ end
233
+
234
+ describe "#project_comment_created" do
235
+ subject do
236
+ hook.project_comment_created(event)
237
+ end
238
+
239
+ let(:event_name) do
240
+ "project_comment_created"
241
+ end
242
+
243
+ let(:resource) do
244
+ build(:comment)
245
+ end
246
+
247
+ it "sends webhook" do
248
+ request = stub_request(:post, url).with(
249
+ body: {
250
+ action: "created",
251
+ model: "comment",
252
+ comment: kind_of(Hash),
253
+ item: kind_of(Hash),
254
+ },
255
+ )
256
+ subject
257
+ expect(request).to have_been_made
258
+ end
259
+ end
260
+
261
+ describe "#project_comment_updated" do
262
+ subject do
263
+ hook.project_comment_updated(event)
264
+ end
265
+
266
+ let(:event_name) do
267
+ "project_comment_updated"
268
+ end
269
+
270
+ let(:resource) do
271
+ build(:comment)
272
+ end
273
+
274
+ it "sends webhook" do
275
+ request = stub_request(:post, url).with(
276
+ body: {
277
+ action: "updated",
278
+ model: "comment",
279
+ comment: kind_of(Hash),
280
+ item: kind_of(Hash),
281
+ },
282
+ )
283
+ subject
284
+ expect(request).to have_been_made
285
+ end
286
+ end
287
+
288
+ describe "#project_comment_destroyed" do
289
+ subject do
290
+ hook.project_comment_destroyed(event)
291
+ end
292
+
293
+ let(:event_name) do
294
+ "project_comment_destroyed"
295
+ end
296
+
297
+ let(:resource) do
298
+ build(:comment)
299
+ end
300
+
301
+ it "sends webhook" do
302
+ request = stub_request(:post, url).with(
303
+ body: {
304
+ action: "destroyed",
305
+ model: "comment",
306
+ comment: kind_of(Hash),
307
+ item: kind_of(Hash),
308
+ },
309
+ )
310
+ subject
311
+ expect(request).to have_been_made
312
+ end
313
+ end
314
+
315
+ describe "#team_member_added" do
316
+ subject do
317
+ hook.team_member_added(event)
318
+ end
319
+
320
+ let(:event_name) do
321
+ "team_member_added"
322
+ end
323
+
324
+ let(:resource) do
325
+ build(:user)
326
+ end
327
+
328
+ it "sends webhook" do
329
+ request = stub_request(:post, url).with(
330
+ body: {
331
+ action: "added",
332
+ model: "member",
333
+ user: kind_of(Hash),
334
+ },
335
+ )
336
+ subject
337
+ expect(request).to have_been_made
338
+ end
339
+ end
340
+
341
+ describe "#team_member_removed" do
342
+ subject do
343
+ hook.team_member_added(event)
344
+ end
345
+
346
+ let(:event_name) do
347
+ "team_member_removed"
348
+ end
349
+
350
+ let(:resource) do
351
+ build(:user)
352
+ end
353
+
354
+ it "sends webhook" do
355
+ request = stub_request(:post, url).with(
356
+ body: {
357
+ action: "added",
358
+ model: "member",
359
+ user: kind_of(Hash),
360
+ },
361
+ )
362
+ subject
363
+ expect(request).to have_been_made
364
+ end
365
+ end
366
+
367
+ describe "#project_created" do
368
+ subject do
369
+ hook.project_created(event)
370
+ end
371
+
372
+ let(:event_name) do
373
+ "project_created"
374
+ end
375
+
376
+ let(:resource) do
377
+ build(:project)
378
+ end
379
+
380
+ it "sends webhook" do
381
+ request = stub_request(:post, url).with(
382
+ body: {
383
+ action: "created",
384
+ model: "project",
385
+ project: kind_of(Hash),
386
+ user: kind_of(Hash),
387
+ },
388
+ )
389
+ subject
390
+ expect(request).to have_been_made
391
+ end
392
+ end
393
+
394
+ describe "#project_updated" do
395
+ subject do
396
+ hook.project_updated(event)
397
+ end
398
+
399
+ let(:event_name) do
400
+ "project_updated"
401
+ end
402
+
403
+ let(:resource) do
404
+ build(:project)
405
+ end
406
+
407
+ it "sends webhook" do
408
+ request = stub_request(:post, url).with(
409
+ body: {
410
+ action: "updated",
411
+ model: "project",
412
+ message: kind_of(String),
413
+ project: kind_of(Hash),
414
+ user: kind_of(Hash),
415
+ },
416
+ )
417
+ subject
418
+ expect(request).to have_been_made
419
+ end
420
+ end
421
+
422
+ describe "#project_destroyed" do
423
+ subject do
424
+ hook.project_destroyed(event)
425
+ end
426
+
427
+ let(:event_name) do
428
+ "project_destroyed"
429
+ end
430
+
431
+ let(:resource) do
432
+ build(:project)
433
+ end
434
+
435
+ it "sends webhook" do
436
+ request = stub_request(:post, url).with(
437
+ body: {
438
+ action: "destroyed",
439
+ model: "project",
440
+ project: kind_of(Hash),
441
+ },
442
+ )
443
+ subject
444
+ expect(request).to have_been_made
445
+ end
446
+ end
447
+
448
+ describe "#ping" do
449
+ subject do
450
+ hook.ping
451
+ end
452
+
453
+ it "enqueues job to send event" do
454
+ request = stub_request(:post, url).with(
455
+ body: {
456
+ action: "requested",
457
+ message: "ping",
458
+ model: "ping",
459
+ },
460
+ )
461
+ subject
462
+ expect(request).to have_been_made
463
+ end
464
+ end
465
+ end
@@ -0,0 +1,17 @@
1
+ require "ffaker"
2
+ require "pry"
3
+ require "webmock/rspec"
4
+ require "faraday"
5
+ require "qiita_team_services"
6
+
7
+ Dir.glob("spec/support/{helpers,matchers}/*.rb").each do |filepath|
8
+ load filepath
9
+ end
10
+
11
+ require "support/factory_girl"
12
+
13
+ FactoryGirl.define do
14
+ sequence(:id)
15
+ end
16
+
17
+ Faraday.default_adapter = :test
@@ -0,0 +1,21 @@
1
+ require "support/resources/comment"
2
+
3
+ FactoryGirl.define do
4
+ factory :comment, aliases: [:item_comment], class: Qiita::Team::Services::Resources::Comment do
5
+ item
6
+ user { build(:user) }
7
+ id { generate(:id) }
8
+ body "# Example"
9
+ rendered_body "<h1>Example</h1>"
10
+ url { "#{item.url}#comment-#{id}" }
11
+ end
12
+
13
+ factory :project_comment, class: Qiita::Team::Services::Resources::Comment do
14
+ item { build(:project) }
15
+ user { build(:user) }
16
+ id { generate(:id) }
17
+ body "# Example"
18
+ rendered_body "<h1>Example</h1>"
19
+ url { "#{item.url}#comment-#{id}" }
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ require "support/resources/item"
2
+
3
+ FactoryGirl.define do
4
+ factory :item, class: Qiita::Team::Services::Resources::Item do
5
+ body "# Example"
6
+ coediting false
7
+ comment_count 0
8
+ created_at { Time.now }
9
+ created_at_as_seconds { created_at.to_i }
10
+ created_at_in_words "just now"
11
+ editor { build(:user) }
12
+ id { generate(:id) }
13
+ lgtm_count 0
14
+ message { FFaker::Lorem.sentence }
15
+ rendered_body "<h1>Example</h1>"
16
+ stock_count 0
17
+ stock_users []
18
+ tags { [build(:tagging)] }
19
+ title "Example title"
20
+ updated_at { Time.now }
21
+ updated_at_in_words "just now"
22
+ url { "#{build(:team).url}/#{id}" }
23
+ user
24
+ uuid { "4bd431809afb1bb99e4#{generate(:id)}" }
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ require "support/resources/project"
2
+
3
+ FactoryGirl.define do
4
+ factory :project, class: Qiita::Team::Services::Resources::Project do
5
+ user { build(:user) }
6
+ editor { user }
7
+ id { generate(:id) }
8
+ name "Example name"
9
+ body "# Example"
10
+ rendered_body "<h1>Example</h1>"
11
+ message { FFaker::Lorem.sentence }
12
+ url { "#{build(:team).url}/projects/#{id}" }
13
+ archived false
14
+ created_at { Time.now }
15
+ updated_at { Time.now }
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ require "support/resources/tagging"
2
+
3
+ FactoryGirl.define do
4
+ factory :tagging, class: Qiita::Team::Services::Resources::Tagging do
5
+ name "Ruby"
6
+ url_name "ruby"
7
+ versions %w(1.9.3 2.0.0)
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require "support/resources/team"
2
+
3
+ FactoryGirl.define do
4
+ factory :team, class: Qiita::Team::Services::Resources::Team do
5
+ id { generate(:id) }
6
+ name { "name#{id}" }
7
+ url { "https://#{name}.qiita.com" }
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require "support/resources/user"
2
+
3
+ FactoryGirl.define do
4
+ factory :user, aliases: [:team_member], class: Qiita::Team::Services::Resources::User do
5
+ id { generate(:id) }
6
+ url_name { "url_name#{id}" }
7
+ name { "name#{id}" }
8
+ url { "#{build(:team).url}/#{name}" }
9
+ profile_image_url "http://example.com"
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ require "factory_girl"
2
+
3
+ Dir.glob("spec/support/factories/*.rb")
4
+ .map { |filepath| filepath.split("/").last.split(".").first }
5
+ .each { |name| require "support/factories/#{name}" }
6
+
7
+ RSpec.configure do |config|
8
+ config.include FactoryGirl::Syntax::Methods
9
+
10
+ config.before(:suite) do
11
+ FactoryGirl.lint
12
+ end
13
+ end
@@ -0,0 +1,49 @@
1
+ require "active_support/concern"
2
+
3
+ module Qiita::Team::Services
4
+ module Helpers
5
+ module EventHelper
6
+ extend ActiveSupport::Concern
7
+
8
+ {
9
+ item_comment: [
10
+ :created,
11
+ :updated,
12
+ :destroyed,
13
+ ],
14
+ project_comment: [
15
+ :created,
16
+ :updated,
17
+ :destroyed,
18
+ ],
19
+ item: [
20
+ :became_coediting,
21
+ :created,
22
+ :updated,
23
+ :destroyed,
24
+ ],
25
+ project: [
26
+ :activated,
27
+ :archived,
28
+ :created,
29
+ :updated,
30
+ :destroyed,
31
+ ],
32
+ team_member: [
33
+ :added,
34
+ :removed,
35
+ ],
36
+ }.each_pair do |resource_name, action_names|
37
+ action_names.each do |action_name|
38
+ event_name = "#{resource_name}_#{action_name}"
39
+ define_method "#{event_name}_event" do |resource = nil, user = nil, team = nil|
40
+ resource ||= build(resource_name)
41
+ user ||= build(:user)
42
+ team ||= build(:team)
43
+ Events.create(event_name, resource, user, team)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ require "active_support/concern"
2
+ require "action_view/helpers"
3
+
4
+ module Qiita::Team::Services
5
+ module Helpers
6
+ module HookHelper
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ shared_examples "hook" do
11
+ describe ".service_name" do
12
+ subject do
13
+ described_class.service_name
14
+ end
15
+
16
+ it { should be_a String }
17
+ end
18
+
19
+ describe ".render_form" do
20
+ subject do
21
+ extend ActionView::Helpers::FormHelper
22
+ extend ActionView::Helpers::FormTagHelper
23
+ extend ActionView::Helpers::FormOptionsHelper
24
+ described_class.render_form(binding)
25
+ end
26
+
27
+ it { should be_a String }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ require "active_support/concern"
2
+
3
+ module Qiita::Team::Services
4
+ module Helpers
5
+ module HttpClientStubHelper
6
+ extend ActiveSupport::Concern
7
+
8
+ # @param service [#http_client]
9
+ def get_http_client_stub(service)
10
+ stubs = Faraday::Adapter::Test::Stubs.new
11
+ http_client = Faraday.new do |faraday|
12
+ faraday.adapter :test, stubs
13
+ end
14
+ allow(service).to receive(:http_client).and_return(http_client)
15
+ stubs
16
+ end
17
+ end
18
+ end
19
+ end