apes 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rubocop.yml +82 -0
- data/.travis-gemfile +15 -0
- data/.travis.yml +15 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +22 -0
- data/README.md +177 -0
- data/Rakefile +44 -0
- data/apes.gemspec +34 -0
- data/doc/Apes.html +130 -0
- data/doc/Apes/Concerns.html +127 -0
- data/doc/Apes/Concerns/Errors.html +1089 -0
- data/doc/Apes/Concerns/Pagination.html +636 -0
- data/doc/Apes/Concerns/Request.html +766 -0
- data/doc/Apes/Concerns/Response.html +940 -0
- data/doc/Apes/Controller.html +1100 -0
- data/doc/Apes/Errors.html +125 -0
- data/doc/Apes/Errors/AuthenticationError.html +133 -0
- data/doc/Apes/Errors/BadRequestError.html +157 -0
- data/doc/Apes/Errors/BaseError.html +320 -0
- data/doc/Apes/Errors/InvalidDataError.html +157 -0
- data/doc/Apes/Errors/MissingDataError.html +157 -0
- data/doc/Apes/Model.html +378 -0
- data/doc/Apes/PaginationCursor.html +2138 -0
- data/doc/Apes/RuntimeConfiguration.html +909 -0
- data/doc/Apes/Serializers.html +125 -0
- data/doc/Apes/Serializers/JSON.html +389 -0
- data/doc/Apes/Serializers/JWT.html +452 -0
- data/doc/Apes/Serializers/List.html +347 -0
- data/doc/Apes/UrlsParser.html +1432 -0
- data/doc/Apes/Validators.html +125 -0
- data/doc/Apes/Validators/BaseValidator.html +278 -0
- data/doc/Apes/Validators/BooleanValidator.html +494 -0
- data/doc/Apes/Validators/EmailValidator.html +350 -0
- data/doc/Apes/Validators/PhoneValidator.html +375 -0
- data/doc/Apes/Validators/ReferenceValidator.html +372 -0
- data/doc/Apes/Validators/TimestampValidator.html +640 -0
- data/doc/Apes/Validators/UuidValidator.html +372 -0
- data/doc/Apes/Validators/ZipCodeValidator.html +372 -0
- data/doc/Apes/Version.html +189 -0
- data/doc/ApplicationController.html +547 -0
- data/doc/Concerns.html +128 -0
- data/doc/Concerns/ErrorHandling.html +826 -0
- data/doc/Concerns/PaginationHandling.html +463 -0
- data/doc/Concerns/RequestHandling.html +512 -0
- data/doc/Concerns/ResponseHandling.html +579 -0
- data/doc/Errors.html +126 -0
- data/doc/Errors/AuthenticationError.html +123 -0
- data/doc/Errors/BadRequestError.html +147 -0
- data/doc/Errors/BaseError.html +289 -0
- data/doc/Errors/InvalidDataError.html +147 -0
- data/doc/Errors/MissingDataError.html +147 -0
- data/doc/Model.html +315 -0
- data/doc/PaginationCursor.html +764 -0
- data/doc/Serializers.html +126 -0
- data/doc/Serializers/JSON.html +253 -0
- data/doc/Serializers/JWT.html +253 -0
- data/doc/Serializers/List.html +245 -0
- data/doc/Validators.html +126 -0
- data/doc/Validators/BaseValidator.html +209 -0
- data/doc/Validators/BooleanValidator.html +391 -0
- data/doc/Validators/EmailValidator.html +298 -0
- data/doc/Validators/PhoneValidator.html +313 -0
- data/doc/Validators/ReferenceValidator.html +284 -0
- data/doc/Validators/TimestampValidator.html +476 -0
- data/doc/Validators/UuidValidator.html +310 -0
- data/doc/Validators/ZipCodeValidator.html +310 -0
- data/doc/_index.html +435 -0
- data/doc/class_list.html +58 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +57 -0
- data/doc/css/style.css +339 -0
- data/doc/file.README.html +252 -0
- data/doc/file_list.html +60 -0
- data/doc/frames.html +26 -0
- data/doc/index.html +252 -0
- data/doc/js/app.js +219 -0
- data/doc/js/full_list.js +181 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +615 -0
- data/doc/top-level-namespace.html +112 -0
- data/lib/apes.rb +40 -0
- data/lib/apes/concerns/errors.rb +111 -0
- data/lib/apes/concerns/pagination.rb +81 -0
- data/lib/apes/concerns/request.rb +237 -0
- data/lib/apes/concerns/response.rb +74 -0
- data/lib/apes/controller.rb +77 -0
- data/lib/apes/errors.rb +38 -0
- data/lib/apes/model.rb +94 -0
- data/lib/apes/pagination_cursor.rb +152 -0
- data/lib/apes/runtime_configuration.rb +80 -0
- data/lib/apes/serializers.rb +88 -0
- data/lib/apes/urls_parser.rb +233 -0
- data/lib/apes/validators.rb +234 -0
- data/lib/apes/version.rb +24 -0
- data/spec/apes/concerns/errors_spec.rb +141 -0
- data/spec/apes/concerns/pagination_spec.rb +114 -0
- data/spec/apes/concerns/request_spec.rb +244 -0
- data/spec/apes/concerns/response_spec.rb +79 -0
- data/spec/apes/controller_spec.rb +54 -0
- data/spec/apes/errors_spec.rb +14 -0
- data/spec/apes/models_spec.rb +148 -0
- data/spec/apes/pagination_cursor_spec.rb +113 -0
- data/spec/apes/runtime_configuration_spec.rb +100 -0
- data/spec/apes/serializers_spec.rb +70 -0
- data/spec/apes/urls_parser_spec.rb +150 -0
- data/spec/apes/validators_spec.rb +237 -0
- data/spec/spec_helper.rb +30 -0
- data/views/_included.json.jbuilder +9 -0
- data/views/_pagination.json.jbuilder +9 -0
- data/views/collection.json.jbuilder +4 -0
- data/views/errors/400.json.jbuilder +9 -0
- data/views/errors/403.json.jbuilder +7 -0
- data/views/errors/404.json.jbuilder +6 -0
- data/views/errors/422.json.jbuilder +19 -0
- data/views/errors/500.json.jbuilder +12 -0
- data/views/errors/501.json.jbuilder +7 -0
- data/views/layouts/general.json.jbuilder +36 -0
- data/views/object.json.jbuilder +4 -0
- metadata +262 -0
data/lib/apes/version.rb
ADDED
@@ -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
|