diff --git a/jsonapi-resources.gemspec b/jsonapi-resources.gemspec index 7be374b3d..52c1f1909 100644 --- a/jsonapi-resources.gemspec +++ b/jsonapi-resources.gemspec @@ -31,6 +31,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'database_cleaner' spec.add_dependency 'activerecord', '>= 7.1', '< 9.0' # versions 7.1, 7.2, 8.0, 8.1, and above, but not 9.0 spec.add_dependency 'railties', '>= 7.1', '< 9.0' # versions 7.1, 7.2, 8.0, 8.1, and above, but not 9.0 - spec.add_dependency 'rack', '~> 2.0' + spec.add_dependency 'rack', '~> 3.0' spec.add_dependency 'concurrent-ruby' end diff --git a/lib/jsonapi/error.rb b/lib/jsonapi/error.rb index 8eeb87cff..9e23d30ca 100644 --- a/lib/jsonapi/error.rb +++ b/lib/jsonapi/error.rb @@ -17,7 +17,7 @@ def initialize(options = {}) @source = options[:source] @links = options[:links] - @status = Rack::Utils.status_code(options[:status]).to_s + @status = status_code(options[:status]).to_s @meta = options[:meta] end @@ -48,11 +48,26 @@ def update_with_overrides(error_object_overrides) if error_object_overrides[:status] # :nocov: - @status = Rack::Utils::SYMBOL_TO_STATUS_CODE[error_object_overrides[:status]].to_s + @status = status_code(error_object_overrides[:status]).to_s # :nocov: end @meta = error_object_overrides[:meta] || @meta end + + private + + # Extracted from Rack 2 + def status_code(status) + if status.nil? + raise ArgumentError, "Status code is required" + end + + if status.is_a?(Symbol) + Rack::Utils::SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" } + else + status.to_i + end + end end class Warning diff --git a/lib/jsonapi/exceptions.rb b/lib/jsonapi/exceptions.rb index e917118cf..a0c437eea 100644 --- a/lib/jsonapi/exceptions.rb +++ b/lib/jsonapi/exceptions.rb @@ -498,7 +498,7 @@ def errors def json_api_error(attr_key, message) create_error_object(code: JSONAPI::VALIDATION_ERROR, - status: :unprocessable_entity, + status: :unprocessable_content, title: message, detail: detail(attr_key, message), source: { pointer: pointer(attr_key) }, @@ -532,7 +532,7 @@ def general_error?(attr_key) class SaveFailed < Error def errors [create_error_object(code: JSONAPI::SAVE_FAILED, - status: :unprocessable_entity, + status: :unprocessable_content, title: I18n.translate('jsonapi-resources.exceptions.save_failed.title', default: 'Save failed or was cancelled'), detail: I18n.translate('jsonapi-resources.exceptions.save_failed.detail', diff --git a/lib/jsonapi/resources/version.rb b/lib/jsonapi/resources/version.rb index f54a58b95..32ebf10d8 100644 --- a/lib/jsonapi/resources/version.rb +++ b/lib/jsonapi/resources/version.rb @@ -1,5 +1,5 @@ module JSONAPI module Resources - VERSION = '0.4.0' + VERSION = '0.4.1' end end diff --git a/test/controllers/controller_test.rb b/test/controllers/controller_test.rb index e2568f979..a2f220664 100644 --- a/test/controllers/controller_test.rb +++ b/test/controllers/controller_test.rb @@ -761,7 +761,7 @@ def test_create_link_to_missing_object } } - assert_response :unprocessable_entity + assert_response :unprocessable_content # TODO: check if this validation is working assert_match /author - can't be blank/, response.body assert_nil response.location @@ -864,7 +864,7 @@ def test_create_with_invalid_data } } - assert_response :unprocessable_entity + assert_response :unprocessable_content assert_equal "/data/relationships/author", json_response['errors'][0]['source']['pointer'] assert_equal "can't be blank", json_response['errors'][0]['title'] @@ -2019,7 +2019,7 @@ def test_delete_with_validation_error_base assert_equal "can't destroy me", json_response['errors'][0]['title'] assert_equal "/data", json_response['errors'][0]['source']['pointer'] - assert_response :unprocessable_entity + assert_response :unprocessable_content end def test_delete_with_validation_error_attr @@ -2028,7 +2028,7 @@ def test_delete_with_validation_error_attr assert_equal "is locked", json_response['errors'][0]['title'] assert_equal "/data/attributes/title", json_response['errors'][0]['source']['pointer'] - assert_response :unprocessable_entity + assert_response :unprocessable_content end def test_delete_single @@ -2631,7 +2631,7 @@ def test_create_validations_missing_attribute } } - assert_response :unprocessable_entity + assert_response :unprocessable_content assert_equal 2, json_response['errors'].size assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][0]['code'] assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][1]['code'] @@ -2653,7 +2653,7 @@ def test_update_validations_missing_attribute } } - assert_response :unprocessable_entity + assert_response :unprocessable_content assert_equal 1, json_response['errors'].size assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][0]['code'] assert_match /name - can't be blank/, response.body @@ -3183,7 +3183,7 @@ def test_create_with_invalid_data } } - assert_response :unprocessable_entity + assert_response :unprocessable_content assert_equal "/data/attributes/spouse-name", json_response['errors'][0]['source']['pointer'] assert_equal "can't be blank", json_response['errors'][0]['title'] @@ -3779,7 +3779,7 @@ def test_save_model_callbacks_fail } } - assert_response :unprocessable_entity + assert_response :unprocessable_content assert_match /Save failed or was cancelled/, json_response['errors'][0]['detail'] end end @@ -4077,7 +4077,7 @@ def test_delete_with_validation_error_base_on_resource assert_equal "can't destroy me", json_response['errors'][0]['title'] assert_equal "/data/attributes/base", json_response['errors'][0]['source']['pointer'] - assert_response :unprocessable_entity + assert_response :unprocessable_content end end diff --git a/test/integration/requests/request_test.rb b/test/integration/requests/request_test.rb index 6ce002c7e..b48914afe 100644 --- a/test/integration/requests/request_test.rb +++ b/test/integration/requests/request_test.rb @@ -453,6 +453,9 @@ def test_post_single_minimal_invalid } assert_jsonapi_response 422 + assert_equal JSONAPI::VALIDATION_ERROR, json_response['errors'][0]['code'] + assert_equal '422', json_response['errors'][0]['status'] + assert_match "can't be blank", json_response['errors'][0]['title'] end def test_update_relationship_without_content_type diff --git a/test/unit/jsonapi_request/jsonapi_error_test.rb b/test/unit/jsonapi_request/jsonapi_error_test.rb new file mode 100644 index 000000000..801b8204d --- /dev/null +++ b/test/unit/jsonapi_request/jsonapi_error_test.rb @@ -0,0 +1,45 @@ +require File.expand_path('../../../test_helper', __FILE__) + +class JSONAPIErrorTest < Minitest::Test + def test_status_code_requires_status + error = assert_raises(ArgumentError) do + JSONAPI::Error.new(code: JSONAPI::BAD_REQUEST) + end + + assert_equal('Status code is required', error.message) + end + + def test_status_code_accepts_symbol + error = JSONAPI::Error.new(code: JSONAPI::VALIDATION_ERROR, status: :unprocessable_content) + + assert_equal('422', error.status) + end + + def test_status_code_accepts_integer + error = JSONAPI::Error.new(code: JSONAPI::VALIDATION_ERROR, status: 422) + + assert_equal('422', error.status) + end + + def test_status_code_accepts_string + error = JSONAPI::Error.new(code: JSONAPI::VALIDATION_ERROR, status: '422') + + assert_equal('422', error.status) + end + + def test_status_code_rejects_unknown_symbol + error = assert_raises(ArgumentError) do + JSONAPI::Error.new(code: JSONAPI::BAD_REQUEST, status: :not_a_real_status) + end + + assert_equal('Unrecognized status code :not_a_real_status', error.message) + end + + def test_status_code_rejects_nil + error = assert_raises(ArgumentError) do + JSONAPI::Error.new(code: JSONAPI::BAD_REQUEST, status: nil) + end + + assert_equal('Status code is required', error.message) + end +end