diff --git a/test/helpers/functional_helpers.rb b/test/helpers/functional_helpers.rb index 3d6dc9d34..d8df4e203 100644 --- a/test/helpers/functional_helpers.rb +++ b/test/helpers/functional_helpers.rb @@ -55,5 +55,23 @@ module FunctionalHelpers def json_response JSON.parse(@response.body) end + + # JSON:API gives no meaning to the order of to-many linkage, so sort it before comparing responses + def normalize_linkage_order(json) + return json unless json.is_a?(Hash) + + json.deep_dup.tap do |normalized| + resources = Array.wrap(normalized['data']) + Array.wrap(normalized['included']) + resources.each { |resource| sort_to_many_linkage(resource) } + end + end + + private + + def sort_to_many_linkage(resource) + (resource['relationships'] || {}).each_value do |relationship| + relationship['data'].sort_by! { |r| [r['type'], r['id']] } if relationship['data'].is_a?(Array) + end + end end end \ No newline at end of file diff --git a/test/helpers/functional_helpers_test.rb b/test/helpers/functional_helpers_test.rb new file mode 100644 index 000000000..f892d6863 --- /dev/null +++ b/test/helpers/functional_helpers_test.rb @@ -0,0 +1,43 @@ +require File.expand_path('../../test_helper', __FILE__) + +class FunctionalHelpersTest < ActionController::TestCase + + def test_normalize_linkage_order_sorts_to_many_linkage_in_data_and_included + json = { + 'data' => [ + { 'type' => 'books', 'id' => '1', 'relationships' => { + 'book_comments' => { 'data' => [{ 'type' => 'book_comments', 'id' => '9' }, + { 'type' => 'book_comments', 'id' => '2' }] } } } + ], + 'included' => [ + { 'type' => 'people', 'id' => '3', 'relationships' => { + 'comments' => { 'data' => [{ 'type' => 'comments', 'id' => '5' }, + { 'type' => 'comments', 'id' => '4' }] } } } + ] + } + + normalized = normalize_linkage_order(json) + + assert_equal %w[2 9], normalized['data'][0]['relationships']['book_comments']['data'].map { |r| r['id'] } + assert_equal %w[4 5], normalized['included'][0]['relationships']['comments']['data'].map { |r| r['id'] } + end + + def test_normalize_linkage_order_leaves_everything_else_unchanged + assert_nil normalize_linkage_order(nil) + + json = { + 'data' => [ + { 'type' => 'books', 'id' => '2', 'relationships' => { + 'author' => { 'data' => { 'type' => 'people', 'id' => '7' } }, + 'tags' => { 'links' => { 'related' => '/books/2/tags' } } } }, + { 'type' => 'books', 'id' => '1' } + ] + } + + normalized = normalize_linkage_order(json) + + assert_equal %w[2 1], normalized['data'].map { |r| r['id'] } + assert_equal({ 'type' => 'people', 'id' => '7' }, normalized['data'][0]['relationships']['author']['data']) + assert_equal({ 'links' => { 'related' => '/books/2/tags' } }, normalized['data'][0]['relationships']['tags']) + end +end diff --git a/test/integration/requests/request_test.rb b/test/integration/requests/request_test.rb index b7895608c..bae5b9eaa 100644 --- a/test/integration/requests/request_test.rb +++ b/test/integration/requests/request_test.rb @@ -884,6 +884,19 @@ def test_pagination_related_resources_data_includes assert_equal 'This is comment 18 on book 1.', json_response['data'][9]['attributes']['body'] end + def test_included_resource_inverse_linkage_with_table_statistics + # Once tables have statistics (autovacuum analyzes them mid-suite in CI), Postgres can plan + # SELECT DISTINCT as a HashAggregate, so the cached and uncached linkage come back in different orders. + ActiveRecord::Base.connection.execute('ANALYZE books') + ActiveRecord::Base.connection.execute('ANALYZE book_comments') + Api::V2::BookResource.paginator :offset + Api::V2::BookCommentResource.paginator :offset + + assert_cacheable_jsonapi_get '/api/v2/books/1/book_comments?page[limit]=10&include=book' + linkage_ids = json_response['included'].first['relationships']['book_comments']['data'].map { |r| r['id'] } + assert_equal %w[51 53 55 57 59 61 63 65 67 69], linkage_ids.sort_by(&:to_i) + end + def test_pagination_empty_results Api::V2::BookResource.paginator :offset Api::V2::BookCommentResource.paginator :offset diff --git a/test/test_helper.rb b/test/test_helper.rb index a02fff9e7..6108362a6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -505,8 +505,8 @@ def assert_cacheable_jsonapi_get(url, cached_classes = :all) end assert_equal( - non_caching_response.pretty_inspect, - json_response.pretty_inspect, + normalize_linkage_order(non_caching_response).pretty_inspect, + normalize_linkage_order(json_response).pretty_inspect, "Cache warmup response must match normal response" ) @@ -515,8 +515,8 @@ def assert_cacheable_jsonapi_get(url, cached_classes = :all) end assert_equal( - non_caching_response.pretty_inspect, - json_response.pretty_inspect, + normalize_linkage_order(non_caching_response).pretty_inspect, + normalize_linkage_order(json_response).pretty_inspect, "Cached response must match normal response" ) assert_equal 0, cached[:total][:misses], "Cached response must not cause any cache misses" @@ -584,8 +584,8 @@ def assert_cacheable_get(action, **args) "Cache (mode: #{mode}) #{phase} response status must match normal response" ) assert_equal( - non_caching_response.pretty_inspect, - json_response_sans_all_backtraces.pretty_inspect, + normalize_linkage_order(non_caching_response).pretty_inspect, + normalize_linkage_order(json_response_sans_all_backtraces).pretty_inspect, "Cache (mode: #{mode}) #{phase} response body must match normal response" ) assert_operator(