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,207 @@
1
+ require 'spec_helper'
2
+
3
+ describe TentD::API::Groups 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
+ describe 'GET /groups/count' do
20
+ before { authorize!(:read_groups) }
21
+ it 'should return number of groups' do
22
+ TentD::Model::Group.all.destroy
23
+ Fabricate(:group)
24
+ json_get '/groups/count', params, env
25
+ expect(last_response.body).to eq(1.to_json)
26
+ end
27
+ end
28
+
29
+ describe 'GET /groups' do
30
+ context 'when read_groups scope authorized' do
31
+ before { authorize!(:read_groups) }
32
+
33
+ it 'should return all groups' do
34
+ Fabricate(:group, :name => 'chunky-bacon')
35
+
36
+ with_constants "TentD::API::PER_PAGE" => TentD::Model::Group.count + 1 do
37
+ json_get '/groups', params, env
38
+ expect(JSON.parse(last_response.body).size).to eq(TentD::Model::Group.count)
39
+ end
40
+ end
41
+
42
+ it 'should order by id desc' do
43
+ TentD::Model::Group.all.destroy
44
+ first_group = Fabricate(:group)
45
+ last_group = Fabricate(:group)
46
+
47
+ json_get '/groups', params, env
48
+ body = JSON.parse(last_response.body)
49
+ body_ids = body.map { |i| i['id'] }
50
+ expect(body_ids).to eq([last_group.public_id, first_group.public_id])
51
+ end
52
+
53
+ context 'with params' do
54
+ it 'should filter by before_id' do
55
+ TentD::Model::Group.all.destroy
56
+ group = Fabricate(:group)
57
+ before_group = Fabricate(:group)
58
+
59
+ params[:before_id] = before_group.public_id
60
+ json_get '/groups', params, env
61
+ expect(last_response.status).to eq(200)
62
+
63
+ body = JSON.parse(last_response.body)
64
+ body_ids = body.map { |i| i['id'] }
65
+ expect(body_ids).to eq([group.public_id])
66
+ end
67
+
68
+ it 'should filter by since_id' do
69
+ since_group = Fabricate(:group)
70
+ group = Fabricate(:group)
71
+
72
+ params[:since_id] = since_group.public_id
73
+ json_get '/groups', params, env
74
+ expect(last_response.status).to eq(200)
75
+
76
+ body = JSON.parse(last_response.body)
77
+ body_ids = body.map { |i| i['id'] }
78
+ expect(body_ids).to eq([group.public_id])
79
+ end
80
+
81
+ it 'should support limit' do
82
+ 2.times { Fabricate(:group) }
83
+ params[:limit] = 1
84
+
85
+ json_get '/groups', params, env
86
+ expect(last_response.status).to eq(200)
87
+
88
+ body = JSON.parse(last_response.body)
89
+ expect(body.size).to eq(1)
90
+ end
91
+
92
+ it 'should never return more than TentD::API::MAX_PER_PAGE groups' do
93
+ Fabricate(:group)
94
+ with_constants "TentD::API::MAX_PER_PAGE" => 0 do
95
+ params[:limit] = 1
96
+ json_get '/groups', params, env
97
+ expect(last_response.status).to eq(200)
98
+ expect(JSON.parse(last_response.body).size).to eq(0)
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ context 'when read_groups scope unauthorized' do
105
+ it 'should return 403' do
106
+ get '/groups', params, env
107
+ expect(last_response.status).to eq(403)
108
+ end
109
+ end
110
+ end
111
+
112
+ describe 'GET /groups/:id' do
113
+ context 'when read_groups scope is authorized' do
114
+ before { authorize!(:read_groups) }
115
+
116
+ it 'should find group with :id' do
117
+ group = Fabricate(:group)
118
+ get "/groups/#{group.public_id}", params, env
119
+ expect(JSON.parse(last_response.body)['id']).to eq(group.public_id)
120
+ end
121
+
122
+ it "should render 404 if :id doesn't exist" do
123
+ get "/groups/invalid-id", params, env
124
+ expect(last_response.status).to eq(404)
125
+ end
126
+ end
127
+
128
+ context 'when read_groups scope is unauthorized' do
129
+ it 'should return 403' do
130
+ get '/groups/group-id', params, env
131
+ expect(last_response.status).to eq(403)
132
+ end
133
+ end
134
+ end
135
+
136
+ describe 'PUT /groups/:id' do
137
+ context 'when write_groups scope is authorized' do
138
+ before { authorize!(:write_groups) }
139
+
140
+ it 'should update group with :id' do
141
+ group = Fabricate(:group, :name => 'foo-bar')
142
+ group.name = 'bar-baz'
143
+ expect(group.save).to be_true
144
+ json_put "/groups/#{group.public_id}", group, env
145
+ actual_group = TentD::Model::Group.first(:id => group.id)
146
+ expect(actual_group.name).to eq(group.name)
147
+ expect(JSON.parse(last_response.body)['id']).to eq(actual_group.public_id)
148
+ end
149
+
150
+ it 'should return 404 unless group with :id exists' do
151
+ json_put '/groups/invalid-id', params, env
152
+ expect(last_response.status).to eq(404)
153
+ end
154
+ end
155
+
156
+ context 'when write_groups scope is not authorized' do
157
+ it 'should return 403' do
158
+ json_put '/groups/group-id', params, env
159
+ expect(last_response.status).to eq(403)
160
+ end
161
+ end
162
+ end
163
+
164
+ describe 'POST /groups' do
165
+ context 'when write_groups scope is authorized' do
166
+ before { authorize!(:write_groups) }
167
+
168
+ it 'should create group' do
169
+ expect(lambda { json_post "/groups", { :name => 'bacon-bacon' }, env }).
170
+ to change(TentD::Model::Group, :count).by(1)
171
+ end
172
+ end
173
+
174
+ context 'when write_groups scope is not authorized' do
175
+ it 'should return 403' do
176
+ json_post '/groups', params, env
177
+ expect(last_response.status).to eq(403)
178
+ end
179
+ end
180
+ end
181
+
182
+ describe 'DELETE /groups' do
183
+ context 'when write_groups scope is authorized' do
184
+ before { authorize!(:write_groups) }
185
+
186
+ it 'should destroy group' do
187
+ group = Fabricate(:group, :name => 'foo-bar-baz')
188
+ expect(lambda { delete "/groups/#{group.public_id}", params, env }).
189
+ to change(TentD::Model::Group, :count).by(-1)
190
+ end
191
+
192
+ it 'should returh 404 if group does not exist' do
193
+ Fabricate(:group, :name => 'baz')
194
+ expect(lambda { delete "/groups/invalid-id", params, env }).
195
+ to change(TentD::Model::Group, :count).by(0)
196
+ expect(last_response.status).to eq(404)
197
+ end
198
+ end
199
+
200
+ context 'when write_groups scope is not authorized' do
201
+ it 'should return 403' do
202
+ delete '/groups/group-id', params, env
203
+ expect(last_response.status).to eq(403)
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,874 @@
1
+ require 'spec_helper'
2
+
3
+ describe TentD::API::Posts do
4
+ def app
5
+ TentD::API.new
6
+ end
7
+
8
+ let(:authorized_post_types) { [] }
9
+
10
+ def authorize!(*scopes)
11
+ options = scopes.last if scopes.last.kind_of?(Hash)
12
+ scopes.delete(options)
13
+ methods = {
14
+ :kind_of? => true,
15
+ :id => nil,
16
+ :scopes => scopes,
17
+ :post_types => authorized_post_types,
18
+ }
19
+ if options
20
+ methods[:app] = options[:app] if options[:app]
21
+ methods[:entity] = options[:entity] if options[:entity]
22
+ end
23
+ env['current_auth'] = stub(methods)
24
+ end
25
+
26
+ let(:env) { Hash.new }
27
+ let(:params) { Hash.new }
28
+
29
+ describe 'GET /posts/count' do
30
+ it_should_get_count = proc do
31
+ it 'should return count of posts' do
32
+ TentD::Model::Post.all.destroy
33
+ post = Fabricate(:post, :public => true)
34
+ json_get '/posts/count', params, env
35
+ expect(last_response.body).to eq(1.to_json)
36
+ end
37
+
38
+ it 'should return count of posts with type' do
39
+ TentD::Model::Post.all.destroy
40
+ type = TentD::TentType.new("https://tent.io/types/post/example/v0.1.0")
41
+ type2 = TentD::TentType.new("https://tent.io/types/post/blog/v0.1.0")
42
+ post = Fabricate(:post, :public => true, :type_base => type.base, :type_version => type.version)
43
+ post2 = Fabricate(:post, :public => true, :type_base => type.base, :type_version => type.version, :original => false)
44
+ post3 = Fabricate(:post, :public => true, :type_base => type2.base, :type_version => type2.version)
45
+
46
+ params[:post_types] = type.uri
47
+ json_get '/posts/count', params, env
48
+ expect(last_response.body).to eq(1.to_json)
49
+
50
+ end
51
+ end
52
+
53
+ context &it_should_get_count
54
+
55
+ context 'when read_posts scope authorized' do
56
+ before { authorize!(:read_posts) }
57
+
58
+ context &it_should_get_count
59
+
60
+ context 'when specific types authorized' do
61
+ let(:authorized_post_types) { %w(https://tent.io/types/post/example/v0.1.0 https://tent.io/types/post/blog/v0.1.0) }
62
+ context &it_should_get_count
63
+ end
64
+ end
65
+ end
66
+
67
+ describe 'GET /posts/:post_id' do
68
+ let(:env) { Hashie::Mash.new }
69
+ let(:params) { Hashie::Mash.new }
70
+ with_version = proc do
71
+ context 'with params[:version] specified' do
72
+ context 'when version exists' do
73
+ it 'should return specified post version' do
74
+ first_version = post.latest_version(:fields => [:version]).version
75
+ post.update(:content => { 'text' => 'foo bar baz' })
76
+ latest_version = post.latest_version(:fields => [:version]).version
77
+ expect(first_version).to_not eq(latest_version)
78
+
79
+ json_get "/posts/#{post.public_id}?version=#{first_version}", params, env
80
+ expect(last_response.status).to eq(200)
81
+ body = JSON.parse(last_response.body)
82
+ expect(body['id']).to eq(post.public_id)
83
+ expect(body['version']).to eq(first_version)
84
+
85
+ json_get "/posts/#{post.public_id}?version=#{latest_version}", params, env
86
+ expect(last_response.status).to eq(200)
87
+ body = JSON.parse(last_response.body)
88
+ expect(body['id']).to eq(post.public_id)
89
+ expect(body['version']).to eq(latest_version)
90
+
91
+ json_get "/posts/#{post.public_id}", params, env
92
+ expect(last_response.status).to eq(200)
93
+ body = JSON.parse(last_response.body)
94
+ expect(body['id']).to eq(post.public_id)
95
+ expect(body['version']).to eq(latest_version)
96
+ end
97
+ end
98
+
99
+ context 'when version does not exist' do
100
+ it 'should return 404' do
101
+ json_get "/posts/#{post.public_id}?version=12", params, env
102
+ expect(last_response.status).to eq(404)
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ with_view = proc do
109
+ context 'with params[:view] specified' do
110
+ it 'should return post using specified view' do
111
+ post.update(
112
+ :views => {
113
+ 'mini' => {
114
+ 'content' => ['mini_text', 'title']
115
+ }
116
+ },
117
+ :content => {
118
+ 'text' => 'The quick brown fox jumps over the lazy dog',
119
+ 'mini_text' => 'The quick brown fox...',
120
+ 'title' => 'Quick Fox'
121
+ }
122
+ )
123
+
124
+ json_get "/posts/#{post.public_id}?view=mini", params, env
125
+ expect(last_response.status).to eq(200)
126
+
127
+ body = JSON.parse(last_response.body)
128
+ expect(body['id']).to eq(post.public_id)
129
+ expect(body['content']).to eq({
130
+ 'mini_text' => 'The quick brown fox...',
131
+ 'title' => 'Quick Fox'
132
+ })
133
+ end
134
+ end
135
+ end
136
+
137
+ using_permissions = proc do
138
+ not_authenticated = proc do
139
+ it "should find existing post by public_id" do
140
+ post = Fabricate(:post, :public => true)
141
+ json_get "/posts/#{post.public_id}"
142
+ expect(last_response.status).to eq(200)
143
+ expect(JSON.parse(last_response.body)['id']).to eq(post.public_id)
144
+ end
145
+
146
+ it "should not find existing post by actual id" do
147
+ post = Fabricate(:post, :public => true)
148
+ json_get "/posts/#{post.id}"
149
+ expect(last_response.status).to eq(404)
150
+ end
151
+
152
+ it "should be 404 if post_id doesn't exist" do
153
+ TentD::Model::Post.all.destroy!
154
+ json_get "/posts/1"
155
+ expect(last_response.status).to eq(404)
156
+ end
157
+ end
158
+
159
+ context &not_authenticated
160
+
161
+ with_permissions = proc do
162
+ it 'should return post' do
163
+ json_get "/posts/#{post.public_id}", params, env
164
+ expect(last_response.status).to eq(200)
165
+ expect(JSON.parse(last_response.body)['id']).to eq(post.public_id)
166
+ end
167
+
168
+ context &with_version
169
+ context &with_view
170
+ end
171
+
172
+ current_auth_examples = proc do
173
+ context 'when post is not public' do
174
+ let(:group) { Fabricate(:group, :name => 'friends') }
175
+ let(:post) { Fabricate(:post, :public => false) }
176
+
177
+ context 'when has explicit permission' do
178
+ before do
179
+ case current_auth
180
+ when TentD::Model::Follower
181
+ current_auth.access_permissions.create(:post_id => post.id)
182
+ else
183
+ current_auth.permissions.create(:post_id => post.id)
184
+ end
185
+ env.current_auth = current_auth
186
+ end
187
+
188
+ context &with_permissions
189
+ end
190
+
191
+ context 'when has permission via groups' do
192
+ before do
193
+ post.permissions.create(:group_public_id => group.public_id)
194
+ current_auth.groups = [group.public_id]
195
+ current_auth.save
196
+ env.current_auth = current_auth
197
+ end
198
+
199
+ context &with_permissions
200
+ end
201
+
202
+ context 'when does not have permission' do
203
+ it 'should return 404' do
204
+ post # create post
205
+ json_get "/posts/#{post.public_id}", params, env
206
+ expect(last_response.status).to eq(404)
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ context 'when Follower' do
213
+ let(:current_auth) { Fabricate(:follower) }
214
+
215
+ context &current_auth_examples
216
+ end
217
+
218
+ context 'when AppAuthorization' do
219
+ let(:current_auth) { Fabricate(:app_authorization, :app => Fabricate(:app)) }
220
+
221
+ context &not_authenticated
222
+ end
223
+ end
224
+
225
+ context 'without authorization', &using_permissions
226
+
227
+ context 'with read_posts scope authorized' do
228
+ before { authorize!(:read_posts) }
229
+ let(:post_type) { 'https://tent.io/types/post/status' }
230
+
231
+ post_type_authorized = proc do
232
+ context 'when post exists' do
233
+ it 'should return post' do
234
+ post = Fabricate(:post, :public => false, :type_base => post_type)
235
+ json_get "/posts/#{post.public_id}", params, env
236
+ expect(last_response.status).to eq(200)
237
+ expect(JSON.parse(last_response.body)['id']).to eq(post.public_id)
238
+ end
239
+ end
240
+
241
+ context 'when no post exists with :id' do
242
+ it 'should respond 404' do
243
+ json_get "/posts/invalid-id", params, env
244
+ expect(last_response.status).to eq(404)
245
+ end
246
+ end
247
+ end
248
+
249
+ context 'when post type is authorized' do
250
+ let(:authorized_post_types) { [post_type] }
251
+ context &post_type_authorized
252
+ end
253
+
254
+ context 'when all post types authorized' do
255
+ let(:authorized_post_types) { ['all'] }
256
+ let(:post) { Fabricate(:post, :public => false) }
257
+
258
+ context &post_type_authorized
259
+
260
+ context &with_version
261
+ context &with_view
262
+ end
263
+
264
+ context 'when post type is not authorized' do
265
+ it 'should return 404' do
266
+ post = Fabricate(:post, :public => false, :type_base => post_type)
267
+ json_get "/posts/#{post.public_id}", params, env
268
+ expect(last_response.status).to eq(404)
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ describe 'GET /posts' do
275
+ let(:post_public?) { true }
276
+ with_params = proc do
277
+ it "should respond with first TentD::API::PER_PAGE posts if no params given" do
278
+ with_constants "TentD::API::PER_PAGE" => 1 do
279
+ 0.upto(TentD::API::PER_PAGE+1).each { Fabricate(:post, :public => post_public?) }
280
+ json_get '/posts', params, env
281
+ expect(JSON.parse(last_response.body).size).to eq(1)
282
+ end
283
+ end
284
+
285
+ it "should filter by params[:post_types]" do
286
+ picture_type = TentD::TentType.new("https://tent.io/types/post/picture/v0.1.0")
287
+ blog_type = TentD::TentType.new("https://tent.io/types/post/blog/v0.1.0")
288
+
289
+ picture_post = Fabricate(:post, :public => post_public?, :type_base => picture_type.base)
290
+ non_picture_post = Fabricate(:post, :public => post_public?)
291
+ blog_post = Fabricate(:post, :public => post_public?, :type_base => blog_type.base)
292
+
293
+ posts = TentD::Model::Post.all(:type_base => [picture_type.base, blog_type.base])
294
+ post_types = [picture_post, blog_post].map { |p| URI.encode_www_form_component(p.type.uri) }
295
+
296
+ json_get "/posts?post_types=#{post_types.join(',')}", params, env
297
+ body = JSON.parse(last_response.body)
298
+ expect(body.size).to eq(posts.size)
299
+ body_ids = body.map { |i| i['id'] }
300
+ posts.each { |post|
301
+ expect(body_ids).to include(post.public_id)
302
+ }
303
+ end
304
+
305
+ it "should filter by params[:mentioned_post]" do
306
+ mentioned_post = Fabricate(:post, :public => post_public?)
307
+ post = Fabricate(:post, :public => post_public?, :mentions => [
308
+ Fabricate(:mention, :mentioned_post_id => mentioned_post.public_id, :entity => mentioned_post.entity)
309
+ ])
310
+
311
+ json_get "/posts?mentioned_post=#{mentioned_post.public_id}&mentioned_entity=#{URI.encode_www_form_component(mentioned_post.entity)}", params, env
312
+ body = JSON.parse(last_response.body)
313
+ expect(body.size).to eq(1)
314
+ body_ids = body.map { |i| i['id'] }
315
+ expect(body_ids).to include(post.public_id)
316
+ end
317
+
318
+ it "should filter by params[:entity]" do
319
+ other_post = Fabricate(:post, :public => post_public?)
320
+ first_post = Fabricate(:post, :public => post_public?, :entity => 'https://412doe.example.org')
321
+ last_post = Fabricate(:post, :public => post_public?, :entity => 'https://124alex.example.com')
322
+
323
+ params[:entity] = [first_post.entity, last_post.entity]
324
+ json_get "/posts", params, env
325
+ body = JSON.parse(last_response.body)
326
+ expect(body.size).to eq(2)
327
+ body_ids = body.map { |i| i['id'] }
328
+ expect(body_ids).to include(first_post.public_id)
329
+ expect(body_ids).to include(last_post.public_id)
330
+ end
331
+
332
+ it "should order by published_at desc" do
333
+ TentD::Model::Post.all.destroy
334
+ first_post = Fabricate(:post, :public => post_public?, :published_at => Time.at(Time.now.to_i-86400)) # 1.day.ago
335
+ latest_post = Fabricate(:post, :public => post_public?, :published_at => Time.at(Time.now.to_i+86400)) # 1.day.from_now
336
+
337
+ json_get "/posts", params, env
338
+ body = JSON.parse(last_response.body)
339
+ expect(body.size).to eq(2)
340
+ expect(body.first['id']).to eq(latest_post.public_id)
341
+ expect(body.last['id']).to eq(first_post.public_id)
342
+ end
343
+
344
+ it "should filter by params[:since_id]" do
345
+ since_post = Fabricate(:post, :public => post_public?)
346
+ post = Fabricate(:post, :public => post_public?)
347
+
348
+ json_get "/posts?since_id=#{since_post.public_id}", params, env
349
+ body = JSON.parse(last_response.body)
350
+ expect(body.size).to eq(1)
351
+ body_ids = body.map { |i| i['id'] }
352
+ expect(body_ids.first).to eq(post.public_id)
353
+ end
354
+
355
+ it "should filter by params[:before_id]" do
356
+ TentD::Model::Post.all.destroy
357
+ post = Fabricate(:post, :public => post_public?)
358
+ before_post = Fabricate(:post, :public => post_public?)
359
+
360
+ json_get "/posts?before_id=#{before_post.public_id}", params, env
361
+ body = JSON.parse(last_response.body)
362
+ expect(body.size).to eq(1)
363
+ body_ids = body.map { |i| i['id'] }
364
+ expect(body_ids.first).to eq(post.public_id)
365
+ end
366
+
367
+ it "should filter by both params[:since_id] and params[:before_id]" do
368
+ since_post = Fabricate(:post, :public => post_public?)
369
+ post = Fabricate(:post, :public => post_public?)
370
+ before_post = Fabricate(:post, :public => post_public?)
371
+
372
+ json_get "/posts?before_id=#{before_post.public_id}&since_id=#{since_post.public_id}", params, env
373
+ body = JSON.parse(last_response.body)
374
+ expect(body.size).to eq(1)
375
+ body_ids = body.map { |i| i['id'] }
376
+ expect(body_ids.first).to eq(post.public_id)
377
+ end
378
+
379
+ it "should filter by params[:since_time]" do
380
+ since_post = Fabricate(:post, :public => post_public?)
381
+ since_post.published_at = Time.at(Time.now.to_i + 86400) # 1.day.from_now
382
+ post = Fabricate(:post, :public => post_public?)
383
+ post.published_at = Time.at(Time.now.to_i + (86400 * 2)) # 2.days.from_now
384
+ post.save
385
+
386
+ json_get "/posts?since_time=#{since_post.published_at.to_time.to_i}", params, env
387
+ body = JSON.parse(last_response.body)
388
+ expect(body.size).to eq(1)
389
+ body_ids = body.map { |i| i['id'] }
390
+ expect(body_ids.first).to eq(post.public_id)
391
+ end
392
+
393
+ it "should filter by params[:before_time]" do
394
+ post = Fabricate(:post, :public => post_public?)
395
+ post.published_at = Time.at(Time.now.to_i - (86400 * 2)) # 2.days.ago
396
+ post.save
397
+ before_post = Fabricate(:post, :public => post_public?)
398
+ before_post.published_at = Time.at(Time.now.to_i - 86400) # 1.day.ago
399
+ before_post.save
400
+
401
+ json_get "/posts?before_time=#{before_post.published_at.to_time.to_i}", params, env
402
+ body = JSON.parse(last_response.body)
403
+ expect(body.size).to eq(1)
404
+ body_ids = body.map { |i| i['id'] }
405
+ expect(body_ids.first).to eq(post.public_id)
406
+ end
407
+
408
+ it "should filter by both params[:before_time] and params[:since_time]" do
409
+ now = Time.at(Time.now.to_i - (86400 * 6)) # 6.days.ago
410
+ since_post = Fabricate(:post, :public => post_public?)
411
+ since_post.published_at = Time.at(now.to_i - (86400 * 3)) # 3.days.ago
412
+ since_post.save
413
+ post = Fabricate(:post, :public => post_public?)
414
+ post.published_at = Time.at(now.to_i - (86400 * 2)) # 2.days.ago
415
+ post.save
416
+ before_post = Fabricate(:post, :public => post_public?)
417
+ before_post.published_at = Time.at(now.to_i - 86400) # 1.day.ago
418
+ before_post.save
419
+
420
+ json_get "/posts?before_time=#{before_post.published_at.to_time.to_i}&since_time=#{since_post.published_at.to_time.to_i}", params, env
421
+ body = JSON.parse(last_response.body)
422
+ expect(body.size).to eq(1)
423
+ body_ids = body.map { |i| i['id'] }
424
+ expect(body_ids.first).to eq(post.public_id)
425
+ end
426
+
427
+ it "should set feed length with params[:limit]" do
428
+ 0.upto(2).each { Fabricate(:post, :public => post_public?) }
429
+ json_get '/posts?limit=1', params, env
430
+ expect(JSON.parse(last_response.body).size).to eq(1)
431
+ end
432
+
433
+ it "limit should never exceed TentD::API::MAX_PER_PAGE" do
434
+ with_constants "TentD::API::MAX_PER_PAGE" => 0 do
435
+ 0.upto(2).each { Fabricate(:post, :public => post_public?) }
436
+ json_get '/posts?limit=1', params, env
437
+ expect(last_response.body).to eq([].to_json)
438
+ end
439
+ end
440
+ end
441
+
442
+ context 'without authorization', &with_params
443
+
444
+ context 'with read_posts scope authorized' do
445
+ before { authorize!(:read_posts) }
446
+ let(:post_public?) { false }
447
+
448
+ context 'when post type authorized' do
449
+ let(:authorized_post_types) { ["https://tent.io/types/post/status/v0.1.0", "https://tent.io/types/post/picture/v0.1.0", "https://tent.io/types/post/blog/v0.1.0"] }
450
+
451
+ context &with_params
452
+ end
453
+
454
+ context 'when all post types authorized' do
455
+ let(:authorized_post_types) { ['all'] }
456
+
457
+ context &with_params
458
+ end
459
+
460
+ context 'when post type not authorized' do
461
+ let(:authorized_post_types) { %w(https://tent.io/types/post/status/v0.1.0) }
462
+ it 'should return empty array' do
463
+ TentD::Model::Post.all.destroy
464
+ post = Fabricate(:post, :public => false, :type_base => 'https://tent.io/types/post/repost', :type_version => '0.1.0')
465
+ json_get "/posts", params, env
466
+ expect(last_response.body).to eq([].to_json)
467
+ end
468
+ end
469
+ end
470
+ end
471
+
472
+ describe 'POST /posts' do
473
+ let(:p) { Fabricate.build(:post) }
474
+
475
+ context 'as app with write_posts scope authorized' do
476
+ let(:application) { Fabricate.build(:app) }
477
+ before { authorize!(:write_posts, :app => application) }
478
+
479
+ it "should create post" do
480
+ post_attributes = p.attributes
481
+ post_attributes[:type] = p.type.uri
482
+ expect(lambda {
483
+ expect(lambda {
484
+ json_post "/posts", post_attributes, env
485
+ expect(last_response.status).to eq(200)
486
+ }).to change(TentD::Model::Post, :count).by(1)
487
+ }).to change(TentD::Model::PostVersion, :count).by(1)
488
+ post = TentD::Model::Post.last
489
+ expect(post.app_name).to eq(application.name)
490
+ expect(post.app_url).to eq(application.url)
491
+ body = JSON.parse(last_response.body)
492
+ expect(body['id']).to eq(post.public_id)
493
+ expect(body['app']).to eq('url' => application.url, 'name' => application.name)
494
+ end
495
+
496
+ it 'should create post with views' do
497
+ post_attributes = p.attributes
498
+ post_attributes.delete(:id)
499
+ post_attributes[:type] = p.type.uri
500
+ post_attributes[:views] = {
501
+ 'mini' => {
502
+ 'content' => ['mini_text', 'title']
503
+ }
504
+ }
505
+
506
+ expect(lambda {
507
+ expect(lambda {
508
+ json_post "/posts", post_attributes, env
509
+ expect(last_response.status).to eq(200)
510
+ }).to change(TentD::Model::Post, :count).by(1)
511
+ }).to change(TentD::Model::PostVersion, :count).by(1)
512
+ post = TentD::Model::Post.last
513
+
514
+ expect(post.views).to eq(post_attributes[:views])
515
+ end
516
+
517
+ it 'should create post with mentions' do
518
+ post_attributes = Hashie::Mash.new(p.attributes)
519
+ post_attributes.delete(:id)
520
+ post_attributes[:type] = p.type.uri
521
+ mentions = [
522
+ { :entity => "https://johndoe.example.com" },
523
+ { :entity => "https://alexsmith.example.org", :post => "post-uid" }
524
+ ]
525
+ post_attributes.merge!(
526
+ :mentions => mentions
527
+ )
528
+
529
+ expect(lambda {
530
+ json_post "/posts", post_attributes, env
531
+ expect(last_response.status).to eq(200)
532
+ }).to change(TentD::Model::Post, :count).by(1)
533
+
534
+ post = TentD::Model::Post.last
535
+ expect(post.as_json[:mentions]).to eq(mentions)
536
+ expect(post.mentions.map(&:id)).to eq(post.latest_version.mentions.map(&:id))
537
+ end
538
+
539
+ it 'should create post with permissions' do
540
+ TentD::Model::Group.all.destroy
541
+ TentD::Model::Follower.all.destroy
542
+ TentD::Model::Following.all.destroy
543
+ group = Fabricate(:group)
544
+ follower = Fabricate(:follower, :entity => 'https://john321.example.org')
545
+ following = Fabricate(:following, :entity => 'https://smith123.example.com')
546
+
547
+ post_attributes = p.attributes
548
+ post_attributes.delete(:id)
549
+ post_attributes[:type] = p.type.uri
550
+ post_attributes.merge!(
551
+ :permissions => {
552
+ :public => false,
553
+ :groups => [{ id: group.public_id }],
554
+ :entities => {
555
+ follower.entity => true,
556
+ following.entity => true
557
+ }
558
+ }
559
+ )
560
+
561
+ expect(lambda {
562
+ expect(lambda {
563
+ json_post "/posts", post_attributes, env
564
+ expect(last_response.status).to eq(200)
565
+ }).to change(TentD::Model::Post, :count).by(1)
566
+ }).to change(TentD::Model::Permission, :count).by(3)
567
+
568
+ post = TentD::Model::Post.last
569
+ expect(post.public).to be_false
570
+ end
571
+
572
+ it 'should create post with multipart attachments' do
573
+ post_attributes = p.attributes
574
+ post_attributes.delete(:id)
575
+ post_attributes[:type] = p.type.uri
576
+ attachments = { :foo => [{ :filename => 'a', :content_type => 'text/plain', :content => 'asdf' },
577
+ { :filename => 'a', :content_type => 'application/json', :content => 'asdf123' },
578
+ { :filename => 'b', :content_type => 'text/plain', :content => '1234' }],
579
+ :bar => { :filename => 'bar.html', :content_type => 'text/html', :content => '54321' } }
580
+ expect(lambda {
581
+ expect(lambda {
582
+ expect(lambda {
583
+ multipart_post('/posts', post_attributes, attachments, env)
584
+ }).to change(TentD::Model::Post, :count).by(1)
585
+ }).to change(TentD::Model::PostVersion, :count).by(1)
586
+ }).to change(TentD::Model::PostAttachment, :count).by(4)
587
+ body = JSON.parse(last_response.body)
588
+ expect(body['id']).to eq(TentD::Model::Post.last.public_id)
589
+
590
+ post = TentD::Model::Post.last
591
+ expect(post.attachments.map(&:id)).to eq(post.latest_version.attachments.map(&:id))
592
+ end
593
+ end
594
+
595
+ context 'without app write_posts scope authorized' do
596
+ it 'should respond 403' do
597
+ expect(lambda { json_post "/posts", {}, env }).to_not change(TentD::Model::Post, :count)
598
+ expect(last_response.status).to eq(403)
599
+ end
600
+ end
601
+
602
+ context 'as follower' do
603
+ before { authorize!(:entity => 'https://smith.example.com') }
604
+
605
+ it 'should allow a post from the follower' do
606
+ post_attributes = p.attributes
607
+ post_attributes[:id] = rand(36 ** 6).to_s(36)
608
+ post_attributes[:type] = p.type.uri
609
+ json_post "/posts", post_attributes, env
610
+ body = JSON.parse(last_response.body)
611
+ post = TentD::Model::Post.last
612
+ expect(body['id']).to eq(post.public_id)
613
+ expect(post.public_id).to eq(post_attributes[:id])
614
+ end
615
+
616
+ it "should not allow a post that isn't from the follower" do
617
+ post_attributes = p.attributes
618
+ post_attributes.delete(:id)
619
+ post_attributes[:type] = p.type.uri
620
+ json_post "/posts", post_attributes.merge(:entity => 'example.org'), env
621
+ expect(last_response.status).to eq(403)
622
+ end
623
+
624
+ describe 'profile update post' do
625
+ let(:following) { Fabricate(:following) }
626
+ let(:post_attributes) {
627
+ {
628
+ :type => 'https://tent.io/types/post/profile/v0.1.0',
629
+ :entity => following.entity,
630
+ :content => {
631
+ :action => 'update',
632
+ :types => ['https://tent.io/types/info/core/v0.1.0'],
633
+ }
634
+ }
635
+ }
636
+
637
+ it "should trigger a profile update" do
638
+ env['current_auth'] = following
639
+ TentD::Notifications.expects(:update_following_profile).with(:following_id => following.id)
640
+ json_post "/notifications/#{following.public_id}", post_attributes, env
641
+ expect(last_response.status).to eq(200)
642
+ end
643
+ end
644
+
645
+ describe 'delete post' do
646
+ let(:following) { Fabricate(:following) }
647
+ let(:p) { Fabricate(:post, :entity => following.entity, :following => following, :original => false) }
648
+ let(:post_attributes) {
649
+ {
650
+ :type => 'https://tent.io/types/post/delete/v0.1.0',
651
+ :entity => following.entity,
652
+ :content => {
653
+ :id => p.public_id
654
+ }
655
+ }
656
+ }
657
+
658
+ it "should trigger a post deletion" do
659
+ env['current_auth'] = following
660
+ json_post "/notifications/#{following.public_id}", post_attributes, env
661
+ expect(last_response.status).to eq(200)
662
+ expect(TentD::Model::Post.first(:id => p.id)).to be_nil
663
+ end
664
+ end
665
+ end
666
+
667
+ context 'as anonymous' do
668
+ before { Fabricate(:following) }
669
+
670
+ it 'should not allow a post by an entity that is a following' do
671
+ post_attributes = p.attributes
672
+ post_attributes.delete(:id)
673
+ post_attributes[:type] = p.type.uri
674
+ json_post "/posts", post_attributes, env
675
+ expect(last_response.status).to eq(403)
676
+ end
677
+
678
+ it 'should allow a post by an entity that is not a following' do
679
+ post_attributes = p.attributes
680
+ post_attributes[:id] = rand(36 ** 6).to_s(36)
681
+ post_attributes[:type] = p.type.uri
682
+ json_post "/posts", post_attributes.merge(:entity => 'example.org'), env
683
+ body = JSON.parse(last_response.body)
684
+ post = TentD::Model::Post.last
685
+ expect(body['id']).to eq(post.public_id)
686
+ expect(post.public_id).to eq(post_attributes[:id])
687
+ end
688
+ end
689
+ end
690
+
691
+ describe 'DELETE /posts/:post_id' do
692
+ let(:post) { Fabricate(:post, :original => true) }
693
+
694
+ context 'when authorized' do
695
+ before { authorize!(:write_posts) }
696
+
697
+ context 'when post exists' do
698
+ it 'should delete post and create post deleted notification' do
699
+ delete "/posts/#{post.public_id}", params, env
700
+ expect(last_response.status).to eq(200)
701
+ expect(TentD::Model::Post.first(:id => post.id)).to be_nil
702
+
703
+ deleted_post = post
704
+ post = TentD::Model::Post.last
705
+ expect(post.content['id']).to eq(deleted_post.public_id)
706
+ expect(post.type.base).to eq('https://tent.io/types/post/delete')
707
+ expect(post.type_version).to eq('0.1.0')
708
+ end
709
+ end
710
+
711
+ context 'when post is not original' do
712
+ let(:post) { Fabricate(:post, :original => false) }
713
+
714
+ it 'should return 403' do
715
+ delete "/posts/#{post.public_id}", params, env
716
+ expect(last_response.status).to eq(403)
717
+ end
718
+ end
719
+
720
+ context 'when post does not exist' do
721
+ it 'should return 404' do
722
+ delete "/posts/post-id", params, env
723
+ expect(last_response.status).to eq(404)
724
+ end
725
+ end
726
+ end
727
+
728
+ context 'when not authorized' do
729
+ it 'should return 403' do
730
+ delete "/posts/#{post.public_id}", params, env
731
+ expect(last_response.status).to eq(403)
732
+ end
733
+ end
734
+ end
735
+
736
+ describe 'GET /posts/:post_id/attachments/:attachment_name' do
737
+ let(:post) { Fabricate(:post) }
738
+ let(:attachment) { Fabricate(:post_attachment, :post => post) }
739
+
740
+ it 'should get an attachment' do
741
+ get "/posts/#{post.public_id}/attachments/#{attachment.name}", {}, 'HTTP_ACCEPT' => attachment.type
742
+ expect(last_response.status).to eq(200)
743
+ expect(last_response.headers['Content-Type']).to eq(attachment.type)
744
+ expect(last_response.body).to eq('54321')
745
+ end
746
+
747
+ context 'with params[:version]' do
748
+ it 'should get specified version of attachment' do
749
+ post_version = Fabricate(:post_version, :post => post, :public_id => post.public_id, :version => 12)
750
+ new_attachment = Fabricate(:post_attachment, :post => nil, :post_version => post_version, :data => Base64.encode64('ChunkyBacon'))
751
+
752
+ expect(post.latest_version(:fields => [:id]).id).to eq(post_version.id)
753
+ expect(new_attachment.name).to eq(attachment.name)
754
+
755
+ get "/posts/#{post.public_id}/attachments/#{attachment.name}", { :version => post_version.version}, 'HTTP_ACCEPT' => attachment.type
756
+
757
+ expect(last_response.status).to eq(200)
758
+ expect(last_response.headers['Content-Type']).to eq(attachment.type)
759
+ expect(last_response.body).to eq('ChunkyBacon')
760
+ end
761
+
762
+ it "should return 404 if specified version doesn't exist" do
763
+ get "/posts/#{post.public_id}/attachments/#{attachment.name}", { :version => 20}, 'HTTP_ACCEPT' => attachment.type
764
+ expect(last_response.status).to eq(404)
765
+ end
766
+ end
767
+
768
+ it "should 404 if the attachment doesn't exist" do
769
+ get "/posts/#{post.public_id}/attachments/asdf"
770
+ expect(last_response.status).to eq(404)
771
+ end
772
+
773
+ it "should 404 if the post doesn't exist" do
774
+ get "/posts/asdf/attachments/asdf"
775
+ expect(last_response.status).to eq(404)
776
+ end
777
+ end
778
+
779
+ describe 'PUT /posts/:post_id' do
780
+ let(:post) { Fabricate(:post) }
781
+
782
+ context 'when authorized' do
783
+ before { authorize!(:write_posts) }
784
+
785
+ it 'should update post' do
786
+ Fabricate(:post_attachment, :post => post)
787
+ Fabricate(:mention, :post => post)
788
+
789
+ post_attributes = {
790
+ :content => {
791
+ "text" => "Foo Bar Baz"
792
+ },
793
+ :views => {
794
+ 'mini' => { 'content' => ['mini_text'] }
795
+ },
796
+ :entity => "#{post.entity}/foo/bar",
797
+ :public => !post.public,
798
+ :licenses => post.licenses.to_a + ['https://license.example.org']
799
+ }
800
+
801
+ expect(lambda {
802
+ expect(lambda {
803
+ expect(lambda {
804
+ json_put "/posts/#{post.public_id}", post_attributes, env
805
+ expect(last_response.status).to eq(200)
806
+
807
+ post.reload
808
+ expect(post.content).to eq(post_attributes[:content])
809
+ expect(post.licenses).to eq(post_attributes[:licenses])
810
+ expect(post.views).to eq(post_attributes[:views])
811
+ expect(post.public).to_not eq(post_attributes[:public])
812
+ expect(post.entity).to_not eq(post_attributes[:entity])
813
+ }).to change(post.versions, :count).by(1)
814
+ }).to_not change(post.mentions, :count)
815
+ }).to_not change(post.attachments, :count)
816
+ end
817
+
818
+ it 'should update mentions' do
819
+ existing_mentions = 2.times.map { Fabricate(:mention, :post => post) }
820
+ post_attributes = {
821
+ :mentions => [
822
+ { :entity => "https://johndoe.example.com" },
823
+ { :entity => "https://alexsmith.example.org", :post => "post-uid" }
824
+ ]
825
+ }
826
+
827
+ expect(lambda {
828
+ expect(lambda {
829
+ json_put "/posts/#{post.public_id}", post_attributes, env
830
+ expect(last_response.status).to eq(200)
831
+
832
+ existing_mentions.each do |m|
833
+ m.reload
834
+ expect(m.post_version_id).to_not be_nil
835
+ expect(m.post_id).to be_nil
836
+ end
837
+ }).to change(TentD::Model::Mention, :count).by(2)
838
+ }).to change(post.versions, :count).by(1)
839
+ end
840
+
841
+ it 'should update attachments' do
842
+ existing_attachments = 2.times.map { Fabricate(:post_attachment, :post => post) }
843
+ attachments = { :foo => [{ :filename => 'a', :content_type => 'text/plain', :content => 'asdf' },
844
+ { :filename => 'a', :content_type => 'application/json', :content => 'asdf123' },
845
+ { :filename => 'b', :content_type => 'text/plain', :content => '1234' }],
846
+ :bar => { :filename => 'bar.html', :content_type => 'text/html', :content => '54321' } }
847
+ expect(lambda {
848
+ expect(lambda {
849
+ expect(lambda {
850
+ multipart_put("/posts/#{post.public_id}", {}, attachments, env)
851
+ expect(last_response.status).to eq(200)
852
+
853
+ existing_attachments.each do |a|
854
+ a.reload
855
+ expect(a.post_version_id).to_not be_nil
856
+ expect(a.post_id).to be_nil
857
+ end
858
+ }).to change(TentD::Model::Post, :count).by(0)
859
+ }).to change(TentD::Model::PostVersion, :count).by(1)
860
+ }).to change(TentD::Model::PostAttachment, :count).by(4)
861
+
862
+ post.reload
863
+ expect(post.attachments.map(&:id)).to eq(post.latest_version.attachments.map(&:id))
864
+ end
865
+ end
866
+
867
+ context 'when not authorized' do
868
+ it 'should return 403' do
869
+ json_put "/posts/#{post.public_id}", params, env
870
+ expect(last_response.status).to eq(403)
871
+ end
872
+ end
873
+ end
874
+ end