infinum_json_api_setup 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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