|
7 | 7 | subject { last_response }
|
8 | 8 | let(:json_included) { JSON.parse(last_response.body)['included'] }
|
9 | 9 |
|
10 |
| - let(:comments_policy_scope) { Comment.none } |
| 10 | + let(:comments_policy_scope) { Comment.all } |
11 | 11 | let(:article_policy_scope) { Article.all }
|
12 | 12 | let(:user_policy_scope) { User.all }
|
13 | 13 |
|
| 14 | + # Take the stubbed scope and call merge(policy_scope.scope.all) so that the original |
| 15 | + # scope's conditions are not lost. Without it, the stub will always return all records |
| 16 | + # the user has access to regardless of context. |
14 | 17 | before do
|
15 |
| - allow_any_instance_of(ArticlePolicy::Scope).to receive(:resolve).and_return( |
16 |
| - article_policy_scope |
17 |
| - ) |
18 |
| - allow_any_instance_of(CommentPolicy::Scope).to receive(:resolve).and_return( |
19 |
| - comments_policy_scope |
20 |
| - ) |
21 |
| - allow_any_instance_of(UserPolicy::Scope).to receive(:resolve).and_return( |
22 |
| - user_policy_scope |
23 |
| - ) |
| 18 | + allow_any_instance_of(ArticlePolicy::Scope).to receive(:resolve) do |policy_scope| |
| 19 | + article_policy_scope.merge(policy_scope.scope.all) |
| 20 | + end |
| 21 | + allow_any_instance_of(CommentPolicy::Scope).to receive(:resolve) do |policy_scope| |
| 22 | + comments_policy_scope.merge(policy_scope.scope.all) |
| 23 | + end |
| 24 | + allow_any_instance_of(UserPolicy::Scope).to receive(:resolve) do |policy_scope| |
| 25 | + user_policy_scope.merge(policy_scope.scope.all) |
| 26 | + end |
24 | 27 | end
|
25 | 28 |
|
26 | 29 | before do
|
|
31 | 34 | describe 'one-level deep has_many relationship' do
|
32 | 35 | let(:include_query) { 'comments' }
|
33 | 36 |
|
34 |
| - let(:comments_policy_scope) { Comment.all } |
35 |
| - |
36 | 37 | context 'unauthorized for include_has_many_resource for Comment' do
|
37 | 38 | before {
|
38 | 39 | disallow_operation(
|
|
58 | 59 |
|
59 | 60 | it { is_expected.to be_successful }
|
60 | 61 |
|
61 |
| - let(:comments_policy_scope) { Comment.limit(1) } |
62 |
| - |
63 |
| - it 'includes only comments allowed by policy scope' do |
64 |
| - expect(json_included.length).to eq(1) |
65 |
| - expect(json_included.first["id"]).to eq(comments_policy_scope.first.id.to_s) |
| 62 | + it 'includes only comments allowed by policy scope and associated with the article' do |
| 63 | + expect(json_included.length).to eq(article.comments.count) |
| 64 | + expect( |
| 65 | + json_included.map { |included| included["id"].to_i } |
| 66 | + ).to match_array(article.comments.map(&:id)) |
66 | 67 | end
|
67 | 68 | end
|
68 | 69 | end
|
|
104 | 105 |
|
105 | 106 | describe 'multiple one-level deep relationships' do
|
106 | 107 | let(:include_query) { 'author,comments' }
|
107 |
| - let(:comments_policy_scope) { Comment.all } |
108 | 108 |
|
109 | 109 | context 'unauthorized for include_has_one_resource for article.author' do
|
110 | 110 | before do
|
|
136 | 136 |
|
137 | 137 | it { is_expected.to be_successful }
|
138 | 138 |
|
139 |
| - let(:comments_policy_scope) { Comment.limit(1) } |
140 |
| - |
141 |
| - it 'includes only comments allowed by policy scope' do |
| 139 | + it 'includes only comments allowed by policy scope and associated with the article' do |
142 | 140 | json_comments = json_included.select { |item| item['type'] == 'comments' }
|
143 |
| - expect(json_comments.length).to eq(comments_policy_scope.length) |
144 |
| - expect(json_comments.map { |i| i['id'] }).to eq(comments_policy_scope.pluck(:id).map(&:to_s)) |
| 141 | + expect(json_comments.length).to eq(article.comments.count) |
| 142 | + expect( |
| 143 | + json_comments.map { |i| i['id'] } |
| 144 | + ).to match_array(article.comments.pluck(:id).map(&:to_s)) |
145 | 145 | end
|
146 | 146 |
|
147 | 147 | it 'includes the associated author resource' do
|
|
181 | 181 |
|
182 | 182 | it { is_expected.to be_successful }
|
183 | 183 |
|
184 |
| - let(:comments_policy_scope) { Comment.all } |
185 |
| - |
186 | 184 | it 'includes the first level resource' do
|
187 | 185 | json_users = json_included.select { |item| item['type'] == 'users' }
|
188 | 186 | expect(json_users).to include(a_hash_including('id' => article.author.id.to_s))
|
189 | 187 | end
|
190 | 188 |
|
191 | 189 | describe 'second level resources' do
|
192 |
| - let(:comments_policy_scope) { Comment.limit(1) } |
193 |
| - |
194 | 190 | it 'includes only resources allowed by policy scope' do
|
195 | 191 | second_level_items = json_included.select { |item| item['type'] == 'comments' }
|
196 |
| - expect(second_level_items.length).to eq(comments_policy_scope.length) |
197 |
| - expect(second_level_items.map { |i| i['id'] }).to eq(comments_policy_scope.pluck(:id).map(&:to_s)) |
| 192 | + expect(second_level_items.length).to eq(article.author.comments.count) |
| 193 | + expect( |
| 194 | + second_level_items.map { |i| i['id'] } |
| 195 | + ).to match_array(article.author.comments.pluck(:id).map(&:to_s)) |
198 | 196 | end
|
199 | 197 | end
|
200 | 198 | end
|
|
226 | 224 | end
|
227 | 225 | end
|
228 | 226 |
|
| 227 | + shared_examples_for :scope_limited_directive_tests do |
| 228 | + describe 'one-level deep has_many relationship' do |
| 229 | + let(:comments_policy_scope) { Comment.where(id: article.comments.first.id) } |
| 230 | + let(:include_query) { 'comments' } |
| 231 | + |
| 232 | + context 'authorized for include_has_many_resource for Comment' do |
| 233 | + before { |
| 234 | + allow_operation( |
| 235 | + 'include_has_many_resource', |
| 236 | + source_record: an_instance_of(Article), |
| 237 | + record_class: Comment, |
| 238 | + authorizer: chained_authorizer |
| 239 | + ) |
| 240 | + } |
| 241 | + |
| 242 | + it { is_expected.to be_successful } |
| 243 | + |
| 244 | + it 'includes only comments allowed by policy scope' do |
| 245 | + expect(json_included.length).to eq(comments_policy_scope.length) |
| 246 | + expect(json_included.first["id"]).to eq(comments_policy_scope.first.id.to_s) |
| 247 | + end |
| 248 | + end |
| 249 | + end |
| 250 | + |
| 251 | + describe 'multiple one-level deep relationships' do |
| 252 | + let(:include_query) { 'author,comments' } |
| 253 | + let(:comments_policy_scope) { Comment.where(id: article.comments.first.id) } |
| 254 | + |
| 255 | + context 'authorized for both operations' do |
| 256 | + before do |
| 257 | + allow_operation('include_has_one_resource', source_record: an_instance_of(Article), related_record: an_instance_of(User), authorizer: chained_authorizer) |
| 258 | + allow_operation('include_has_many_resource', source_record: an_instance_of(Article), record_class: Comment, authorizer: chained_authorizer) |
| 259 | + end |
| 260 | + |
| 261 | + it { is_expected.to be_successful } |
| 262 | + |
| 263 | + it 'includes only comments allowed by policy scope and associated with the article' do |
| 264 | + json_comments = json_included.select { |item| item['type'] == 'comments' } |
| 265 | + expect(json_comments.length).to eq(comments_policy_scope.length) |
| 266 | + expect( |
| 267 | + json_comments.map { |i| i['id'] } |
| 268 | + ).to match_array(comments_policy_scope.pluck(:id).map(&:to_s)) |
| 269 | + end |
| 270 | + |
| 271 | + it 'includes the associated author resource' do |
| 272 | + json_users = json_included.select { |item| item['type'] == 'users' } |
| 273 | + expect(json_users).to include(a_hash_including('id' => article.author.id.to_s)) |
| 274 | + end |
| 275 | + end |
| 276 | + end |
| 277 | + |
| 278 | + describe 'a deep relationship' do |
| 279 | + let(:include_query) { 'author.comments' } |
| 280 | + let(:comments_policy_scope) { Comment.where(id: article.author.comments.first.id) } |
| 281 | + |
| 282 | + context 'authorized for first relationship' do |
| 283 | + before { allow_operation('include_has_one_resource', source_record: an_instance_of(Article), related_record: an_instance_of(User), authorizer: chained_authorizer) } |
| 284 | + |
| 285 | + context 'authorized for second relationship' do |
| 286 | + before { allow_operation('include_has_many_resource', source_record: an_instance_of(User), record_class: Comment, authorizer: chained_authorizer) } |
| 287 | + |
| 288 | + it { is_expected.to be_successful } |
| 289 | + |
| 290 | + it 'includes the first level resource' do |
| 291 | + json_users = json_included.select { |item| item['type'] == 'users' } |
| 292 | + expect(json_users).to include(a_hash_including('id' => article.author.id.to_s)) |
| 293 | + end |
| 294 | + |
| 295 | + describe 'second level resources' do |
| 296 | + it 'includes only resources allowed by policy scope' do |
| 297 | + second_level_items = json_included.select { |item| item['type'] == 'comments' } |
| 298 | + expect(second_level_items.length).to eq(comments_policy_scope.length) |
| 299 | + expect( |
| 300 | + second_level_items.map { |i| i['id'] } |
| 301 | + ).to match_array(comments_policy_scope.pluck(:id).map(&:to_s)) |
| 302 | + end |
| 303 | + end |
| 304 | + end |
| 305 | + end |
| 306 | + end |
| 307 | + end |
| 308 | + |
| 309 | + shared_examples_for :scope_limited_directive_test_modify_relationships do |
| 310 | + describe 'one-level deep has_many relationship' do |
| 311 | + let(:comments_policy_scope) { Comment.where(id: existing_comments.first.id) } |
| 312 | + let(:include_query) { 'comments' } |
| 313 | + |
| 314 | + context 'authorized for include_has_many_resource for Comment' do |
| 315 | + before { |
| 316 | + allow_operation( |
| 317 | + 'include_has_many_resource', |
| 318 | + source_record: an_instance_of(Article), |
| 319 | + record_class: Comment, |
| 320 | + authorizer: chained_authorizer |
| 321 | + ) |
| 322 | + } |
| 323 | + |
| 324 | + it { is_expected.to be_not_found } |
| 325 | + end |
| 326 | + end |
| 327 | + |
| 328 | + describe 'multiple one-level deep relationships' do |
| 329 | + let(:include_query) { 'author,comments' } |
| 330 | + let(:comments_policy_scope) { Comment.where(id: existing_comments.first.id) } |
| 331 | + |
| 332 | + context 'authorized for both operations' do |
| 333 | + before do |
| 334 | + allow_operation('include_has_one_resource', source_record: an_instance_of(Article), related_record: an_instance_of(User), authorizer: chained_authorizer) |
| 335 | + allow_operation('include_has_many_resource', source_record: an_instance_of(Article), record_class: Comment, authorizer: chained_authorizer) |
| 336 | + end |
| 337 | + |
| 338 | + it { is_expected.to be_not_found } |
| 339 | + end |
| 340 | + end |
| 341 | + |
| 342 | + describe 'a deep relationship' do |
| 343 | + let(:include_query) { 'author.comments' } |
| 344 | + let(:comments_policy_scope) { Comment.where(id: existing_author.comments.first.id) } |
| 345 | + |
| 346 | + context 'authorized for first relationship' do |
| 347 | + before { allow_operation('include_has_one_resource', source_record: an_instance_of(Article), related_record: an_instance_of(User), authorizer: chained_authorizer) } |
| 348 | + |
| 349 | + context 'authorized for second relationship' do |
| 350 | + before { allow_operation('include_has_many_resource', source_record: an_instance_of(User), record_class: Comment, authorizer: chained_authorizer) } |
| 351 | + |
| 352 | + it { is_expected.to be_not_found } |
| 353 | + end |
| 354 | + end |
| 355 | + end |
| 356 | + end |
| 357 | + |
229 | 358 | describe 'GET /articles' do
|
230 | 359 | subject(:last_response) { get("/articles?include=#{include_query}") }
|
231 | 360 | let!(:chained_authorizer) { allow_operation('find', source_class: Article) }
|
|
244 | 373 |
|
245 | 374 | # TODO: Test properly with multiple articles, not just one.
|
246 | 375 | include_examples :include_directive_tests
|
| 376 | + include_examples :scope_limited_directive_tests |
247 | 377 | end
|
248 | 378 |
|
249 | 379 | describe 'GET /articles/:id' do
|
|
261 | 391 | let!(:chained_authorizer) { allow_operation('show', source_record: article) }
|
262 | 392 |
|
263 | 393 | include_examples :include_directive_tests
|
| 394 | + include_examples :scope_limited_directive_tests |
264 | 395 | end
|
265 | 396 |
|
266 | 397 | describe 'PATCH /articles/:id' do
|
|
290 | 421 | let!(:chained_authorizer) { allow_operation('replace_fields', source_record: article, related_records_with_context: []) }
|
291 | 422 |
|
292 | 423 | include_examples :include_directive_tests
|
| 424 | + include_examples :scope_limited_directive_tests |
293 | 425 |
|
294 | 426 | context 'the request has already failed validations' do
|
295 | 427 | let(:include_query) { 'author.comments' }
|
|
315 | 447 | {
|
316 | 448 | relation_type: :to_one,
|
317 | 449 | relation_name: :author,
|
318 |
| - records: existing_author |
| 450 | + records: [existing_author] |
319 | 451 | },
|
320 | 452 | {
|
321 | 453 | relation_type: :to_many,
|
|
325 | 457 | # down in the other specs.
|
326 | 458 | #
|
327 | 459 | # This is fine, because we test resource create relationships with specific matcher
|
328 |
| - records: kind_of(Array) |
| 460 | + records: kind_of(Enumerable) |
329 | 461 | }
|
330 | 462 | ]
|
331 | 463 | end
|
|
367 | 499 | end
|
368 | 500 |
|
369 | 501 | include_examples :include_directive_tests
|
| 502 | + include_examples :scope_limited_directive_test_modify_relationships |
370 | 503 |
|
371 | 504 | context 'the request has already failed validations' do
|
372 | 505 | let(:include_query) { 'author.comments' }
|
|
395 | 528 | let!(:chained_authorizer) { allow_operation('show_related_resources', source_record: article, related_record_class: article.class) }
|
396 | 529 |
|
397 | 530 | include_examples :include_directive_tests
|
| 531 | + include_examples :scope_limited_directive_tests |
398 | 532 | end
|
399 | 533 |
|
400 | 534 | describe 'GET /articles/:id/article' do
|
|
412 | 546 | let!(:chained_authorizer) { allow_operation('show_related_resource', source_record: article, related_record: article) }
|
413 | 547 |
|
414 | 548 | include_examples :include_directive_tests
|
| 549 | + include_examples :scope_limited_directive_tests |
415 | 550 | end
|
416 | 551 | end
|
0 commit comments