Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
* [#2894](https://github.com/ruby-grape/grape/pull/2894): Read the request method once in `default_status` instead of asking through `post?` and `delete?` - [@ericproulx](https://github.com/ericproulx).
* [#2895](https://github.com/ruby-grape/grape/pull/2895): Cut per-request work out of the endpoint, validation and router paths: scan the router's compiled union by capture number rather than by name, skip the Array boxing in `AttributesIterator` for a flat scope, read a coerced attribute once instead of three times, and resolve the formatter's config and the coercers' type checks once at build time - [@ericproulx](https://github.com/ericproulx).
* [#2900](https://github.com/ruby-grape/grape/pull/2900): Remove the deprecations announced in 3.2 and 3.3: `Grape::Router.normalize_path`, Hash access on middleware `Options` and their `DEFAULT_OPTIONS` constants, the positional options Hash for `auth`/`http_basic`/`desc`, a Hash returned from a `rescue_from` handler, and `@option` on validators (see UPGRADING) - [@ericproulx](https://github.com/ericproulx).
* [#2896](https://github.com/ruby-grape/grape/pull/2896): Bring test suite line coverage to 100% - [@dblock](https://github.com/dblock).
* Your contribution here.

#### Fixes
Expand Down
12 changes: 12 additions & 0 deletions spec/grape/api/instance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,16 @@ def app
expect(last_response.body).to eq 'Not found! (2)'
end
end

describe '.cascade?' do
subject(:an_instance) do
Class.new(Grape::API::Instance) do
cascade true
end
end

it 'returns the configured cascade setting' do
expect(an_instance.compile!.cascade?).to be(true)
end
end
end
11 changes: 11 additions & 0 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2090,6 +2090,17 @@ def hello
expect(last_response.body).to eql 'Hello, world.'
end

it 'includes all known helpers in scope when called with no args and no block' do
subject.helpers do
def hello
'Hello, world.'
end
end

new_mod = subject.helpers
expect(new_mod.instance_methods).to include(:hello)
end

it 'is scopable' do
subject.helpers do
def generic
Expand Down
6 changes: 6 additions & 0 deletions spec/grape/dsl/request_response_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@
expect(subject.inheritable_setting.all_rescue_handler).to eq(with_block)
end

it 'converts a String :with option to a Symbol' do
subject.rescue_from :all, with: 'my_handler'
expect(subject.inheritable_setting.rescue_all?).to be(true)
expect(subject.inheritable_setting.all_rescue_handler).to eq(:my_handler)
end

it 'abort if :with option value is not Symbol, String or Proc' do
expect { subject.rescue_from :all, with: 1234 }.to raise_error(ArgumentError, "with: #{integer_class_name}, expected Symbol, String or Proc")
end
Expand Down
28 changes: 26 additions & 2 deletions spec/grape/endpoint_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,9 @@ def memoized
subject.before do
# Placeholder
end
subject.params do
optional :id, type: Integer
end
subject.get do
'hello'
end
Expand All @@ -1064,7 +1067,10 @@ def memoized
have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class),
filters: a_collection_containing_exactly(an_instance_of(Proc)),
type: :before }),
have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(described_class) }),
have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(described_class),
validators: an_instance_of(Array),
request: a_kind_of(Grape::Request) }),
have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(described_class) }),
have_attributes(name: 'endpoint_run.grape', payload: { endpoint: a_kind_of(described_class),
env: an_instance_of(Hash) }),
have_attributes(name: 'format_response.grape', payload: { env: an_instance_of(Hash),
Expand All @@ -1078,7 +1084,10 @@ def memoized
have_attributes(name: 'endpoint_run_filters.grape', payload: { endpoint: a_kind_of(described_class),
filters: a_collection_containing_exactly(an_instance_of(Proc)),
type: :before }),
have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(described_class) }),
have_attributes(name: 'endpoint_run_validators.grape', payload: { endpoint: a_kind_of(described_class),
validators: an_instance_of(Array),
request: a_kind_of(Grape::Request) }),
have_attributes(name: 'endpoint_render.grape', payload: { endpoint: a_kind_of(described_class) }),
have_attributes(name: 'format_response.grape', payload: { env: an_instance_of(Hash),
formatter: a_kind_of(Module) })
)
Expand Down Expand Up @@ -1126,5 +1135,20 @@ def memoized
it 'does not raise an error' do
expect { subject }.not_to raise_error
end

context 'when the endpoint has handled a request (env is set)' do
it 'includes the route origin in the inspect output' do
inspect_output = nil
api = Class.new(Grape::API) do
get('/hello') do
inspect_output = inspect
'world'
end
end
env = Rack::MockRequest.env_for('/hello')
api.call(env)
expect(inspect_output).to eq("#{described_class} in '/hello' endpoint")
end
end
end
end
9 changes: 9 additions & 0 deletions spec/grape/error_formatter/base_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

describe Grape::ErrorFormatter::Base do
describe '.format_structured_message' do
it 'raises NotImplementedError' do
expect { described_class.format_structured_message({}) }.to raise_error(NotImplementedError)
end
end
end
7 changes: 7 additions & 0 deletions spec/grape/exceptions/validation_errors_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@
end
end

describe '#to_json' do
it 'returns the JSON representation of #as_json' do
error = described_class.new(exceptions: [validation_error])
expect(error.to_json).to eq(error.as_json.to_json)
end
end

describe '#full_messages' do
context 'with errors' do
subject { described_class.new(exceptions: [validation_error_1, validation_error_2]).full_messages }
Expand Down
7 changes: 7 additions & 0 deletions spec/grape/exceptions/validation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,11 @@
expect(error.message).to eq('raw message')
end
end

describe '#as_json' do
it 'returns the string representation of the error' do
error = described_class.new(params: ['id'], message: 'raw message')
expect(error.as_json).to eq(error.to_s)
end
end
end
9 changes: 9 additions & 0 deletions spec/grape/formatter/base_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

describe Grape::Formatter::Base do
describe '.call' do
it 'raises NotImplementedError' do
expect { described_class.call({}, {}) }.to raise_error(NotImplementedError)
end
end
end
21 changes: 21 additions & 0 deletions spec/grape/formatter/json_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

describe Grape::Formatter::Json do
describe '.call' do
it 'returns the string representation of a Grape::PrecompiledJson object' do
precompiled = Grape::PrecompiledJson.new('{"a":1}')
expect(described_class.call(precompiled, {})).to eq('{"a":1}')
end

it 'calls #to_json when the object responds to it' do
object = { a: 1 }
expect(described_class.call(object, {})).to eq(object.to_json)
end

it 'falls back to Grape::Json.dump when the object does not respond to #to_json' do
object = Object.new
allow(object).to receive(:respond_to?).with(:to_json).and_return(false)
expect(described_class.call(object, {})).to eq(Grape::Json.dump(object))
end
end
end
62 changes: 62 additions & 0 deletions spec/grape/formatter/serializable_hash_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

describe Grape::Formatter::SerializableHash do
describe '.call' do
it 'returns the string representation of a Grape::PrecompiledJson object' do
precompiled = Grape::PrecompiledJson.new('{"a":1}')
expect(described_class.call(precompiled, {})).to eq('{"a":1}')
end

it 'returns a String object unchanged' do
expect(described_class.call('already a string', {})).to eq('already a string')
end

it 'serializes an object responding to #serializable_hash' do
object = Class.new do
def serializable_hash
{ a: 1 }
end
end.new
expect(described_class.call(object, {})).to eq(Grape::Json.dump(a: 1))
end

it 'serializes an Array of objects responding to #serializable_hash' do
klass = Class.new do
def initialize(value)
@value = value
end

def serializable_hash
{ value: @value }
end
end
objects = [klass.new(1), klass.new(2)]
expect(described_class.call(objects, {})).to eq(Grape::Json.dump([{ value: 1 }, { value: 2 }]))
end

it 'serializes a Hash, recursively serializing its values' do
object = Class.new do
def serializable_hash
{ a: 1 }
end
end.new
expect(described_class.call({ nested: object }, {})).to eq(Grape::Json.dump(nested: { a: 1 }))
end

it 'serializes a Hash, leaving non-serializable leaf values untouched' do
expect(described_class.call({ plain: 'value' }, {})).to eq(Grape::Json.dump(plain: 'value'))
end

it 'calls #to_json when the object responds to it and is not otherwise serializable' do
object = 1234
expect(described_class.call(object, {})).to eq(object.to_json)
end

it 'falls back to Grape::Json.dump when the object is not serializable and does not respond to #to_json' do
object = Object.new
allow(object).to receive(:respond_to?).and_call_original
allow(object).to receive(:respond_to?).with(:to_json).and_return(false)
expect(described_class.call(object, {})).to eq(Grape::Json.dump(object))
end
end
end
17 changes: 17 additions & 0 deletions spec/grape/middleware/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,23 @@ def after
expect(last_response.headers['X-Test-Before']).to eq('Hi')
expect(last_response.headers['X-Test-After']).to eq('Bye')
end

context 'when the downstream app returns a Rack::Response' do
let(:app) do
context = self

Rack::Builder.app do
use context.example_ware
run ->(_) { Rack::Response.new('Yeah', 200, {}) }
end
end

it 'merges the header onto the Rack::Response' do
get '/'
expect(last_response.headers['X-Test-Before']).to eq('Hi')
expect(last_response.headers['X-Test-After']).to eq('Bye')
end
end
end

context 'header overwrite' do
Expand Down
28 changes: 28 additions & 0 deletions spec/grape/middleware/error_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -460,4 +460,32 @@ def initialize
end
end
end

describe '#error!' do
it 'sets the status and renders a formatted error response' do
env = Rack::MockRequest.env_for('/')
endpoint = Spec::Support::EndpointFaker::FakerAPI.endpoints.first
env[Grape::Env::API_ENDPOINT] = endpoint
middleware = described_class.new(->(_env) {})
middleware.instance_variable_set(:@env, env)

expect(endpoint).to receive(:status).with(422)
response = middleware.__send__(:error!, 'failure', 422)
expect(response.status).to eq(422)
expect(response.body).to eq(['failure'])
end
end

describe '#error?' do
subject(:middleware) { described_class.new(->(_env) {}) }

it 'returns true for a Grape::Exceptions::ErrorResponse' do
response = Grape::Exceptions::ErrorResponse.new(message: 'oops', status: 500, headers: {})
expect(middleware.__send__(:error?, response)).to be true
end

it 'returns false for any other object' do
expect(middleware.__send__(:error?, 'not an error')).to be false
end
end
end
44 changes: 44 additions & 0 deletions spec/grape/middleware/filter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

describe Grape::Middleware::Filter do
let(:before_proc) { -> {} }
let(:after_proc) { -> {} }
let(:app) { ->(_env) { [200, {}, ['Hi there.']] } }
let(:middleware) { described_class.new(app, before: before_proc, after: after_proc) }

describe '#before' do
it 'instance_evals the :before option against the app' do
expect(app).to receive(:instance_eval) do |&block|
expect(block).to eq(before_proc)
end
middleware.before
end

context 'when no :before option is given' do
let(:middleware) { described_class.new(app) }

it 'does nothing' do
expect(app).not_to receive(:instance_eval)
middleware.before
end
end
end

describe '#after' do
it 'instance_evals the :after option against the app' do
expect(app).to receive(:instance_eval) do |&block|
expect(block).to eq(after_proc)
end
middleware.after
end

context 'when no :after option is given' do
let(:middleware) { described_class.new(app) }

it 'does nothing' do
expect(app).not_to receive(:instance_eval)
middleware.after
end
end
end
end
14 changes: 14 additions & 0 deletions spec/grape/middleware/formatter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,20 @@ def to_xml
expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true
expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing')
end

it "merges into a pre-existing rack.request.form_hash when parsing the body from #{method}" do
subject.call(
Rack::PATH_INFO => '/info',
Rack::REQUEST_METHOD => method,
'CONTENT_TYPE' => content_type,
Rack::RACK_INPUT => io,
'CONTENT_LENGTH' => io.length.to_s,
Rack::RACK_REQUEST_FORM_HASH => { 'existing' => 'value' }
)
expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['existing']).to eq('value')
expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['is_boolean']).to be true
expect(subject.env[Rack::RACK_REQUEST_FORM_HASH]['string']).to eq('thing')
end
end

context 'when Content-Type is not supported' do
Expand Down
22 changes: 22 additions & 0 deletions spec/grape/middleware/stack/middleware_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@

describe Grape::Middleware::Stack::Middleware do
let(:middleware_class) { Class.new }
let(:foo_middleware) { Class.new }
let(:bar_middleware) { Class.new }

describe '#==' do
it 'compares equal to another Middleware wrapping the same class' do
first = described_class.new(foo_middleware, [], nil)
second = described_class.new(foo_middleware, [42], proc {})
expect(first).to eq(second)
end

it 'compares unequal to another Middleware wrapping a different class' do
first = described_class.new(foo_middleware, [], nil)
second = described_class.new(bar_middleware, [], nil)
expect(first).not_to eq(second)
end
end

describe '#inspect' do
it "returns the wrapped class's #to_s" do
expect(described_class.new(foo_middleware, [], nil).inspect).to eq(foo_middleware.to_s)
end
end

describe '#hash' do
it 'matches for two entries wrapping the same class' do
Expand Down
Loading
Loading