Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions test/helpers/functional_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
43 changes: 43 additions & 0 deletions test/helpers/functional_helpers_test.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions test/integration/requests/request_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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"
Expand Down Expand Up @@ -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(
Expand Down
Loading