tentd 0.0.1

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 (101) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +9 -0
  5. data/Guardfile +6 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +49 -0
  8. data/Rakefile +8 -0
  9. data/bin/tent-server +3 -0
  10. data/lib/tentd.rb +31 -0
  11. data/lib/tentd/api.rb +58 -0
  12. data/lib/tentd/api/apps.rb +196 -0
  13. data/lib/tentd/api/authentication_finalize.rb +12 -0
  14. data/lib/tentd/api/authentication_lookup.rb +27 -0
  15. data/lib/tentd/api/authentication_verification.rb +50 -0
  16. data/lib/tentd/api/authorizable.rb +21 -0
  17. data/lib/tentd/api/authorization.rb +14 -0
  18. data/lib/tentd/api/core_profile_data.rb +45 -0
  19. data/lib/tentd/api/followers.rb +218 -0
  20. data/lib/tentd/api/followings.rb +241 -0
  21. data/lib/tentd/api/groups.rb +161 -0
  22. data/lib/tentd/api/middleware.rb +32 -0
  23. data/lib/tentd/api/posts.rb +373 -0
  24. data/lib/tentd/api/profile.rb +78 -0
  25. data/lib/tentd/api/router.rb +123 -0
  26. data/lib/tentd/api/router/caching_headers.rb +49 -0
  27. data/lib/tentd/api/router/extract_params.rb +88 -0
  28. data/lib/tentd/api/router/serialize_response.rb +38 -0
  29. data/lib/tentd/api/user_lookup.rb +10 -0
  30. data/lib/tentd/core_ext/hash/slice.rb +29 -0
  31. data/lib/tentd/datamapper/array_property.rb +23 -0
  32. data/lib/tentd/datamapper/query.rb +19 -0
  33. data/lib/tentd/json_patch.rb +181 -0
  34. data/lib/tentd/model.rb +30 -0
  35. data/lib/tentd/model/app.rb +68 -0
  36. data/lib/tentd/model/app_authorization.rb +113 -0
  37. data/lib/tentd/model/follower.rb +105 -0
  38. data/lib/tentd/model/following.rb +100 -0
  39. data/lib/tentd/model/group.rb +24 -0
  40. data/lib/tentd/model/mention.rb +19 -0
  41. data/lib/tentd/model/notification_subscription.rb +56 -0
  42. data/lib/tentd/model/permissible.rb +227 -0
  43. data/lib/tentd/model/permission.rb +28 -0
  44. data/lib/tentd/model/post.rb +178 -0
  45. data/lib/tentd/model/post_attachment.rb +27 -0
  46. data/lib/tentd/model/post_version.rb +64 -0
  47. data/lib/tentd/model/profile_info.rb +80 -0
  48. data/lib/tentd/model/random_public_id.rb +46 -0
  49. data/lib/tentd/model/serializable.rb +58 -0
  50. data/lib/tentd/model/type_properties.rb +36 -0
  51. data/lib/tentd/model/user.rb +39 -0
  52. data/lib/tentd/model/user_scoped.rb +14 -0
  53. data/lib/tentd/notifications.rb +13 -0
  54. data/lib/tentd/notifications/girl_friday.rb +30 -0
  55. data/lib/tentd/notifications/sidekiq.rb +50 -0
  56. data/lib/tentd/tent_type.rb +20 -0
  57. data/lib/tentd/tent_version.rb +41 -0
  58. data/lib/tentd/version.rb +3 -0
  59. data/spec/fabricators/app_authorizations_fabricator.rb +5 -0
  60. data/spec/fabricators/apps_fabricator.rb +11 -0
  61. data/spec/fabricators/followers_fabricator.rb +14 -0
  62. data/spec/fabricators/followings_fabricator.rb +17 -0
  63. data/spec/fabricators/groups_fabricator.rb +3 -0
  64. data/spec/fabricators/mentions_fabricator.rb +3 -0
  65. data/spec/fabricators/notification_subscriptions_fabricator.rb +4 -0
  66. data/spec/fabricators/permissions_fabricator.rb +1 -0
  67. data/spec/fabricators/post_attachments_fabricator.rb +8 -0
  68. data/spec/fabricators/post_versions_fabricator.rb +12 -0
  69. data/spec/fabricators/posts_fabricator.rb +12 -0
  70. data/spec/fabricators/profile_infos_fabricator.rb +30 -0
  71. data/spec/integration/api/apps_spec.rb +466 -0
  72. data/spec/integration/api/followers_spec.rb +535 -0
  73. data/spec/integration/api/followings_spec.rb +688 -0
  74. data/spec/integration/api/groups_spec.rb +207 -0
  75. data/spec/integration/api/posts_spec.rb +874 -0
  76. data/spec/integration/api/profile_spec.rb +285 -0
  77. data/spec/integration/api/router_spec.rb +102 -0
  78. data/spec/integration/model/app_authorization_spec.rb +59 -0
  79. data/spec/integration/model/app_spec.rb +63 -0
  80. data/spec/integration/model/follower_spec.rb +344 -0
  81. data/spec/integration/model/following_spec.rb +97 -0
  82. data/spec/integration/model/group_spec.rb +39 -0
  83. data/spec/integration/model/notification_subscription_spec.rb +145 -0
  84. data/spec/integration/model/post_spec.rb +658 -0
  85. data/spec/spec_helper.rb +37 -0
  86. data/spec/support/expect_server.rb +3 -0
  87. data/spec/support/json_request.rb +54 -0
  88. data/spec/support/with_constant.rb +23 -0
  89. data/spec/support/with_warnings.rb +6 -0
  90. data/spec/unit/api/authentication_finalize_spec.rb +45 -0
  91. data/spec/unit/api/authentication_lookup_spec.rb +65 -0
  92. data/spec/unit/api/authentication_verification_spec.rb +50 -0
  93. data/spec/unit/api/authorizable_spec.rb +50 -0
  94. data/spec/unit/api/authorization_spec.rb +44 -0
  95. data/spec/unit/api/caching_headers_spec.rb +121 -0
  96. data/spec/unit/core_profile_data_spec.rb +64 -0
  97. data/spec/unit/json_patch_spec.rb +407 -0
  98. data/spec/unit/tent_type_spec.rb +28 -0
  99. data/spec/unit/tent_version_spec.rb +68 -0
  100. data/tentd.gemspec +36 -0
  101. metadata +435 -0
@@ -0,0 +1,688 @@
1
+ require 'spec_helper'
2
+
3
+ describe TentD::API::Followings do
4
+ def app
5
+ TentD::API.new
6
+ end
7
+
8
+ def authorize!(*scopes)
9
+ env['current_auth'] = stub(
10
+ :kind_of? => true,
11
+ :id => nil,
12
+ :scopes => scopes
13
+ )
14
+ end
15
+
16
+ let(:env) { Hash.new }
17
+ let(:params) { Hash.new }
18
+
19
+ def stub_notification_http!
20
+ http_stubs.post('/notifications') { [200, {}, []] }
21
+ end
22
+
23
+ describe 'GET /followings/count' do
24
+ it 'should return count of followings' do
25
+ TentD::Model::Following.all.destroy
26
+ following = Fabricate(:following, :public => true)
27
+ json_get '/followings/count', params, env
28
+ expect(last_response.body).to eq(1.to_json)
29
+ end
30
+ end
31
+
32
+ describe 'GET /followings' do
33
+ let(:create_permissions?) { false }
34
+ let(:current_auth) { env['current_auth'] }
35
+
36
+ before do
37
+ @create_permission = lambda do |following|
38
+ TentD::Model::Permission.create(
39
+ :following_id => following.id,
40
+ current_auth.permissible_foreign_key => current_auth.id
41
+ )
42
+ end
43
+ end
44
+
45
+ with_params = proc do
46
+ it 'should order id desc' do
47
+ TentD::Model::Following.all.destroy
48
+ first_following = Fabricate(:following, :public => true)
49
+ last_following = Fabricate(:following, :public => true)
50
+
51
+ json_get "/followings", params, env
52
+ body = JSON.parse(last_response.body)
53
+ body_ids = body.map { |i| i['id'] }
54
+ expect(body_ids).to eq([last_following.public_id, first_following.public_id])
55
+ end
56
+
57
+ context '[:entity]' do
58
+ it 'should only return followings with matching entity uri' do
59
+ other = Fabricate(:following)
60
+ following = Fabricate(:following, :public => true, :entity => 'https://123smith.example.org')
61
+
62
+ json_get "/followings?entity=#{URI.encode_www_form_component(following.entity)}"
63
+ expect(last_response.status).to eq(200)
64
+ body = JSON.parse(last_response.body)
65
+ expect(body.size).to eq(1)
66
+ expect(body.first['id']).to eq(following.public_id)
67
+ end
68
+ end
69
+
70
+ context '[:since_id]' do
71
+ it 'should only return followings with id > :since_id' do
72
+ since_following = Fabricate(:following, :public => !create_permissions?)
73
+ following = Fabricate(:following, :public => !create_permissions?)
74
+
75
+ if create_permissions?
76
+ [since_following, following].each { |f| @create_permission.call(f) }
77
+ end
78
+
79
+ json_get "/followings?since_id=#{since_following.public_id}", params, env
80
+ expect(last_response.status).to eq(200)
81
+ body = JSON.parse(last_response.body)
82
+ expect(body.size).to eq(1)
83
+ expect(body.first['id']).to eq(following.public_id)
84
+ end
85
+ end
86
+
87
+ context '[:before_id]' do
88
+ it 'should only return followings with id < :before_id' do
89
+ TentD::Model::Following.all.destroy!
90
+ following = Fabricate(:following, :public => !create_permissions?)
91
+ before_following = Fabricate(:following, :public => !create_permissions?)
92
+
93
+ if create_permissions?
94
+ [before_following, following].each { |f| @create_permission.call(f) }
95
+ end
96
+
97
+ json_get "/followings?before_id=#{before_following.public_id}", params, env
98
+ expect(last_response.status).to eq(200)
99
+ body = JSON.parse(last_response.body)
100
+ expect(body.size).to eq(1)
101
+ expect(body.first['id']).to eq(following.public_id)
102
+ end
103
+ end
104
+
105
+ context '[:limit]' do
106
+ it 'should only return :limit number of followings' do
107
+ limit = 1
108
+ followings = 0.upto(limit).map { Fabricate(:following, :public => !create_permissions?) }
109
+
110
+ if create_permissions?
111
+ followings.each { |f| @create_permission.call(f) }
112
+ end
113
+
114
+ json_get "/followings?limit=#{limit}", params, env
115
+ expect(JSON.parse(last_response.body).size).to eq(limit)
116
+ end
117
+
118
+ context 'when [:limit] > TentD::API::MAX_PER_PAGE' do
119
+ it 'should only return TentD::API::MAX_PER_PAGE number of followings' do
120
+ with_constants "TentD::API::MAX_PER_PAGE" => 0 do
121
+ limit = 1
122
+ following = Fabricate(:following, :public => !create_permissions?)
123
+
124
+ if create_permissions?
125
+ @create_permission.call(following)
126
+ end
127
+
128
+ json_get "/followings?limit=#{limit}", params, env
129
+ expect(JSON.parse(last_response.body).size).to eq(0)
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ context 'without [:limit]' do
136
+ it 'should only return TentD::API::PER_PAGE number of followings' do
137
+ with_constants "TentD::API::PER_PAGE" => 0 do
138
+ following = Fabricate(:following, :public => !create_permissions?)
139
+
140
+ if create_permissions?
141
+ @create_permission.call(following)
142
+ end
143
+
144
+ json_get "/followings", params, env
145
+ expect(JSON.parse(last_response.body).size).to eq(0)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ without_permissions = proc do
152
+ it 'should only return public followings' do
153
+ TentD::Model::Following.all(:public => true).destroy!
154
+ public_following = Fabricate(:following, :public => true)
155
+ private_following = Fabricate(:following, :public => false)
156
+
157
+ json_get '/followings', params, env
158
+ expect(last_response.status).to eq(200)
159
+ body = JSON.parse(last_response.body)
160
+ body_ids = body.map { |f| f['id'] }
161
+ expect(body_ids).to include(public_following.public_id)
162
+ expect(body_ids).to_not include(private_following.public_id)
163
+ end
164
+
165
+ context 'with params', &with_params
166
+ end
167
+
168
+ with_permissions = proc do
169
+ context 'when permissible' do
170
+ let(:create_permissions?) { true }
171
+ let(:group) { Fabricate(:group, :name => 'chunky-bacon') }
172
+ let!(:public_following) { Fabricate(:following, :public => true) }
173
+ let!(:private_following) { Fabricate(:following, :public => false) }
174
+ let!(:private_permissible_following) { Fabricate(:following, :public => false) }
175
+
176
+ permissible_and_public = proc do
177
+ it 'should return permissible and public followings' do
178
+ json_get '/followings', params, env
179
+ body = JSON.parse(last_response.body)
180
+ body_ids = body.map { |f| f['id'] }
181
+ expect(body_ids).to include(public_following.public_id)
182
+ expect(body_ids).to include(private_permissible_following.public_id)
183
+ expect(body_ids).to_not include(private_following.public_id)
184
+ end
185
+ end
186
+
187
+ context 'explicitly' do
188
+ before {
189
+ @create_permission.call(private_permissible_following)
190
+ }
191
+
192
+ context &permissible_and_public
193
+ end
194
+
195
+ context 'via group' do
196
+ before {
197
+ current_auth.update(:groups => [group.public_id])
198
+ TentD::Model::Permission.create(
199
+ :following_id => private_permissible_following.id,
200
+ :group_public_id => group.public_id
201
+ )
202
+ }
203
+
204
+ context &permissible_and_public
205
+ end
206
+
207
+ context 'with params', &with_params
208
+ end
209
+
210
+ context 'when not permissible', &without_permissions
211
+ end
212
+
213
+ context 'without read_followings scope authorized' do
214
+ context 'without current_auth', &without_permissions
215
+
216
+ context 'with current_auth' do
217
+ context 'when Follower' do
218
+ before{ env['current_auth'] = Fabricate(:follower) }
219
+
220
+ context 'without permissions', &without_permissions
221
+
222
+ context 'with permissions', &with_permissions
223
+ end
224
+
225
+ context 'when AppAuthorization' do
226
+ before { env['current_auth'] = Fabricate(:app_authorization, :app => Fabricate(:app)) }
227
+
228
+ context &without_permissions
229
+ end
230
+ end
231
+ end
232
+
233
+ context 'with read_followings scope authorized' do
234
+ before { authorize!(:read_followings) }
235
+
236
+ it 'should return all followings without mac keys' do
237
+ Fabricate(:following, :public => true)
238
+ Fabricate(:following, :public => false)
239
+ count = TentD::Model::Following.count
240
+ with_constants "TentD::API::MAX_PER_PAGE" => count do
241
+ json_get '/followings', params, env
242
+ expect(last_response.status).to eq(200)
243
+ body = JSON.parse(last_response.body)
244
+ expect(body.size).to eq(count)
245
+ blacklist = %w{ mac_key_id mac_key mac_algorithm mac_timestamp_delta }
246
+ body.each do |actual|
247
+ blacklist.each { |k|
248
+ expect(actual).to_not have_key(k)
249
+ }
250
+ end
251
+ end
252
+ end
253
+
254
+ context 'and read_secrets scope authorized' do
255
+ before {
256
+ authorize!(:read_followings, :read_secrets)
257
+ params['secrets'] = true
258
+ }
259
+
260
+ it 'should return all followings with mac keys' do
261
+ Fabricate(:following, :public => false)
262
+ json_get '/followings', params, env
263
+ expect(last_response.status).to eq(200)
264
+ body = JSON.parse(last_response.body)
265
+ whitelist = %w{ mac_key_id mac_key mac_algorithm }
266
+ body.each do |actual|
267
+ whitelist.each { |k|
268
+ expect(actual).to have_key(k)
269
+ }
270
+ end
271
+ end
272
+ end
273
+
274
+ context 'with params', &with_params
275
+ end
276
+ end
277
+
278
+ describe 'GET /followings/:id' do
279
+ let(:current_auth) { env['current_auth'] }
280
+
281
+ without_permissions = proc do
282
+ it 'should return following if public' do
283
+ following = Fabricate(:following, :public => true)
284
+ json_get "/followings/#{following.public_id}", params, env
285
+ expect(JSON.parse(last_response.body)['id']).to eq(following.public_id)
286
+ end
287
+
288
+ it 'should return 403 unless public' do
289
+ following = Fabricate(:following, :public => false)
290
+ json_get "/followings/#{following.public_id}", params, env
291
+ expect(last_response.status).to eq(403)
292
+ end
293
+
294
+ it 'should return 403 unless exists' do
295
+ following = Fabricate(:following, :public => true)
296
+ json_get "/followings/invalid-id", params, env
297
+ expect(last_response.status).to eq(403)
298
+ end
299
+ end
300
+
301
+ with_permissions = proc do
302
+ context 'explicitly' do
303
+ it 'should return following' do
304
+ following = Fabricate(:following, :public => false)
305
+ TentD::Model::Permission.create(
306
+ :following_id => following.id,
307
+ current_auth.permissible_foreign_key => current_auth.id
308
+ )
309
+ json_get "/followings/#{following.public_id}", params, env
310
+ expect(JSON.parse(last_response.body)['id']).to eq(following.public_id)
311
+ end
312
+ end
313
+
314
+ context 'via group' do
315
+ it 'should return following' do
316
+ group = Fabricate(:group, :name => 'foo')
317
+ current_auth.update(:groups => [group.public_id])
318
+ following = Fabricate(:following, :public => false, :groups => [group.public_id.to_s])
319
+ TentD::Model::Permission.create(
320
+ :following_id => following.id,
321
+ :group_public_id => group.public_id
322
+ )
323
+ json_get "/followings/#{following.public_id}", params, env
324
+ expect(JSON.parse(last_response.body)['id']).to eq(following.public_id)
325
+ end
326
+ end
327
+ end
328
+
329
+ context 'when read_followings scope not authorized' do
330
+ context 'without current_auth', &without_permissions
331
+
332
+ context 'with current_auth' do
333
+ context 'when Follower' do
334
+ before { env['current_auth'] = Fabricate(:follower) }
335
+
336
+ context 'when permissible', &with_permissions
337
+ context 'when not permissible', &without_permissions
338
+ end
339
+
340
+ context 'when AppAuthorization' do
341
+ before { env['current_auth'] = Fabricate(:app_authorization, :app => Fabricate(:app)) }
342
+
343
+ context &without_permissions
344
+ end
345
+ end
346
+ end
347
+
348
+ context 'when read_followings scope authorized' do
349
+ before { authorize!(:read_followings) }
350
+
351
+ it 'should return private following without mac keys' do
352
+ following = Fabricate(:following, :public => false)
353
+ json_get "/followings/#{following.public_id}", params, env
354
+ expect(last_response.status).to eq(200)
355
+ body = JSON.parse(last_response.body)
356
+ blacklist = %w{ mac_key_id mac_key mac_algorithm mac_timestamp_delta }
357
+ blacklist.each { |k|
358
+ expect(body).to_not have_key(k)
359
+ }
360
+ expect(body['id']).to eq(following.public_id)
361
+ end
362
+
363
+ it 'should return public following without mac keys' do
364
+ following = Fabricate(:following, :public => true)
365
+ json_get "/followings/#{following.public_id}", params, env
366
+ expect(last_response.status).to eq(200)
367
+ body = JSON.parse(last_response.body)
368
+ blacklist = %w{ mac_key_id mac_key mac_algorithm mac_timestamp_delta }
369
+ blacklist.each { |k|
370
+ expect(body).to_not have_key(k)
371
+ }
372
+ expect(body['id']).to eq(following.public_id)
373
+ end
374
+
375
+ context 'when read_secrets scope authorized' do
376
+ before {
377
+ authorize!(:read_followings, :read_secrets)
378
+ params['secrets'] = true
379
+ }
380
+
381
+ it 'should return following with mac keys' do
382
+ following = Fabricate(:following, :public => true)
383
+ json_get "/followings/#{following.public_id}", params, env
384
+ expect(last_response.status).to eq(200)
385
+ body = JSON.parse(last_response.body)
386
+ whitelist = %w{ mac_key_id mac_key mac_algorithm }
387
+ expect(body['id']).to eq(following.public_id)
388
+ whitelist.each { |k|
389
+ expect(body).to have_key(k)
390
+ }
391
+ end
392
+ end
393
+
394
+ it 'should return 404 if no following with :id exists' do
395
+ json_get '/followings/invalid-id', params, env
396
+ expect(last_response.status).to eq(404)
397
+ end
398
+ end
399
+ end
400
+
401
+ describe 'POST /followings' do
402
+ let(:http_stubs) { Faraday::Adapter::Test::Stubs.new }
403
+ let(:tent_entity) { 'https://smith.example.com' } # me
404
+ let(:entity_url) { "https://sam.example.org" } # them
405
+ let(:link_header) {
406
+ %(<#{entity_url}/tent/profile>; rel="#{TentD::API::PROFILE_REL}")
407
+ }
408
+ let(:tent_profile) {
409
+ %({"https://tent.io/types/info/core/v0.1.0":{"licenses":["http://creativecommons.org/licenses/by/3.0/"],"entity":"#{entity_url}","servers":["#{entity_url}/tent"]}})
410
+ }
411
+ let(:tent_profile_mismatch) {
412
+ %({"https://tent.io/types/info/core/v0.1.0":{"licenses":["http://creativecommons.org/licenses/by/3.0/"],"entity":"https://mismatch.example.org","servers":["#{entity_url}/tent"]}})
413
+ }
414
+ let(:follower) { Fabricate(:follower, :entity => entity_url) }
415
+ let(:follow_response) { { :id => follower.public_id }.merge(follower.attributes.slice(:mac_key_id, :mac_key, :mac_algorithm)) }
416
+ let(:group) { Fabricate(:group, :name => 'family') }
417
+ let(:following_data) do
418
+ {
419
+ 'entity' => entity_url,
420
+ 'groups' => [{ :id => group.public_id.to_s }]
421
+ }
422
+ end
423
+
424
+ context 'when write_followings scope authorized' do
425
+ before do
426
+ @tent_profile = TentD::Model::ProfileInfo.create(
427
+ :type => TentD::Model::ProfileInfo::TENT_PROFILE_TYPE_URI,
428
+ :content => {
429
+ :licenses => ["http://creativecommons.org/licenses/by/3.0/"]
430
+ }
431
+ )
432
+
433
+ @http_stub_head_success = lambda do
434
+ expect(true)
435
+ http_stubs.head('/') { [200, { 'Link' => link_header }, ''] }
436
+ end
437
+
438
+ @http_stub_profile_success = lambda do
439
+ expect(true)
440
+ http_stubs.get('/tent/profile') {
441
+ [200, { 'Content-Type' => TentD::API::MEDIA_TYPE }, tent_profile]
442
+ }
443
+ end
444
+
445
+ @http_stub_profile_mismatch = lambda do
446
+ http_stubs.get('/tent/profile') {
447
+ expect(true)
448
+ [200, { 'Content-Type' => TentD::API::MEDIA_TYPE }, tent_profile_mismatch]
449
+ }
450
+ end
451
+
452
+ @http_stub_follow_success = lambda do
453
+ http_stubs.post('/tent/followers') {
454
+ expect(true)
455
+ [200, { 'Content-Type' => TentD::API::MEDIA_TYPE }, follow_response]
456
+ }
457
+ end
458
+
459
+ @http_stub_success = lambda do
460
+ @http_stub_head_success.call
461
+ @http_stub_profile_success.call
462
+ @http_stub_follow_success.call
463
+ stub_notification_http!
464
+ end
465
+
466
+ authorize!(:write_followings)
467
+ env['tent.entity'] = tent_entity
468
+ end
469
+
470
+ it 'should perform head discovery on following' do
471
+ @http_stub_success.call
472
+ TentClient.any_instance.stubs(:faraday_adapter).returns([:test, http_stubs])
473
+
474
+ json_post '/followings', following_data, env
475
+ end
476
+
477
+ it 'should send follow request to following' do
478
+ @http_stub_success.call
479
+ TentClient.any_instance.stubs(:faraday_adapter).returns([:test, http_stubs])
480
+
481
+ json_post '/followings', following_data, env
482
+ end
483
+
484
+ context 'when discovery fails' do
485
+ it 'should error 404 when no profile' do
486
+ http_stubs.head('/') { [404, {}, 'Not Found'] }
487
+ http_stubs.get('/') { [404, {}, 'Not Found'] }
488
+ TentClient.any_instance.stubs(:faraday_adapter).returns([:test, http_stubs])
489
+
490
+ json_post '/followings', following_data, env
491
+ expect(last_response.status).to eq(404)
492
+ end
493
+
494
+ it 'should error 409 when entity returned does not match' do
495
+ @http_stub_head_success.call
496
+ @http_stub_profile_mismatch.call
497
+ TentClient.any_instance.stubs(:faraday_adapter).returns([:test, http_stubs])
498
+
499
+ json_post '/followings', following_data, env
500
+ expect(last_response.status).to eq(409)
501
+ end
502
+ end
503
+
504
+ context 'when follow request fails' do
505
+ it 'should error' do
506
+ @http_stub_head_success.call
507
+ @http_stub_profile_success.call
508
+ http_stubs.post('/tent/followers') { [404, {}, 'Not Found'] }
509
+ TentClient.any_instance.stubs(:faraday_adapter).returns([:test, http_stubs])
510
+
511
+ json_post '/followings', following_data, env
512
+ expect(last_response.status).to eq(404)
513
+ end
514
+ end
515
+
516
+ context 'when discovery and follow requests success' do
517
+ before do
518
+ @http_stub_success.call
519
+ stub_notification_http!
520
+ TentClient.any_instance.stubs(:faraday_adapter).returns([:test, http_stubs])
521
+ end
522
+
523
+ it 'should create following' do
524
+ expect(lambda {
525
+ json_post '/followings', following_data, env
526
+ }).to change(TentD::Model::Following, :count).by(1)
527
+
528
+ following = TentD::Model::Following.last
529
+ expect(following.entity.to_s).to eq("https://sam.example.org")
530
+ expect(following.groups).to eq([group.public_id.to_s])
531
+ expect(following.remote_id).to eq(follower.public_id.to_s)
532
+ expect(following.mac_key_id).to eq(follower.mac_key_id)
533
+ expect(following.mac_key).to eq(follower.mac_key)
534
+ expect(following.mac_algorithm).to eq(follower.mac_algorithm)
535
+
536
+ expect(JSON.parse(last_response.body)['id']).to eq(following.public_id)
537
+ end
538
+ end
539
+ end
540
+
541
+ context 'when write_followings scope unauthorized' do
542
+ before {
543
+ TentClient.any_instance.stubs(:faraday_adapter).returns([:test, http_stubs])
544
+ }
545
+
546
+ it 'should return 403' do
547
+ json_post '/followings', params, env
548
+ expect(last_response.status).to eq(403)
549
+ end
550
+ end
551
+ end
552
+
553
+ describe 'PUT /followings/:id' do
554
+ let!(:following) { Fabricate(:following) }
555
+
556
+ context 'when write_followings scope authorized' do
557
+ before { authorize!(:write_followings) }
558
+ let(:following) { Fabricate(:following, :public => false) }
559
+ let(:data) do
560
+ data = following.attributes
561
+ data[:groups] = ['group-id-1', 'group-id-2']
562
+ data[:entity] = "https://entity-name.example.org"
563
+ data[:public] = true
564
+ data[:profile] = { 'type-uri' => { 'foo' => 'bar' } }
565
+ data[:licenses] = ['https://license.example.org']
566
+ data[:mac_key_id] = SecureRandom.hex(4)
567
+ data[:mac_key] = SecureRandom.hex(16)
568
+ data[:mac_algorithm] = 'hmac-sha-1'
569
+ data[:mac_timestamp_delta] = Time.now.to_i
570
+ data
571
+ end
572
+
573
+ it 'should update following' do
574
+ json_put "/followings/#{following.public_id}", data, env
575
+ expect(last_response.status).to eq(200)
576
+
577
+ whitelist = [:groups, :entity, :public, :profile, :licenses]
578
+ blacklist = [:mac_key_id, :mac_key, :mac_algorithm, :mac_timestamp_delta]
579
+
580
+ following.reload
581
+ whitelist.each { |key|
582
+ expect(following.send(key).to_json).to eq(data[key].to_json)
583
+ }
584
+
585
+ blacklist.each { |key|
586
+ expect(following.send(key).to_json).to_not eq(data[key].to_json)
587
+ }
588
+ end
589
+
590
+ context 'when write_secrets scope authorized' do
591
+ before {
592
+ authorize!(:write_followings, :write_secrets)
593
+ }
594
+
595
+ it 'should update following mac key' do
596
+ json_put "/followings/#{following.public_id}", data, env
597
+ expect(last_response.status).to eq(200)
598
+
599
+ whitelist = [:groups, :entity, :public, :profile, :licenses]
600
+ whitelist.concat [:mac_key_id, :mac_key, :mac_algorithm, :mac_timestamp_delta]
601
+
602
+ following.reload
603
+ whitelist.each { |key|
604
+ expect(following.send(key).to_json).to eq(data[key].to_json)
605
+ }
606
+ end
607
+ end
608
+
609
+ it 'should return 404 unless following with :id exists' do
610
+ json_put '/followings/invalid-id', params, env
611
+ expect(last_response.status).to eq(404)
612
+ end
613
+ end
614
+
615
+ context 'when write_followings scope not authorized' do
616
+ it 'should return 403' do
617
+ json_put '/followings/following-id', params, env
618
+ expect(last_response.status).to eq(403)
619
+ end
620
+ end
621
+ end
622
+
623
+ describe 'DELETE /followings/:id' do
624
+ let!(:following) { Fabricate(:following, :remote_id => '12345678') }
625
+ let(:http_stubs) { Faraday::Adapter::Test::Stubs.new }
626
+
627
+ context 'when write_followings scope authorized' do
628
+ before { authorize!(:write_followings) }
629
+
630
+ context 'when exists' do
631
+ it 'should delete following' do
632
+ http_stubs.delete("/followers/#{following.remote_id}") { |env|
633
+ [200, {} ['']]
634
+ }
635
+ TentClient.any_instance.stubs(:faraday_adapter).returns([:test, http_stubs])
636
+ expect(lambda { delete "/followings/#{following.public_id}", params, env }).
637
+ to change(TentD::Model::Following, :count).by(-1)
638
+ http_stubs.verify_stubbed_calls
639
+ end
640
+ end
641
+
642
+ context 'when does not exist' do
643
+ it 'should return 404' do
644
+ expect(lambda { delete "/followings/invalid-id", params, env }).
645
+ to_not change(TentD::Model::Following, :count)
646
+ expect(last_response.status).to eq(404)
647
+ end
648
+ end
649
+ end
650
+
651
+ context 'when write_followings scope unauthorized' do
652
+ it 'should return 403' do
653
+ expect(lambda { delete "/followings/invalid-id", params, env }).
654
+ to_not change(TentD::Model::Following, :count)
655
+ expect(last_response.status).to eq(403)
656
+ end
657
+ end
658
+ end
659
+
660
+ describe 'GET /followings/:id/*' do
661
+ let(:following) { Fabricate(:following) }
662
+ let(:http_stubs) { Faraday::Adapter::Test::Stubs.new }
663
+ before { authorize!(:read_followings) }
664
+
665
+ it 'should proxy the request to the following server' do
666
+ http_stubs.get('/profile') { |env|
667
+ expect(env[:request_headers]['Authorization']).to match(/#{following.mac_key_id}/)
668
+ [200, { 'Content-Type' => 'application/json' }, '{}']
669
+ }
670
+ TentClient.any_instance.stubs(:faraday_adapter).returns([:test, http_stubs])
671
+ json_get("/followings/#{following.public_id}/profile", {}, env)
672
+ expect(last_response.status).to eq(200)
673
+ expect(last_response.body).to eq('{}')
674
+ expect(last_response.headers['Content-Type']).to eq('application/json')
675
+ http_stubs.verify_stubbed_calls
676
+ end
677
+ end
678
+
679
+ describe 'GET /follow' do
680
+ before { Fabricate(:app_authorization, :app => Fabricate(:app), :scopes => %w{ follow_ui }, :follow_url => 'https://example.com/follow') }
681
+
682
+ it 'should redirect to app authoirization with follow_ui scope and follow_url' do
683
+ get '/follow', { :entity => 'https://johnsmith.example.org' }, env
684
+ expect(last_response.status).to eq(302)
685
+ expect(last_response.headers['Location']).to eq("https://example.com/follow?entity=https%3A%2F%2Fjohnsmith.example.org")
686
+ end
687
+ end
688
+ end