infinum_json_api_setup 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.overcommit.yml +26 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +29 -0
  6. data/.ruby-version +1 -0
  7. data/.simplecov +3 -0
  8. data/Gemfile +10 -0
  9. data/Gemfile.lock +268 -0
  10. data/README.md +156 -0
  11. data/Rakefile +1 -0
  12. data/bin/setup +18 -0
  13. data/infinum_json_api_setup.gemspec +30 -0
  14. data/lib/generators/infinum_json_api_setup/install_generator.rb +12 -0
  15. data/lib/generators/infinum_json_api_setup/templates/config/locales/json_api.en.yml +21 -0
  16. data/lib/infinum_json_api_setup/error.rb +61 -0
  17. data/lib/infinum_json_api_setup/json_api/content_negotiation.rb +29 -0
  18. data/lib/infinum_json_api_setup/json_api/error_handling.rb +49 -0
  19. data/lib/infinum_json_api_setup/json_api/error_serializer.rb +72 -0
  20. data/lib/infinum_json_api_setup/json_api/request_parsing.rb +17 -0
  21. data/lib/infinum_json_api_setup/json_api/responder.rb +31 -0
  22. data/lib/infinum_json_api_setup/json_api/serializer_options.rb +76 -0
  23. data/lib/infinum_json_api_setup/rails.rb +28 -0
  24. data/lib/infinum_json_api_setup/rspec/helpers/request_helper.rb +80 -0
  25. data/lib/infinum_json_api_setup/rspec/helpers/response_helper.rb +56 -0
  26. data/lib/infinum_json_api_setup/rspec/matchers/have_empty_data.rb +32 -0
  27. data/lib/infinum_json_api_setup/rspec/matchers/have_error_pointer.rb +37 -0
  28. data/lib/infinum_json_api_setup/rspec/matchers/have_resource_count_of.rb +37 -0
  29. data/lib/infinum_json_api_setup/rspec/matchers/include_all_resource_ids.rb +51 -0
  30. data/lib/infinum_json_api_setup/rspec/matchers/include_all_resource_ids_sorted.rb +21 -0
  31. data/lib/infinum_json_api_setup/rspec/matchers/include_all_resource_string_ids.rb +17 -0
  32. data/lib/infinum_json_api_setup/rspec/matchers/include_error_detail.rb +37 -0
  33. data/lib/infinum_json_api_setup/rspec/matchers/include_related_resource.rb +42 -0
  34. data/lib/infinum_json_api_setup/rspec/matchers/json_body_matcher.rb +42 -0
  35. data/lib/infinum_json_api_setup/rspec/matchers/schema_matchers.rb +29 -0
  36. data/lib/infinum_json_api_setup/rspec.rb +21 -0
  37. data/lib/infinum_json_api_setup/version.rb +3 -0
  38. data/lib/infinum_json_api_setup.rb +16 -0
  39. data/spec/controllers/api/v1/base_controller_spec.rb +9 -0
  40. data/spec/dummy/Rakefile +6 -0
  41. data/spec/dummy/app/assets/config/manifest.js +1 -0
  42. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  43. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  44. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  45. data/spec/dummy/app/controllers/api/v1/base_controller.rb +11 -0
  46. data/spec/dummy/app/controllers/api/v1/locations_controller.rb +64 -0
  47. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  48. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  49. data/spec/dummy/app/javascript/packs/application.js +15 -0
  50. data/spec/dummy/app/jobs/application_job.rb +7 -0
  51. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  52. data/spec/dummy/app/models/application_record.rb +3 -0
  53. data/spec/dummy/app/models/concerns/.keep +0 -0
  54. data/spec/dummy/app/models/location.rb +38 -0
  55. data/spec/dummy/app/models/location_label.rb +3 -0
  56. data/spec/dummy/app/policies/application_policy.rb +51 -0
  57. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  58. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  59. data/spec/dummy/app/web/api/v1/locations/label_serializer.rb +13 -0
  60. data/spec/dummy/app/web/api/v1/locations/policy.rb +28 -0
  61. data/spec/dummy/app/web/api/v1/locations/query.rb +12 -0
  62. data/spec/dummy/app/web/api/v1/locations/serializer.rb +15 -0
  63. data/spec/dummy/bin/rails +4 -0
  64. data/spec/dummy/bin/rake +4 -0
  65. data/spec/dummy/bin/setup +33 -0
  66. data/spec/dummy/config/application.rb +39 -0
  67. data/spec/dummy/config/boot.rb +5 -0
  68. data/spec/dummy/config/cable.yml +10 -0
  69. data/spec/dummy/config/database.yml +15 -0
  70. data/spec/dummy/config/environment.rb +5 -0
  71. data/spec/dummy/config/environments/development.rb +65 -0
  72. data/spec/dummy/config/environments/production.rb +114 -0
  73. data/spec/dummy/config/environments/test.rb +60 -0
  74. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  75. data/spec/dummy/config/initializers/backtrace_silencers.rb +10 -0
  76. data/spec/dummy/config/initializers/cors.rb +16 -0
  77. data/spec/dummy/config/initializers/filter_parameter_logging.rb +6 -0
  78. data/spec/dummy/config/initializers/inflections.rb +16 -0
  79. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  80. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  81. data/spec/dummy/config/locales/en.yml +33 -0
  82. data/spec/dummy/config/locales/json_api.en.yml +21 -0
  83. data/spec/dummy/config/puma.rb +43 -0
  84. data/spec/dummy/config/routes.rb +7 -0
  85. data/spec/dummy/config/storage.yml +34 -0
  86. data/spec/dummy/config.ru +6 -0
  87. data/spec/dummy/db/migrate/20210709100750_create_locations.rb +10 -0
  88. data/spec/dummy/db/migrate/20210714104736_create_location_labels.rb +10 -0
  89. data/spec/dummy/db/schema.rb +34 -0
  90. data/spec/dummy/log/.keep +0 -0
  91. data/spec/dummy/storage/.keep +0 -0
  92. data/spec/factories/location.rb +11 -0
  93. data/spec/factories/location_label.rb +6 -0
  94. data/spec/infinum_json_api_setup/rspec/matchers/have_empty_data_spec.rb +43 -0
  95. data/spec/infinum_json_api_setup/rspec/matchers/have_error_pointer_spec.rb +43 -0
  96. data/spec/infinum_json_api_setup/rspec/matchers/have_resource_count_of_spec.rb +43 -0
  97. data/spec/infinum_json_api_setup/rspec/matchers/include_all_resource_ids_sorted_spec.rb +53 -0
  98. data/spec/infinum_json_api_setup/rspec/matchers/include_all_resource_ids_spec.rb +49 -0
  99. data/spec/infinum_json_api_setup/rspec/matchers/include_all_resource_string_ids_spec.rb +49 -0
  100. data/spec/infinum_json_api_setup/rspec/matchers/include_error_detail_spec.rb +43 -0
  101. data/spec/infinum_json_api_setup/rspec/matchers/include_related_resource_spec.rb +43 -0
  102. data/spec/infinum_json_api_setup/rspec/matchers/util/body_parser.rb +30 -0
  103. data/spec/rails_helper.rb +77 -0
  104. data/spec/requests/api/v1/content_negotiation_spec.rb +29 -0
  105. data/spec/requests/api/v1/error_handling_spec.rb +114 -0
  106. data/spec/requests/api/v1/responder_spec.rb +91 -0
  107. data/spec/requests/api/v1/serializer_options_spec.rb +46 -0
  108. data/spec/spec_helper.rb +94 -0
  109. data/spec/support/factory_bot.rb +7 -0
  110. data/spec/support/infinum_json_api_setup.rb +1 -0
  111. data/spec/support/rspec_expectations.rb +5 -0
  112. data/spec/support/test_helpers/matchers/response.rb +17 -0
  113. data/spec/support/test_helpers/request.rb +11 -0
  114. data/spec/support/test_helpers/response.rb +8 -0
  115. metadata +351 -0
@@ -0,0 +1,43 @@
1
+ describe InfinumJsonApiSetup::RSpec::Matchers::HaveResourceCountOf do
2
+ include TestHelpers::Matchers::Response
3
+
4
+ describe 'usage' do
5
+ context 'when data has correct number of items' do
6
+ it 'matches' do
7
+ response = response_with_body(JSON.dump(data: [1, 2]))
8
+
9
+ expect(response).to have_resource_count_of(2)
10
+ end
11
+ end
12
+
13
+ context 'when data has wrong number of items' do
14
+ it 'fails and describes failure reason' do
15
+ response = response_with_body(JSON.dump(data: [1, 2]))
16
+
17
+ expect do
18
+ expect(response).to have_resource_count_of(3)
19
+ end.to fail_with('Expected response data to have 3 items, but had 2')
20
+ end
21
+ end
22
+
23
+ context "when response isn't valid JSON" do
24
+ it 'fails and describes failure reason' do
25
+ response = response_with_body('')
26
+
27
+ expect do
28
+ expect(response).to have_resource_count_of(1)
29
+ end.to fail_with('Failed to parse response body')
30
+ end
31
+ end
32
+
33
+ context "when response doesn't contain data attribute" do
34
+ it 'fails and describes failure reason' do
35
+ response = response_with_body('{}')
36
+
37
+ expect do
38
+ expect(response).to have_resource_count_of(1)
39
+ end.to fail_with('Failed to extract data from response body')
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ describe InfinumJsonApiSetup::RSpec::Matchers::IncludeAllResourceIdsSorted do
2
+ include TestHelpers::Matchers::Response
3
+
4
+ describe 'usage' do
5
+ context "when ID's match" do
6
+ it 'matches' do
7
+ response = response_with_ids([1, 2])
8
+
9
+ expect(response).to include_all_resource_ids_sorted([1, 2])
10
+ end
11
+ end
12
+
13
+ context "when ID's don't match" do
14
+ it 'fails and describes failure reason' do
15
+ response = response_with_ids([1, 2, 3])
16
+
17
+ expect do
18
+ expect(response).to include_all_resource_ids_sorted([4])
19
+ end.to fail_with("Expected response ID's([1, 2, 3]) to match [4]")
20
+ end
21
+ end
22
+
23
+ context "when ID's aren't correctly ordered" do
24
+ it 'fails and describes failure reason' do
25
+ response = response_with_ids([2, 1])
26
+
27
+ expect do
28
+ expect(response).to include_all_resource_ids_sorted([1, 2])
29
+ end.to fail_with("Expected response ID's([2, 1]) to match [1, 2]")
30
+ end
31
+ end
32
+
33
+ context "when response isn't valid JSON" do
34
+ it 'fails and describes failure reason' do
35
+ response = response_with_body('')
36
+
37
+ expect do
38
+ expect(response).to include_all_resource_ids_sorted([1])
39
+ end.to fail_with('Failed to parse response body')
40
+ end
41
+ end
42
+
43
+ context "when response doesn't contain data attribute" do
44
+ it 'fails and describes failure reason' do
45
+ response = response_with_body('{}')
46
+
47
+ expect do
48
+ expect(response).to include_all_resource_ids_sorted([1])
49
+ end.to fail_with('Failed to extract data from response body')
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,49 @@
1
+ describe InfinumJsonApiSetup::RSpec::Matchers::IncludeAllResourceIds do
2
+ include TestHelpers::Matchers::Response
3
+
4
+ describe 'usage' do
5
+ context "when ID's match" do
6
+ it 'matches' do
7
+ response = response_with_ids([1, 2])
8
+
9
+ expect(response).to include_all_resource_ids([1, 2])
10
+ end
11
+
12
+ it "doesn't consider ordering" do
13
+ response = response_with_ids([1, 2, 3])
14
+
15
+ expect(response).to include_all_resource_ids([3, 2, 1])
16
+ end
17
+ end
18
+
19
+ context "when ID's don't match" do
20
+ it 'fails and describes failure reason' do
21
+ response = response_with_ids([1, 2, 3])
22
+
23
+ expect do
24
+ expect(response).to include_all_resource_ids([4])
25
+ end.to fail_with("Expected response ID's([1, 2, 3]) to match [4]")
26
+ end
27
+ end
28
+
29
+ context "when response isn't valid JSON" do
30
+ it 'fails and describes failure reason' do
31
+ response = response_with_body('')
32
+
33
+ expect do
34
+ expect(response).to include_all_resource_ids([1])
35
+ end.to fail_with('Failed to parse response body')
36
+ end
37
+ end
38
+
39
+ context "when response doesn't contain data attribute" do
40
+ it 'fails and describes failure reason' do
41
+ response = response_with_body('{}')
42
+
43
+ expect do
44
+ expect(response).to include_all_resource_ids([1])
45
+ end.to fail_with('Failed to extract data from response body')
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ describe InfinumJsonApiSetup::RSpec::Matchers::IncludeAllResourceStringIds do
2
+ include TestHelpers::Matchers::Response
3
+
4
+ describe 'usage' do
5
+ context "when ID's match" do
6
+ it 'matches' do
7
+ response = response_with_ids(['1', '2'])
8
+
9
+ expect(response).to include_all_resource_string_ids(['1', '2'])
10
+ end
11
+
12
+ it "doesn't consider ordering" do
13
+ response = response_with_ids(['1', '2', '3'])
14
+
15
+ expect(response).to include_all_resource_string_ids(['3', '2', '1'])
16
+ end
17
+ end
18
+
19
+ context "when ID's don't match" do
20
+ it 'fails and describes failure reason' do
21
+ response = response_with_ids(['1', '2', '3'])
22
+
23
+ expect do
24
+ expect(response).to include_all_resource_string_ids(['4'])
25
+ end.to fail_with('Expected response ID\'s(["1", "2", "3"]) to match ["4"]')
26
+ end
27
+ end
28
+
29
+ context "when response isn't valid JSON" do
30
+ it 'fails and describes failure reason' do
31
+ response = response_with_body('')
32
+
33
+ expect do
34
+ expect(response).to include_all_resource_string_ids(['1'])
35
+ end.to fail_with('Failed to parse response body')
36
+ end
37
+ end
38
+
39
+ context "when response doesn't contain data attribute" do
40
+ it 'fails and describes failure reason' do
41
+ response = response_with_body('{}')
42
+
43
+ expect do
44
+ expect(response).to include_all_resource_string_ids(['1'])
45
+ end.to fail_with('Failed to extract data from response body')
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,43 @@
1
+ describe InfinumJsonApiSetup::RSpec::Matchers::IncludeErrorDetail do
2
+ include TestHelpers::Matchers::Response
3
+
4
+ describe 'usage' do
5
+ context 'when body includes error detail' do
6
+ it 'matches' do
7
+ response = response_with_body(JSON.dump(errors: [{ detail: 'a failure' }]))
8
+
9
+ expect(response).to include_error_detail('a failure')
10
+ end
11
+ end
12
+
13
+ context "when body doesn't include specified error detail" do
14
+ it 'fails and describes failure reason' do
15
+ response = response_with_body(JSON.dump(errors: []))
16
+
17
+ expect do
18
+ expect(response).to include_error_detail('a failure')
19
+ end.to fail_with("Expected error details to include 'a failure', but didn't")
20
+ end
21
+ end
22
+
23
+ context "when response isn't valid JSON" do
24
+ it 'fails and describes failure reason' do
25
+ response = response_with_body('')
26
+
27
+ expect do
28
+ expect(response).to include_error_detail('a failure')
29
+ end.to fail_with('Failed to parse response body')
30
+ end
31
+ end
32
+
33
+ context "when response doesn't contain data attribute" do
34
+ it 'fails and describes failure reason' do
35
+ response = response_with_body('{}')
36
+
37
+ expect do
38
+ expect(response).to include_error_detail('a failure')
39
+ end.to fail_with('Failed to extract errors from response body')
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ describe InfinumJsonApiSetup::RSpec::Matchers::IncludeRelatedResource do
2
+ include TestHelpers::Matchers::Response
3
+
4
+ describe 'usage' do
5
+ context 'when body includes related resource' do
6
+ it 'matches' do
7
+ response = response_with_body(JSON.dump(included: [{ type: 'user', id: '1' }]))
8
+
9
+ expect(response).to include_related_resource('user', 1)
10
+ end
11
+ end
12
+
13
+ context "when body doesn't include specified error detail" do
14
+ it 'fails and describes failure reason' do
15
+ response = response_with_body(JSON.dump(included: []))
16
+
17
+ expect do
18
+ expect(response).to include_related_resource('user', 1)
19
+ end.to fail_with("Expected included items to include (type = user, id = 1), but didn't")
20
+ end
21
+ end
22
+
23
+ context "when response isn't valid JSON" do
24
+ it 'fails and describes failure reason' do
25
+ response = response_with_body('')
26
+
27
+ expect do
28
+ expect(response).to include_related_resource('user', 1)
29
+ end.to fail_with('Failed to parse response body')
30
+ end
31
+ end
32
+
33
+ context "when response doesn't contain data attribute" do
34
+ it 'fails and describes failure reason' do
35
+ response = response_with_body('{}')
36
+
37
+ expect do
38
+ expect(response).to include_related_resource('user', 1)
39
+ end.to fail_with('Failed to extract included from response body')
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ require 'json'
2
+
3
+ module InfinumJsonApiSetup
4
+ module RSpec
5
+ module Matchers
6
+ module Util
7
+ class BodyParser
8
+ # @param [String] attribute
9
+ def initialize(attribute)
10
+ @attribute = attribute
11
+ end
12
+
13
+ # @param [ActionDispatch::TestResponse] response
14
+ # @return [Array]
15
+ def process(response)
16
+ [true, JSON.parse(response.body).fetch(attribute)]
17
+ rescue JSON::ParserError => _e
18
+ [false, 'Failed to parse response body']
19
+ rescue KeyError => _e
20
+ [false, "Failed to extract #{attribute} from response body"]
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :attribute
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,77 @@
1
+ require 'simplecov'
2
+
3
+ require 'spec_helper'
4
+ ENV['RAILS_ENV'] ||= 'test'
5
+ require File.expand_path('../spec/dummy/config/environment', __dir__)
6
+ require 'rspec/rails'
7
+
8
+ Dir.chdir File.expand_path('dummy', __dir__) do
9
+ system 'bundle exec rails generate infinum_json_api_setup:install -f -q'
10
+ end
11
+
12
+ # Add additional requires below this line. Rails is not loaded until this point!
13
+ require 'jsonapi/query_builder'
14
+ require 'jsonapi/serializer'
15
+ require 'pundit'
16
+
17
+ # Requires supporting ruby files with custom matchers and macros, etc, in
18
+ # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
19
+ # run as spec files by default. This means that files in spec/support that end
20
+ # in _spec.rb will both be required and run as specs, causing the specs to be
21
+ # run twice. It is recommended that you do not name files matching this glob to
22
+ # end with _spec.rb. You can configure this pattern with the --pattern
23
+ # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
24
+ #
25
+ # The following line is provided for convenience purposes. It has the downside
26
+ # of increasing the boot-up time by auto-requiring all files in the support
27
+ # directory. Alternatively, in the individual `*_spec.rb` files, manually
28
+ # require only the support files necessary.
29
+ #
30
+ Dir[File.expand_path('support/**/*.rb', __dir__)].sort.each { |f| require f }
31
+
32
+ # Checks for pending migrations and applies them before tests are run.
33
+ # If you are not using ActiveRecord, you can remove these lines.
34
+ begin
35
+ ActiveRecord::Migration.maintain_test_schema!
36
+ rescue ActiveRecord::PendingMigrationError => e
37
+ puts e.to_s.strip
38
+ exit 1
39
+ end
40
+ RSpec.configure do |config|
41
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
42
+ config.fixture_path = "#{::Rails.root}/spec/fixtures"
43
+
44
+ # If you're not using ActiveRecord, or you'd prefer not to run each of your
45
+ # examples within a transaction, remove the following line or assign false
46
+ # instead of true.
47
+ config.use_transactional_fixtures = true
48
+
49
+ # You can uncomment this line to turn off ActiveRecord support entirely.
50
+ # config.use_active_record = false
51
+
52
+ # RSpec Rails can automatically mix in different behaviours to your tests
53
+ # based on their file location, for example enabling you to call `get` and
54
+ # `post` in specs under `spec/controllers`.
55
+ #
56
+ # You can disable this behaviour by removing the line below, and instead
57
+ # explicitly tag your specs with their type, e.g.:
58
+ #
59
+ # RSpec.describe UsersController, type: :controller do
60
+ # # ...
61
+ # end
62
+ #
63
+ # The different available types are documented in the features, such as in
64
+ # https://relishapp.com/rspec/rspec-rails/docs
65
+ config.infer_spec_type_from_file_location!
66
+
67
+ # Filter lines from Rails gems in backtraces.
68
+ config.filter_rails_from_backtrace!
69
+ # arbitrary gems may also be filtered via:
70
+ # config.filter_gems_from_backtrace("gem name")
71
+
72
+ # Enables running rspec --only-failures
73
+ config.example_status_persistence_file_path = 'spec/examples.txt'
74
+
75
+ config.include TestHelpers::Request, type: :request
76
+ config.include TestHelpers::Response, type: :request
77
+ end
@@ -0,0 +1,29 @@
1
+ describe 'Content negotiation', type: :request do
2
+ it 'passes through requests demanding JSON:API compliant response' do
3
+ get '/api/v1/locations', headers: { accept: 'application/vnd.api+json' }
4
+
5
+ expect(response).to have_http_status(:ok)
6
+ end
7
+
8
+ it 'responds with 406 NotAcceptable to requests demanding non JSON:API compliant reponse' do
9
+ get '/api/v1/locations', headers: { accept: 'application/json' }
10
+
11
+ expect(response).to have_http_status(:not_acceptable)
12
+ end
13
+
14
+ it 'responds with 415 UnsupportedMediaType to requests containing non JSON:API compliant body' do
15
+ post '/api/v1/locations', params: { user: { name: 'Harry' } },
16
+ headers: { accept: 'application/vnd.api+json',
17
+ 'content-type': 'application/x-www-form-urlencoded' }
18
+
19
+ expect(response).to have_http_status(:unsupported_media_type)
20
+ end
21
+
22
+ it 'skips Content-Type check when request has empty body' do
23
+ get '/api/v1/locations', params: { user: { name: 'Harry' } },
24
+ headers: { accept: 'application/vnd.api+json',
25
+ 'content-type': 'application/x-www-form-urlencoded' }
26
+
27
+ expect(response).to have_http_status(:ok)
28
+ end
29
+ end
@@ -0,0 +1,114 @@
1
+ describe 'Error handling', type: :request do
2
+ context "when request doesn't contain required parameters" do
3
+ it 'responds with 400 BadRequest' do
4
+ post '/api/v1/locations', params: {}, headers: default_headers
5
+
6
+ expect(response).to have_http_status(:bad_request)
7
+ error = json_response['errors'].first
8
+ expect(error['title']).to eq('Bad Request')
9
+ expect(error['detail']).to match('param is missing or the value is empty: location')
10
+ end
11
+ end
12
+
13
+ context 'when request contains semantical errors' do
14
+ it 'responds with 422 UnprocessableEntity and error details' do
15
+ params = { latitude: 999, longitude: 999 }
16
+
17
+ post '/api/v1/locations', params: { location: params }.to_json, headers: default_headers
18
+
19
+ expect(response).to have_http_status(:unprocessable_entity)
20
+ expect(json_response['errors'].map { |details| details['source'] }).to contain_exactly(
21
+ { 'parameter' => 'latitude', 'pointer' => 'data/attributes/latitude' },
22
+ { 'parameter' => 'longitude', 'pointer' => 'data/attributes/longitude' }
23
+ )
24
+ end
25
+ end
26
+
27
+ context "when ActiveRecord can't find requested record" do
28
+ it 'responds with 404 NotFound' do
29
+ get '/api/v1/locations/0', headers: default_headers
30
+
31
+ expect(response).to have_http_status(:not_found)
32
+ expect(json_response['errors'].first['title']).to eq('Not found')
33
+ expect(json_response['errors'].first['detail']).to eq('Resource not found')
34
+ end
35
+ end
36
+
37
+ context 'when client is not authorized to perform requested action' do
38
+ it 'responds with 403 Forbidden' do
39
+ loc = create(:location, :fourth_quadrant)
40
+ get "/api/v1/locations/#{loc.id}", headers: default_headers
41
+
42
+ expect(response).to have_http_status(:forbidden)
43
+ expect(json_response['errors'].first['title']).to eq('Forbidden')
44
+ expect(json_response['errors'].first['detail'])
45
+ .to eq('You are not allowed to perform this action')
46
+ end
47
+ end
48
+
49
+ context 'when request contains unpermitted sort params' do
50
+ let(:bugsnag) { class_double('Bugsnag', notify: nil) }
51
+
52
+ before do
53
+ stub_const('Bugsnag', bugsnag)
54
+ end
55
+
56
+ it 'notifies Bugsnag about the incident' do
57
+ get '/api/v1/locations?sort=-title', headers: default_headers
58
+
59
+ expect(bugsnag).to have_received(:notify)
60
+ end
61
+
62
+ it 'responds with 400 BadRequest' do
63
+ get '/api/v1/locations?sort=-title', headers: default_headers
64
+
65
+ expect(response).to have_http_status(:bad_request)
66
+ error = json_response['errors'].first
67
+ expect(error['title']).to eq('Bad Request')
68
+ expect(error['detail']).to eq('title is not a permitted sort attribute')
69
+ end
70
+ end
71
+
72
+ context 'when action processing causes PG::Error' do
73
+ let(:location_model) { class_double('Location') }
74
+ let(:bugsnag) { class_double('Bugsnag', notify: nil) }
75
+
76
+ before do
77
+ stub_const('Bugsnag', bugsnag)
78
+ stub_const('Location', location_model)
79
+ allow(location_model).to receive(:find) { raise PG::Error }
80
+ end
81
+
82
+ it 'notifies Bugsnag about the incident' do
83
+ get '/api/v1/locations/0', headers: default_headers
84
+
85
+ expect(bugsnag).to have_received(:notify)
86
+ end
87
+
88
+ it 'responds with 500 InternalServerError' do
89
+ get '/api/v1/locations/0', headers: default_headers
90
+
91
+ expect(response).to have_http_status(:internal_server_error)
92
+ expect(json_response['errors'].first['title']).to eq('Internal Server Error')
93
+ expect(json_response['errors'].first['detail']).to eq('Something went wrong')
94
+ end
95
+ end
96
+
97
+ context 'when client requests invalid locale' do
98
+ it 'responds with 500 InternalServerError' do
99
+ get '/api/v1/locations?locale=--', headers: default_headers
100
+
101
+ expect(response).to have_http_status(:bad_request)
102
+ expect(json_response['errors'].first['title']).to eq('Bad Request')
103
+ end
104
+ end
105
+
106
+ context 'when client request contains unparsable params' do
107
+ it 'responds with 400 BadRequest' do
108
+ post '/api/v1/locations', params: '{', headers: default_headers
109
+
110
+ expect(response).to have_http_status(:bad_request)
111
+ expect(json_response['errors'].first['title']).to eq('Bad Request')
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,91 @@
1
+ describe 'Responder', type: :request do
2
+ context 'when GET request' do
3
+ it 'responds with 200 OK' do
4
+ get '/api/v1/locations', headers: default_headers
5
+
6
+ expect(response).to have_http_status(:ok)
7
+ end
8
+ end
9
+
10
+ context 'when POST request' do
11
+ context 'without errors' do
12
+ it 'responds with 201 Created' do
13
+ params = { latitude: 1, longitude: 1 }
14
+
15
+ post '/api/v1/locations', params: { location: params }.to_json, headers: default_headers
16
+
17
+ expect(response).to have_http_status(:created)
18
+ end
19
+ end
20
+
21
+ context 'with errors' do
22
+ it 'responds with 400 BadRequest' do
23
+ post '/api/v1/locations', params: {}, headers: default_headers
24
+
25
+ expect(response).to have_http_status(:bad_request)
26
+ end
27
+ end
28
+ end
29
+
30
+ context 'when PUT request' do
31
+ context 'without errors' do
32
+ it 'responds with 200 OK' do
33
+ location = create(:location, latitude: 1, longitude: 1)
34
+ params = { latitude: 2 }
35
+
36
+ put "/api/v1/locations/#{location.id}", params: { location: params }.to_json,
37
+ headers: default_headers
38
+
39
+ expect(response).to have_http_status(:ok)
40
+ end
41
+ end
42
+
43
+ context 'with errors' do
44
+ it 'responds with 422 UnprocessableEntity' do
45
+ location = create(:location, latitude: 1, longitude: 1)
46
+ params = { latitude: 999 }
47
+
48
+ put "/api/v1/locations/#{location.id}", params: { location: params }.to_json,
49
+ headers: default_headers
50
+
51
+ expect(response).to have_http_status(:unprocessable_entity)
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'when PATCH request' do
57
+ context 'without errors' do
58
+ it 'responds with 200 OK' do
59
+ location = create(:location, latitude: 1, longitude: 1)
60
+ params = { latitude: 2 }
61
+
62
+ patch "/api/v1/locations/#{location.id}", params: { location: params }.to_json,
63
+ headers: default_headers
64
+
65
+ expect(response).to have_http_status(:ok)
66
+ end
67
+ end
68
+
69
+ context 'with errors' do
70
+ it 'responds with 422 UnprocessableEntity' do
71
+ location = create(:location, latitude: 1, longitude: 1)
72
+ params = { latitude: 999 }
73
+
74
+ patch "/api/v1/locations/#{location.id}", params: { location: params }.to_json,
75
+ headers: default_headers
76
+
77
+ expect(response).to have_http_status(:unprocessable_entity)
78
+ end
79
+ end
80
+ end
81
+
82
+ context 'when DELETE request' do
83
+ it 'responds with 204 NoContent' do
84
+ location = create(:location, latitude: 1, longitude: 1)
85
+
86
+ delete "/api/v1/locations/#{location.id}", headers: default_headers
87
+
88
+ expect(response).to have_http_status(:no_content)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,46 @@
1
+ require 'cgi'
2
+ require 'uri'
3
+
4
+ describe 'Serializer options', type: :request do
5
+ it 'adds meta with pagination information' do
6
+ get '/api/v1/locations', params: {}, headers: default_headers
7
+
8
+ expect(response).to have_http_status(:ok)
9
+ expect(json_response).to have_key('meta')
10
+ expect(json_response['meta'].keys).to include(
11
+ 'current_page', 'total_pages', 'total_count', 'padding', 'page_size', 'max_page_size'
12
+ )
13
+ end
14
+
15
+ it 'adds links' do
16
+ get '/api/v1/locations', params: {}, headers: default_headers
17
+
18
+ expect(response).to have_http_status(:ok)
19
+ expect(json_response).to have_key('links')
20
+ expect(json_response['links'].keys).to include('self', 'first', 'last')
21
+ end
22
+
23
+ it 'adds client requested fields information to links' do
24
+ q = { fields: { locations: 'latitude' } }
25
+ get "/api/v1/locations?#{q.to_query}", params: {}, headers: default_headers
26
+
27
+ expect(response).to have_http_status(:ok)
28
+ expect(query_params(json_response.dig('links', 'self')))
29
+ .to include('fields[locations]' => ['latitude'])
30
+ end
31
+
32
+ it 'adds client requested include information to links' do
33
+ q = { include: 'labels' }
34
+ get "/api/v1/locations?#{q.to_query}", params: {}, headers: default_headers
35
+
36
+ expect(response).to have_http_status(:ok)
37
+ expect(query_params(json_response.dig('links', 'self')))
38
+ .to include('include' => ['labels'])
39
+ end
40
+
41
+ private
42
+
43
+ def query_params(link)
44
+ CGI.parse(URI(link).query)
45
+ end
46
+ end