tentd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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