apes 1.0.0

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 (122) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +82 -0
  4. data/.travis-gemfile +15 -0
  5. data/.travis.yml +15 -0
  6. data/.yardopts +1 -0
  7. data/CHANGELOG.md +3 -0
  8. data/Gemfile +22 -0
  9. data/README.md +177 -0
  10. data/Rakefile +44 -0
  11. data/apes.gemspec +34 -0
  12. data/doc/Apes.html +130 -0
  13. data/doc/Apes/Concerns.html +127 -0
  14. data/doc/Apes/Concerns/Errors.html +1089 -0
  15. data/doc/Apes/Concerns/Pagination.html +636 -0
  16. data/doc/Apes/Concerns/Request.html +766 -0
  17. data/doc/Apes/Concerns/Response.html +940 -0
  18. data/doc/Apes/Controller.html +1100 -0
  19. data/doc/Apes/Errors.html +125 -0
  20. data/doc/Apes/Errors/AuthenticationError.html +133 -0
  21. data/doc/Apes/Errors/BadRequestError.html +157 -0
  22. data/doc/Apes/Errors/BaseError.html +320 -0
  23. data/doc/Apes/Errors/InvalidDataError.html +157 -0
  24. data/doc/Apes/Errors/MissingDataError.html +157 -0
  25. data/doc/Apes/Model.html +378 -0
  26. data/doc/Apes/PaginationCursor.html +2138 -0
  27. data/doc/Apes/RuntimeConfiguration.html +909 -0
  28. data/doc/Apes/Serializers.html +125 -0
  29. data/doc/Apes/Serializers/JSON.html +389 -0
  30. data/doc/Apes/Serializers/JWT.html +452 -0
  31. data/doc/Apes/Serializers/List.html +347 -0
  32. data/doc/Apes/UrlsParser.html +1432 -0
  33. data/doc/Apes/Validators.html +125 -0
  34. data/doc/Apes/Validators/BaseValidator.html +278 -0
  35. data/doc/Apes/Validators/BooleanValidator.html +494 -0
  36. data/doc/Apes/Validators/EmailValidator.html +350 -0
  37. data/doc/Apes/Validators/PhoneValidator.html +375 -0
  38. data/doc/Apes/Validators/ReferenceValidator.html +372 -0
  39. data/doc/Apes/Validators/TimestampValidator.html +640 -0
  40. data/doc/Apes/Validators/UuidValidator.html +372 -0
  41. data/doc/Apes/Validators/ZipCodeValidator.html +372 -0
  42. data/doc/Apes/Version.html +189 -0
  43. data/doc/ApplicationController.html +547 -0
  44. data/doc/Concerns.html +128 -0
  45. data/doc/Concerns/ErrorHandling.html +826 -0
  46. data/doc/Concerns/PaginationHandling.html +463 -0
  47. data/doc/Concerns/RequestHandling.html +512 -0
  48. data/doc/Concerns/ResponseHandling.html +579 -0
  49. data/doc/Errors.html +126 -0
  50. data/doc/Errors/AuthenticationError.html +123 -0
  51. data/doc/Errors/BadRequestError.html +147 -0
  52. data/doc/Errors/BaseError.html +289 -0
  53. data/doc/Errors/InvalidDataError.html +147 -0
  54. data/doc/Errors/MissingDataError.html +147 -0
  55. data/doc/Model.html +315 -0
  56. data/doc/PaginationCursor.html +764 -0
  57. data/doc/Serializers.html +126 -0
  58. data/doc/Serializers/JSON.html +253 -0
  59. data/doc/Serializers/JWT.html +253 -0
  60. data/doc/Serializers/List.html +245 -0
  61. data/doc/Validators.html +126 -0
  62. data/doc/Validators/BaseValidator.html +209 -0
  63. data/doc/Validators/BooleanValidator.html +391 -0
  64. data/doc/Validators/EmailValidator.html +298 -0
  65. data/doc/Validators/PhoneValidator.html +313 -0
  66. data/doc/Validators/ReferenceValidator.html +284 -0
  67. data/doc/Validators/TimestampValidator.html +476 -0
  68. data/doc/Validators/UuidValidator.html +310 -0
  69. data/doc/Validators/ZipCodeValidator.html +310 -0
  70. data/doc/_index.html +435 -0
  71. data/doc/class_list.html +58 -0
  72. data/doc/css/common.css +1 -0
  73. data/doc/css/full_list.css +57 -0
  74. data/doc/css/style.css +339 -0
  75. data/doc/file.README.html +252 -0
  76. data/doc/file_list.html +60 -0
  77. data/doc/frames.html +26 -0
  78. data/doc/index.html +252 -0
  79. data/doc/js/app.js +219 -0
  80. data/doc/js/full_list.js +181 -0
  81. data/doc/js/jquery.js +4 -0
  82. data/doc/method_list.html +615 -0
  83. data/doc/top-level-namespace.html +112 -0
  84. data/lib/apes.rb +40 -0
  85. data/lib/apes/concerns/errors.rb +111 -0
  86. data/lib/apes/concerns/pagination.rb +81 -0
  87. data/lib/apes/concerns/request.rb +237 -0
  88. data/lib/apes/concerns/response.rb +74 -0
  89. data/lib/apes/controller.rb +77 -0
  90. data/lib/apes/errors.rb +38 -0
  91. data/lib/apes/model.rb +94 -0
  92. data/lib/apes/pagination_cursor.rb +152 -0
  93. data/lib/apes/runtime_configuration.rb +80 -0
  94. data/lib/apes/serializers.rb +88 -0
  95. data/lib/apes/urls_parser.rb +233 -0
  96. data/lib/apes/validators.rb +234 -0
  97. data/lib/apes/version.rb +24 -0
  98. data/spec/apes/concerns/errors_spec.rb +141 -0
  99. data/spec/apes/concerns/pagination_spec.rb +114 -0
  100. data/spec/apes/concerns/request_spec.rb +244 -0
  101. data/spec/apes/concerns/response_spec.rb +79 -0
  102. data/spec/apes/controller_spec.rb +54 -0
  103. data/spec/apes/errors_spec.rb +14 -0
  104. data/spec/apes/models_spec.rb +148 -0
  105. data/spec/apes/pagination_cursor_spec.rb +113 -0
  106. data/spec/apes/runtime_configuration_spec.rb +100 -0
  107. data/spec/apes/serializers_spec.rb +70 -0
  108. data/spec/apes/urls_parser_spec.rb +150 -0
  109. data/spec/apes/validators_spec.rb +237 -0
  110. data/spec/spec_helper.rb +30 -0
  111. data/views/_included.json.jbuilder +9 -0
  112. data/views/_pagination.json.jbuilder +9 -0
  113. data/views/collection.json.jbuilder +4 -0
  114. data/views/errors/400.json.jbuilder +9 -0
  115. data/views/errors/403.json.jbuilder +7 -0
  116. data/views/errors/404.json.jbuilder +6 -0
  117. data/views/errors/422.json.jbuilder +19 -0
  118. data/views/errors/500.json.jbuilder +12 -0
  119. data/views/errors/501.json.jbuilder +7 -0
  120. data/views/layouts/general.json.jbuilder +36 -0
  121. data/views/object.json.jbuilder +4 -0
  122. metadata +262 -0
@@ -0,0 +1,24 @@
1
+ #
2
+ # This file is part of the apes gem. Copyright (C) 2016 and above Shogun <shogun@cowtech.it>.
3
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
4
+ #
5
+
6
+ # A tiny JSON API framework for Ruby on Rails.
7
+ module Apes
8
+ # The current version of apes, according to semantic versioning.
9
+ #
10
+ # @see http://semver.org
11
+ module Version
12
+ # The major version.
13
+ MAJOR = 1
14
+
15
+ # The minor version.
16
+ MINOR = 0
17
+
18
+ # The patch version.
19
+ PATCH = 0
20
+
21
+ # The current version of apes.
22
+ STRING = [MAJOR, MINOR, PATCH].compact.join(".")
23
+ end
24
+ end
@@ -0,0 +1,141 @@
1
+ require "spec_helper"
2
+
3
+ describe Apes::Concerns::Errors do
4
+ class ErrorHandlingMockContainer
5
+ include Apes::Concerns::Errors
6
+
7
+ def request_valid_content_type
8
+ "FOO"
9
+ end
10
+ end
11
+
12
+ class ErrorHandlingMockModel
13
+ include ActiveModel::Validations
14
+
15
+ attr_reader :field, :other_field
16
+ validates :field, presence: true
17
+ validates :other_field, presence: true
18
+
19
+ def self.i18n_scope
20
+ :activerecord
21
+ end
22
+ end
23
+
24
+ subject { ErrorHandlingMockContainer.new }
25
+
26
+ describe "fail_request!" do
27
+ it "should raise the right exception" do
28
+ expect { subject.fail_request!(1, 2) }.to raise_error(Apes::Errors::BaseError)
29
+ end
30
+ end
31
+
32
+ describe "error_handle_exception" do
33
+ it "should call the right handler" do
34
+ expect(subject).to receive(:error_handle_bad_request)
35
+ subject.error_handle_exception(::Apes::Errors::BadRequestError.new)
36
+
37
+ expect(subject).to receive(:error_handle_invalid_data)
38
+ subject.error_handle_exception(::Apes::Errors::InvalidDataError.new)
39
+
40
+ expect(subject).to receive(:error_handle_others)
41
+ subject.error_handle_exception(RuntimeError.new)
42
+ end
43
+ end
44
+
45
+ describe "error_handle_general" do
46
+ it "should render the right template" do
47
+ expect(subject).to receive(:render_error).with(401, "FOO")
48
+ subject.error_handle_general(::Apes::Errors::BaseError.new({status: 401, error: "FOO"}))
49
+ end
50
+ end
51
+
52
+ describe "error_handle_others" do
53
+ it "should render the right template and store the useful information" do
54
+ allow(Apes::RuntimeConfiguration).to receive(:rails_root).and_return("/abc")
55
+ allow(Apes::RuntimeConfiguration).to receive(:gems_root).and_return("/cde")
56
+
57
+ err = RuntimeError.new
58
+ allow(err).to receive(:backtrace).and_return(["FOO", Apes::RuntimeConfiguration.rails_root + "/foo", Apes::RuntimeConfiguration.gems_root + "/foo"])
59
+
60
+ expect(subject).to receive(:render).with("errors/500", status: :internal_server_error)
61
+ subject.error_handle_others(err)
62
+ expect(subject.instance_variable_get(:@exception)).to be(err)
63
+ expect(subject.instance_variable_get(:@backtrace)).to eq(["FOO", "$RAILS/foo", "$GEMS/foo"])
64
+ end
65
+ end
66
+
67
+ describe "error_handle_debug" do
68
+ it "should render the right template with the right data" do
69
+ err = Lazier::Exceptions::Debug.new({a: 1, b: 2}.to_json)
70
+ expect(subject).to receive(:render).with("errors/400", status: 418, locals: {debug: {"a" => 1, "b" => 2}})
71
+ subject.error_handle_debug(err)
72
+ end
73
+ end
74
+
75
+ describe "error_handle_fordidden" do
76
+ it "should render the right template with the right message" do
77
+ expect(subject).to receive(:render).with("errors/403", status: :forbidden).exactly(2)
78
+
79
+ subject.error_handle_fordidden(RuntimeError.new("FOO"))
80
+ expect(subject.instance_variable_get(:@authentication_error)).to eq({error: "FOO"})
81
+
82
+ subject.error_handle_fordidden(RuntimeError.new(""))
83
+ expect(subject.instance_variable_get(:@authentication_error)).to eq({error: "You don't have access to this resource."})
84
+ end
85
+ end
86
+
87
+ describe "error_handle_not_found" do
88
+ it "should render the right template" do
89
+ expect(subject).to receive(:render).with("errors/404", status: :not_found)
90
+ subject.error_handle_not_found
91
+ end
92
+ end
93
+
94
+ describe "error_handle_bad_request" do
95
+ it "should render the right template with the right reason" do
96
+ expect(subject).to receive(:render).with("errors/400", status: :bad_request)
97
+ subject.error_handle_bad_request
98
+ expect(subject.instance_variable_get(:@reason)).to eq("Invalid Content-Type specified. Please use \"FOO\" when performing write operations.")
99
+ end
100
+ end
101
+
102
+ describe "error_handle_missing_data" do
103
+ it "should render the right template with the right reason" do
104
+ expect(subject).to receive(:render).with("errors/400", status: :bad_request)
105
+ subject.error_handle_missing_data
106
+ expect(subject.instance_variable_get(:@reason)).to eq("Missing data.")
107
+ end
108
+ end
109
+
110
+ describe "error_handle_invalid_data" do
111
+ it "should render the right template with the right reason" do
112
+ expect(subject).to receive(:render).with("errors/400", status: :bad_request)
113
+ subject.error_handle_invalid_data
114
+ expect(subject.instance_variable_get(:@reason)).to eq("Invalid data provided.")
115
+ end
116
+ end
117
+
118
+ describe "error_handle_unknown_attribute" do
119
+ it "should render the right template with the right errors" do
120
+ object = ErrorHandlingMockModel.new
121
+
122
+ expect(subject).to receive(:render).with("errors/422", status: :unprocessable_entity).exactly(2)
123
+ subject.error_handle_unknown_attribute(ActionController::UnpermittedParameters.new(["A", "B"]))
124
+ expect(subject.instance_variable_get(:@errors)).to eq(["A", "B"])
125
+
126
+ subject.error_handle_unknown_attribute(ActiveRecord::UnknownAttributeError.new(object, "C"))
127
+ expect(subject.instance_variable_get(:@errors)).to eq("C")
128
+ end
129
+ end
130
+
131
+ describe "error_handle_validation" do
132
+ it "should render the right template with the right errors" do
133
+ object = ErrorHandlingMockModel.new
134
+ object.validate
135
+
136
+ expect(subject).to receive(:render).with("errors/422", status: :unprocessable_entity)
137
+ subject.error_handle_validation(ActiveRecord::RecordInvalid.new(object))
138
+ expect(subject.instance_variable_get(:@errors)).to eq({field: ["can't be blank"], other_field: ["can't be blank"]})
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,114 @@
1
+ require "spec_helper"
2
+
3
+ describe Apes::Concerns::Pagination do
4
+ class PaginationHandlingMockContainer
5
+ include Apes::Concerns::Pagination
6
+
7
+ attr_reader :cursor
8
+
9
+ def initialize(params = {}, field = :page, count_field = :count)
10
+ @cursor = Apes::PaginationCursor.new(params, field, count_field)
11
+ end
12
+
13
+ def request
14
+ OpenStruct.new(params: {a: 1, b: 2})
15
+ end
16
+
17
+ def url_for(params)
18
+ params
19
+ end
20
+ end
21
+
22
+ subject { PaginationHandlingMockContainer.new }
23
+
24
+ describe "#paginate" do
25
+ context "when NOT using the offset" do
26
+ it "should apply the query" do
27
+ collection = []
28
+ cursor = JWT.encode({aud: "pagination", sub: {value: "2001-02-03T04:05:06.789+0700", size: 34, direction: "next"}}, Apes::RuntimeConfiguration.jwt_token, "HS256")
29
+ subject = PaginationHandlingMockContainer.new({page: cursor})
30
+
31
+ expect(collection).to receive(:columns_hash).and_return({"created_at" => OpenStruct.new(type: "datetime")})
32
+ expect(collection).to receive(:where).with("created_at > ?", DateTime.civil(2001, 2, 3, 4, 5, 6.789, "+7")).and_return(collection)
33
+ expect(collection).to receive(:limit).with(34).and_return(collection)
34
+ expect(collection).to receive(:order).with("created_at ASC").and_return(collection)
35
+
36
+ expect(subject.paginate(collection, sort_field: :created_at, sort_order: :asc)).to eq(collection)
37
+ end
38
+ end
39
+
40
+ context "when using the offset" do
41
+ it "should apply the query" do
42
+ collection = []
43
+ cursor = JWT.encode({aud: "pagination", sub: {value: 12, size: 56, use_offset: true, direction: "next"}}, Apes::RuntimeConfiguration.jwt_token, "HS256")
44
+ subject = PaginationHandlingMockContainer.new({page: cursor})
45
+
46
+ expect(collection).to receive(:offset).with(12).and_return(collection)
47
+ expect(collection).to receive(:limit).with(56).and_return(collection)
48
+ expect(collection).to receive(:order).with("id ASC").and_return(collection)
49
+
50
+ expect(subject.paginate(collection, sort_field: :id, sort_order: :asc)).to eq(collection)
51
+ end
52
+ end
53
+
54
+ context "when NOT going next" do
55
+ it "should reverse results" do
56
+ collection = []
57
+ cursor = JWT.encode({aud: "pagination", sub: {value: 12, size: 56, use_offset: true, direction: "prev"}}, Apes::RuntimeConfiguration.jwt_token, "HS256")
58
+ subject = PaginationHandlingMockContainer.new({page: cursor})
59
+
60
+ expect(collection).to receive(:offset).and_return(collection)
61
+ expect(collection).to receive(:limit).and_return(collection)
62
+ expect(collection).to receive(:order).and_return(collection)
63
+ expect(collection).to receive(:reverse_order).and_return(collection)
64
+ expect(collection).to receive(:reverse).and_return(collection)
65
+ expect(subject.paginate(collection, sort_field: :id, sort_order: :asc)).to eq(collection)
66
+ end
67
+ end
68
+ end
69
+
70
+ describe "#pagination_field" do
71
+ it "should return the right field" do
72
+ expect(subject.pagination_field).to eq(:handle)
73
+
74
+ subject.instance_variable_set(:@pagination_field, :foo)
75
+ expect(subject.pagination_field).to eq(:foo)
76
+ end
77
+ end
78
+
79
+ describe "#pagination_skip?" do
80
+ it "should return the right value" do
81
+ expect(subject.pagination_skip?).to be_nil
82
+
83
+ subject.instance_variable_set(:@skip_pagination, "FOO")
84
+ expect(subject.pagination_skip?).to eq("FOO")
85
+ end
86
+ end
87
+
88
+ describe "#pagination_supported?" do
89
+ it "check whether pagination is supported" do
90
+ expect(subject.pagination_supported?).to be_falsey
91
+
92
+ subject.instance_variable_set(:@objects, OpenStruct.new(first: true))
93
+ expect(subject.pagination_supported?).to be_falsey
94
+
95
+ subject.instance_variable_set(:@objects, [])
96
+ expect(subject.pagination_supported?).to be_truthy
97
+ end
98
+ end
99
+
100
+ describe "#pagination_url" do
101
+ it "should return null when not supported" do
102
+ expect(subject.pagination_url("next")).to be_nil
103
+ end
104
+
105
+ it "should return the right URL when supported" do
106
+ subject.instance_variable_set(:@objects, ["FOO"])
107
+
108
+ allow(subject.cursor).to receive(:might_exist?).with("prev", ["FOO"]).and_return(true)
109
+ allow(subject.cursor).to receive(:save).with(["FOO"], "prev", field: :handle).and_return("URL")
110
+
111
+ expect(subject.pagination_url("prev")).to eq({a: 1, b: 2, only_path: false, page: "URL"})
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,244 @@
1
+ require "spec_helper"
2
+
3
+ describe Apes::Concerns::Request do
4
+ class RequestHandlingMockContainer
5
+ include Apes::Concerns::Request
6
+
7
+ attr_reader :request, :response, :params, :headers
8
+
9
+ def initialize(request = nil, params = nil)
10
+ @request = OpenStruct.new(request || {format: nil, url: "", body: "FOO"})
11
+ @response ||= OpenStruct.new({content_type: :html})
12
+ @params = params || HashWithIndifferentAccess.new
13
+ @headers = HashWithIndifferentAccess.new
14
+ end
15
+
16
+ def fail_request!(status, error)
17
+ raise(Apes::Errors::BaseError, {status: status, error: error})
18
+ end
19
+ end
20
+
21
+ FirstMockModel = Struct.new(:id)
22
+
23
+ class SecondMockModel
24
+ end
25
+
26
+ class MockModel
27
+ include ActiveModel::Model
28
+ include Apes::Model
29
+
30
+ ATTRIBUTES = [:id, :other, :created_at, :first_mock_model]
31
+ RELATIONSHIPS = {first_mock_model: nil, second: SecondMockModel}
32
+
33
+ def self.column_types
34
+ {"id" => OpenStruct.new(type: :string), "other" => OpenStruct.new(type: :boolean), "created_at" => OpenStruct.new(type: :datetime)}
35
+ end
36
+ end
37
+
38
+ before(:each) do
39
+ allow(Apes::RuntimeConfiguration).to receive(:timestamp_formats).and_return({full: "%FT%T.%L%z"})
40
+ allow(Apes::RuntimeConfiguration).to receive(:development?).and_return(true)
41
+ end
42
+
43
+ subject { RequestHandlingMockContainer.new }
44
+
45
+ describe "#request_handle_cors" do
46
+ it "should set the right headers" do
47
+ allow(subject).to receive(:request_source_host).and_return("FOO")
48
+
49
+ subject.request_handle_cors
50
+ expect(subject.headers).to eq({
51
+ "Access-Control-Allow-Headers" => "Content-Type, X-User-Email, X-User-Token",
52
+ "Access-Control-Allow-Methods" => "POST, GET, PUT, DELETE, OPTIONS",
53
+ "Access-Control-Allow-Origin" => "http://FOO:4200",
54
+ "Access-Control-Max-Age" => "31557600"
55
+ })
56
+ end
57
+ end
58
+
59
+ describe "#request_validate" do
60
+ it "should override the request format and the response content type, then prepare the data" do
61
+ subject.request_validate
62
+
63
+ expect(subject.request.format).to eq(:json)
64
+ expect(subject.response.content_type).to eq("application/vnd.api+json")
65
+ expect(subject.params[:data]).to be_a(HashWithIndifferentAccess)
66
+ end
67
+
68
+ it "should complain when the provided content_type is wrong" do
69
+ allow(subject.request).to receive(:post?).and_return(true)
70
+ allow(subject.request).to receive(:content_type).and_return("FOO")
71
+
72
+ expect { subject.request_validate }.to raise_error(Apes::Errors::BadRequestError)
73
+ end
74
+
75
+ it "should complain when the data is missing" do
76
+ allow(subject.request).to receive(:post?).and_return(true)
77
+ allow(subject.request).to receive(:content_type).and_return(Apes::Concerns::Request::CONTENT_TYPE)
78
+
79
+ expect { subject.request_validate }.to raise_error(Apes::Errors::MissingDataError)
80
+ end
81
+
82
+ it "should complain when the data is not a valid JSON" do
83
+ subject.request.body = OpenStruct.new(read: "FOO")
84
+ allow(subject.request).to receive(:post?).and_return(true)
85
+ allow(subject.request).to receive(:content_type).and_return(Apes::Concerns::Request::CONTENT_TYPE)
86
+
87
+ expect { subject.request_validate }.to raise_error(Apes::Errors::InvalidDataError)
88
+ end
89
+
90
+ it "should complain when the data is missing in the data attribute for JSON API requests" do
91
+ subject.request.body = OpenStruct.new(read: {a: 1}.to_json)
92
+ allow(subject.request).to receive(:post?).and_return(true)
93
+ allow(subject.request).to receive(:content_type).and_return(Apes::Concerns::Request::CONTENT_TYPE)
94
+
95
+ expect { subject.request_validate }.to raise_error(Apes::Errors::MissingDataError)
96
+ end
97
+ end
98
+
99
+ describe "#request_source_host" do
100
+ it "should return the right host" do
101
+ subject.request.url = "http://abc.google.it:1234"
102
+ expect(subject.request_source_host).to eq("abc.google.it")
103
+ end
104
+ end
105
+
106
+ describe "#request_valid_content_type" do
107
+ it "should return the right type" do
108
+ expect(subject.request_valid_content_type).to eq("application/vnd.api+json")
109
+ end
110
+ end
111
+
112
+ describe "#request_extract_model" do
113
+ before(:each) do
114
+ allow(subject.request).to receive(:post?).and_return(true)
115
+ allow(subject.request).to receive(:content_type).and_return(Apes::Concerns::Request::CONTENT_TYPE)
116
+ ActionController::Parameters.action_on_unpermitted_parameters = :raise
117
+ end
118
+
119
+ it "should complain if the type is missing in the data" do
120
+ subject.request.body = OpenStruct.new(read: {data: {foo: 1, body: 1}}.to_json)
121
+ subject.request_validate
122
+
123
+ expect(subject).to receive(:fail_request!).with(:bad_request, "No type provided when type \"mock_model\" was expected.").and_raise(RuntimeError)
124
+ expect { subject.request_extract_model(MockModel.new) }.to raise_error(RuntimeError)
125
+ end
126
+
127
+ it "should complain if the type is wrong in the data" do
128
+ subject.request.body = OpenStruct.new(read: {data: {type: "foo", foo: 1, body: 1}}.to_json)
129
+ subject.request_validate
130
+
131
+ expect(subject).to receive(:fail_request!).with(:bad_request, "Invalid type \"foo\" provided when type \"mock_model\" was expected.").and_raise(RuntimeError)
132
+ expect { subject.request_extract_model(MockModel.new) }.to raise_error(RuntimeError)
133
+ end
134
+
135
+ it "should complain if the data is not inside the attributes field" do
136
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", foo: 1, body: 1}}.to_json)
137
+ subject.request_validate
138
+
139
+ expect(subject).to receive(:fail_request!).with(:bad_request, "Missing attributes in the \"attributes\" field.").and_raise(RuntimeError)
140
+ expect { subject.request_extract_model(MockModel.new) }.to raise_error(RuntimeError)
141
+ end
142
+
143
+ it "should complain if any unknown attribute is present for the mock_model" do
144
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", attributes: {foo: 1, other: 1}}}.to_json)
145
+ subject.request_validate
146
+
147
+ expect { subject.request_extract_model(MockModel.new) }.to raise_error(ActionController::UnpermittedParameters) do |error|
148
+ expect(error.params).to eq(["attributes.foo"])
149
+ end
150
+ end
151
+
152
+ it "should allowed hash attributes" do
153
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", attributes: {foo: 1, other: {a: 1}}}}.to_json)
154
+ subject.request_validate
155
+
156
+ expect { subject.request_extract_model(MockModel.new) }.to raise_error(ActionController::UnpermittedParameters) do |error|
157
+ expect(error.params).to eq(["attributes.foo"])
158
+ end
159
+ end
160
+
161
+ it "should return attributes for the mock_model" do
162
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", attributes: {id: "1", other: 1}}}.to_json)
163
+ subject.request_validate
164
+
165
+ expect(subject.request_extract_model(MockModel.new)).to eq({id: "1", other: 1}.with_indifferent_access)
166
+ end
167
+
168
+ it "should return relationships for the model" do
169
+ first = FirstMockModel.new(1)
170
+ allow(FirstMockModel).to receive(:find_with_any).and_return(first)
171
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", attributes: {id: 1}, relationships: {first_mock_model: {data: {type: "first_mock_model", id: first.id}}}}}.to_json)
172
+ subject.request_validate
173
+
174
+ expect(subject.request_extract_model(MockModel.new)).to eq({id: 1, first_mock_model: first}.with_indifferent_access)
175
+ end
176
+
177
+ it "should move inline references to the relationship objects" do
178
+ first = FirstMockModel.new(1)
179
+ allow(FirstMockModel).to receive(:find_with_any).and_return(first)
180
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", attributes: {id: 1, first_mock_model: first.id}}}.to_json)
181
+ subject.request_validate
182
+
183
+ extracted = subject.request_extract_model(MockModel.new)
184
+ expect(extracted.keys.map(&:to_sym)).to eq([:id, :first_mock_model])
185
+ expect(extracted[:first_mock_model].id).to eq(first.id)
186
+ end
187
+
188
+ it "should reject unallowed relationships" do
189
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", attributes: {id: 1}, relationships: {another: {}}}}.to_json)
190
+ subject.request_validate
191
+
192
+ target = MockModel.new
193
+ expect { subject.request_extract_model(target) }.to raise_error(ActionController::UnpermittedParameters) do |error|
194
+ expect(error.params).to eq(["relationships.another"])
195
+ end
196
+ end
197
+
198
+ it "should reject malformed relationships" do
199
+ first = FirstMockModel.new(1)
200
+
201
+ target = MockModel.new
202
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", attributes: {id: 1}, relationships: {first_mock_model: {data: {id: first.id}}}}}.to_json)
203
+ subject.request_validate
204
+ subject.request_extract_model(target)
205
+ expect(target.additional_errors.to_hash).to eq({first_mock_model: ["Relationship does not contain the \"data.type\" attribute"]})
206
+
207
+ target = MockModel.new
208
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", attributes: {id: 1}, relationships: {first_mock_model: {data: {type: "first_mock_model"}}}}}.to_json)
209
+ subject.request_validate
210
+ subject.request_extract_model(target)
211
+ expect(target.additional_errors.to_hash).to eq({first_mock_model: ["Relationship does not contain the \"data.id\" attribute"]})
212
+
213
+ target = MockModel.new
214
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", attributes: {id: 1}, relationships: {first_mock_model: {data: {type: "foo", id: 1}}}}}.to_json)
215
+ subject.request_validate
216
+ subject.request_extract_model(target)
217
+ expect(target.additional_errors.to_hash).to eq({first_mock_model: ["Invalid relationship type \"foo\" provided for when type \"first_mock_model\" was expected."]})
218
+ end
219
+
220
+ it "should reject invalid relationships" do
221
+ allow(SecondMockModel).to receive(:find_with_any).and_return(false)
222
+ target = MockModel.new
223
+ subject.request.body = OpenStruct.new(read: {data: {type: "mock_model", attributes: {id: 1}, relationships: {second: {data: {type: "second_mock_model", id: -1}}}}}.to_json)
224
+ subject.request_validate
225
+ subject.request_extract_model(target)
226
+ expect(target.additional_errors.to_hash).to eq({second: ["Refers to a non existing \"second_mock_model\" resource."]})
227
+ end
228
+ end
229
+
230
+ describe "#request_cast_attributes" do
231
+ it "should correctly cast attributes for a mock_model" do
232
+ attributes = {id: 3, other: "YES", created_at: "2001-02-03T04:05:06.789+0700"}.with_indifferent_access
233
+ casted_attributes = {id: 3, other: true, created_at: DateTime.civil(2001, 2, 3, 4, 5, 6.789, "+7")}.with_indifferent_access
234
+ expect(subject.request_cast_attributes(MockModel.new, attributes)).to eq(casted_attributes)
235
+ end
236
+
237
+ it "should add casting errors to the mock_model validation errors" do
238
+ attributes = {id: 3, other: "yes", created_at: "NO"}.with_indifferent_access
239
+ object = MockModel.new
240
+ subject.request_cast_attributes(object, attributes)
241
+ expect(object.additional_errors.to_hash).to eq(created_at: ["Invalid timestamp \"NO\"."])
242
+ end
243
+ end
244
+ end