apes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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