require 'spec_helper' require 'shared/versioning_examples' describe Grape::API do subject { Class.new(Grape::API) } def app subject end describe '.prefix' do it 'routes root through with the prefix' do subject.prefix 'awesome/sauce' subject.get do "Hello there." end get 'awesome/sauce/' last_response.body.should eql "Hello there." end it 'routes through with the prefix' do subject.prefix 'awesome/sauce' subject.get :hello do "Hello there." end get 'awesome/sauce/hello' last_response.body.should eql "Hello there." get '/hello' last_response.status.should eql 404 end end describe '.version' do context 'when defined' do it 'returns version value' do subject.version 'v1' subject.version.should == 'v1' end end context 'when not defined' do it 'returns nil' do subject.version.should be_nil end end end describe '.version using path' do it_should_behave_like 'versioning' do let(:macro_options) do { using: :path } end end end describe '.version using param' do it_should_behave_like 'versioning' do let(:macro_options) do { using: :param, parameter: "apiver" } end end end describe '.version using header' do it_should_behave_like 'versioning' do let(:macro_options) do { using: :header, vendor: 'mycompany', format: 'json' } end end # Behavior as defined by rfc2616 when no header is defined # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html describe 'no specified accept header' do # subject.version 'v1', using: :header # subject.get '/hello' do # 'hello' # end # it 'routes' do # get '/hello' # last_response.status.should eql 200 # end end # pending 'routes if any media type is allowed' end describe '.version using accept_version_header' do it_should_behave_like 'versioning' do let(:macro_options) do { using: :accept_version_header } end end end describe '.represent' do it 'requires a :with option' do expect { subject.represent Object, {} }.to raise_error(Grape::Exceptions::InvalidWithOptionForRepresent) end it 'adds the association to the :representations setting' do klass = Class.new subject.represent Object, with: klass subject.settings[:representations][Object].should == klass end end describe '.namespace' do it 'is retrievable and converted to a path' do subject.namespace :awesome do namespace.should == '/awesome' end end it 'comes after the prefix and version' do subject.prefix :rad subject.version 'v1', using: :path subject.namespace :awesome do get('/hello') { "worked" } end get "/rad/v1/awesome/hello" last_response.body.should == "worked" end it 'cancels itself after the block is over' do subject.namespace :awesome do namespace.should == '/awesome' end subject.namespace.should == '/' end it 'is stackable' do subject.namespace :awesome do namespace :rad do namespace.should == '/awesome/rad' end namespace.should == '/awesome' end subject.namespace.should == '/' end it 'accepts path segments correctly' do subject.namespace :members do namespace '/:member_id' do namespace.should == '/members/:member_id' get '/' do params[:member_id] end end end get '/members/23' last_response.body.should == "23" end it 'is callable with nil just to push onto the stack' do subject.namespace do version 'v2', using: :path get('/hello') { "inner" } end subject.get('/hello') { "outer" } get '/v2/hello' last_response.body.should == "inner" get '/hello' last_response.body.should == "outer" end %w(group resource resources segment).each do |als| it '`.#{als}` is an alias' do subject.send(als, :awesome) do namespace.should == "/awesome" end end end end describe '.route_param' do it 'adds a parameterized route segment namespace' do subject.namespace :users do route_param :id do get do params[:id] end end end get '/users/23' last_response.body.should == '23' end it 'should be able to define requirements with a single hash' do subject.namespace :users do route_param :id, requirements: /[0-9]+/ do get do params[:id] end end end get '/users/michael' last_response.status.should == 404 get '/users/23' last_response.status.should == 200 end end describe '.route' do it 'allows for no path' do subject.namespace :votes do get do "Votes" end post do "Created a Vote" end end get '/votes' last_response.body.should eql 'Votes' post '/votes' last_response.body.should eql 'Created a Vote' end it 'handles empty calls' do subject.get "/" get "/" last_response.body.should eql "" end describe 'root routes should work with' do before do subject.format :txt def subject.enable_root_route! get("/") { "root" } end end after do last_response.body.should eql "root" end describe 'path versioned APIs' do before do subject.version 'v1', using: :path subject.enable_root_route! end it 'without a format' do versioned_get "/", "v1", using: :path end it 'with a format' do get "/v1/.json" end end it 'header versioned APIs' do subject.version 'v1', using: :header, vendor: 'test' subject.enable_root_route! versioned_get "/", "v1", using: :header, vendor: 'test' end it 'header versioned APIs with multiple headers' do subject.version ['v1', 'v2'], using: :header, vendor: 'test' subject.enable_root_route! versioned_get "/", "v1", using: :header, vendor: 'test' versioned_get "/", "v2", using: :header, vendor: 'test' end it 'param versioned APIs' do subject.version 'v1', using: :param subject.enable_root_route! versioned_get "/", "v1", using: :param end it 'Accept-Version header versioned APIs' do subject.version 'v1', using: :accept_version_header subject.enable_root_route! versioned_get "/", "v1", using: :accept_version_header end it 'unversioned APIs' do subject.enable_root_route! get "/" end end it 'allows for multiple paths' do subject.get(["/abc", "/def"]) do "foo" end get '/abc' last_response.body.should eql 'foo' get '/def' last_response.body.should eql 'foo' end context 'format' do before(:each) do subject.get("/abc") do RSpec::Mocks::Mock.new(to_json: 'abc', to_txt: 'def') end end it 'allows .json' do get '/abc.json' last_response.status.should == 200 last_response.body.should eql 'abc' # json-encoded symbol end it 'allows .txt' do get '/abc.txt' last_response.status.should == 200 last_response.body.should eql 'def' # raw text end end it 'allows for format without corrupting a param' do subject.get('/:id') do { "id" => params[:id] } end get '/awesome.json' last_response.body.should eql '{"id":"awesome"}' end it 'allows for format in namespace with no path' do subject.namespace :abc do get do ["json"] end end get '/abc.json' last_response.body.should eql '["json"]' end it 'allows for multiple verbs' do subject.route([:get, :post], '/abc') do "hiya" end subject.endpoints.first.routes.each do |route| route.route_path.should eql '/abc(.:format)' end get '/abc' last_response.body.should eql 'hiya' post '/abc' last_response.body.should eql 'hiya' end [:put, :post].each do |verb| context verb do ['string', :symbol, 1, -1.1, {}, [], true, false, nil].each do |object| it "allows a(n) #{object.class} json object in params" do subject.format :json subject.send(verb) do env['api.request.body'] end send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json' last_response.status.should == (verb == :post ? 201 : 200) last_response.body.should eql MultiJson.dump(object) last_request.params.should eql Hash.new end it "stores input in api.request.input" do subject.format :json subject.send(verb) do env['api.request.input'] end send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json' last_response.status.should == (verb == :post ? 201 : 200) last_response.body.should eql MultiJson.dump(object).to_json end context "chunked transfer encoding" do it "stores input in api.request.input" do subject.format :json subject.send(verb) do env['api.request.input'] end send verb, '/', MultiJson.dump(object), 'CONTENT_TYPE' => 'application/json', 'HTTP_TRANSFER_ENCODING' => 'chunked', 'CONTENT_LENGTH' => nil last_response.status.should == (verb == :post ? 201 : 200) last_response.body.should eql MultiJson.dump(object).to_json end end end end end it 'allows for multipart paths' do subject.route([:get, :post], '/:id/first') do "first" end subject.route([:get, :post], '/:id') do "ola" end subject.route([:get, :post], '/:id/first/second') do "second" end get '/1' last_response.body.should eql 'ola' post '/1' last_response.body.should eql 'ola' get '/1/first' last_response.body.should eql 'first' post '/1/first' last_response.body.should eql 'first' get '/1/first/second' last_response.body.should eql 'second' end it 'allows for :any as a verb' do subject.route(:any, '/abc') do "lol" end %w(get post put delete options patch).each do |m| send(m, '/abc') last_response.body.should eql 'lol' end end verbs = %w(post get head delete put options patch) verbs.each do |verb| it 'allows and properly constrain a #{verb.upcase} method' do subject.send(verb, '/example') do verb end send(verb, '/example') last_response.body.should eql verb == 'head' ? '' : verb # Call it with a method other than the properly constrained one. send(used_verb = verbs[(verbs.index(verb) + 2) % verbs.size], '/example') last_response.status.should eql used_verb == 'options' ? 204 : 405 end end it 'returns a 201 response code for POST by default' do subject.post('example') do "Created" end post '/example' last_response.status.should eql 201 last_response.body.should eql 'Created' end it 'returns a 405 for an unsupported method with an X-Custom-Header' do subject.before { header 'X-Custom-Header', 'foo' } subject.get 'example' do "example" end put '/example' last_response.status.should eql 405 last_response.body.should eql '' last_response.headers['X-Custom-Header'].should eql 'foo' end specify '405 responses includes an Allow header specifying supported methods' do subject.get 'example' do "example" end subject.post 'example' do "example" end put '/example' last_response.headers['Allow'].should eql 'OPTIONS, GET, POST, HEAD' end specify '405 responses includes an Content-Type header' do subject.get 'example' do "example" end subject.post 'example' do "example" end put '/example' last_response.headers['Content-Type'].should eql 'text/plain' end it 'adds an OPTIONS route that returns a 204, an Allow header and a X-Custom-Header' do subject.before { header 'X-Custom-Header', 'foo' } subject.get 'example' do "example" end options '/example' last_response.status.should eql 204 last_response.body.should eql '' last_response.headers['Allow'].should eql 'OPTIONS, GET, HEAD' last_response.headers['X-Custom-Header'].should eql 'foo' end it 'allows HEAD on a GET request' do subject.get 'example' do "example" end head '/example' last_response.status.should eql 200 last_response.body.should eql '' end it 'overwrites the default HEAD request' do subject.head 'example' do error! 'nothing to see here', 400 end subject.get 'example' do "example" end head '/example' last_response.status.should eql 400 end end context "do_not_route_head!" do before :each do subject.do_not_route_head! subject.get 'example' do "example" end end it 'options does not contain HEAD' do options '/example' last_response.status.should eql 204 last_response.body.should eql '' last_response.headers['Allow'].should eql 'OPTIONS, GET' end it 'does not allow HEAD on a GET request' do head '/example' last_response.status.should eql 405 end end context "do_not_route_options!" do before :each do subject.do_not_route_options! subject.get 'example' do "example" end end it 'options does not exist' do options '/example' last_response.status.should eql 405 end end describe 'filters' do it 'adds a before filter' do subject.before { @foo = 'first' } subject.before { @bar = 'second' } subject.get '/' do "#{@foo} #{@bar}" end get '/' last_response.body.should eql 'first second' end it 'adds a before filter to current and child namespaces only' do subject.get '/' do "root - #{@foo}" end subject.namespace :blah do before { @foo = 'foo' } get '/' do "blah - #{@foo}" end namespace :bar do get '/' do "blah - bar - #{@foo}" end end end get '/' last_response.body.should eql 'root - ' get '/blah' last_response.body.should eql 'blah - foo' get '/blah/bar' last_response.body.should eql 'blah - bar - foo' end it 'adds a after_validation filter' do subject.after_validation { @foo = "first #{params[:id] }:#{params[:id].class}" } subject.after_validation { @bar = 'second' } subject.params do requires :id, type: Integer end subject.get '/' do "#{@foo} #{@bar}" end get '/', id: "32" last_response.body.should eql 'first 32:Fixnum second' end it 'adds a after filter' do m = double('after mock') subject.after { m.do_something! } subject.after { m.do_something! } subject.get '/' do @var ||= 'default' end m.should_receive(:do_something!).exactly(2).times get '/' last_response.body.should eql 'default' end it 'calls all filters when validation passes' do a = double('before mock') b = double('before_validation mock') c = double('after_validation mock') d = double('after mock') subject.params do requires :id, type: Integer end subject.resource ':id' do before { a.do_something! } before_validation { b.do_something! } after_validation { c.do_something! } after { d.do_something! } get do 'got it' end end a.should_receive(:do_something!).exactly(1).times b.should_receive(:do_something!).exactly(1).times c.should_receive(:do_something!).exactly(1).times d.should_receive(:do_something!).exactly(1).times get '/123' last_response.status.should eql 200 last_response.body.should eql 'got it' end it 'calls only before filters when validation fails' do a = double('before mock') b = double('before_validation mock') c = double('after_validation mock') d = double('after mock') subject.params do requires :id, type: Integer end subject.resource ':id' do before { a.do_something! } before_validation { b.do_something! } after_validation { c.do_something! } after { d.do_something! } get do 'got it' end end a.should_receive(:do_something!).exactly(1).times b.should_receive(:do_something!).exactly(1).times c.should_receive(:do_something!).exactly(0).times d.should_receive(:do_something!).exactly(0).times get '/abc' last_response.status.should eql 400 last_response.body.should eql 'id is invalid' end it 'calls filters in the correct order' do i = 0 a = double('before mock') b = double('before_validation mock') c = double('after_validation mock') d = double('after mock') subject.params do requires :id, type: Integer end subject.resource ':id' do before { a.here(i += 1) } before_validation { b.here(i += 1) } after_validation { c.here(i += 1) } after { d.here(i += 1) } get do 'got it' end end a.should_receive(:here).with(1).exactly(1).times b.should_receive(:here).with(2).exactly(1).times c.should_receive(:here).with(3).exactly(1).times d.should_receive(:here).with(4).exactly(1).times get '/123' last_response.status.should eql 200 last_response.body.should eql 'got it' end end context 'format' do before do subject.get("/foo") { "bar" } end it 'sets content type for txt format' do get '/foo' last_response.headers['Content-Type'].should eql 'text/plain' end it 'sets content type for json' do get '/foo.json' last_response.headers['Content-Type'].should eql 'application/json' end it 'sets content type for error' do subject.get('/error') { error!('error in plain text', 500) } get '/error' last_response.headers['Content-Type'].should eql 'text/plain' end it 'sets content type for error' do subject.format :json subject.get('/error') { error!('error in json', 500) } get '/error.json' last_response.headers['Content-Type'].should eql 'application/json' end it 'sets content type for xml' do subject.format :xml subject.get('/error') { error!('error in xml', 500) } get '/error.xml' last_response.headers['Content-Type'].should eql 'application/xml' end context 'with a custom content_type' do before do subject.content_type :custom, 'application/custom' subject.formatter :custom, lambda { |object, env| "custom" } subject.get('/custom') { 'bar' } subject.get('/error') { error!('error in custom', 500) } end it 'sets content type' do get '/custom.custom' last_response.headers['Content-Type'].should eql 'application/custom' end it 'sets content type for error' do get '/error.custom' last_response.headers['Content-Type'].should eql 'application/custom' end end end context 'custom middleware' do module ApiSpec class PhonyMiddleware def initialize(app, *args) @args = args @app = app @block = true if block_given? end def call(env) env['phony.args'] ||= [] env['phony.args'] << @args env['phony.block'] = true if @block @app.call(env) end end end describe '.middleware' do it 'includes middleware arguments from settings' do settings = Grape::Util::HashStack.new settings.stub(:stack).and_return([{ middleware: [[ApiSpec::PhonyMiddleware, 'abc', 123]] }]) subject.stub(:settings).and_return(settings) subject.middleware.should eql [[ApiSpec::PhonyMiddleware, 'abc', 123]] end it 'includes all middleware from stacked settings' do settings = Grape::Util::HashStack.new settings.stub(:stack).and_return [ { middleware: [[ApiSpec::PhonyMiddleware, 123], [ApiSpec::PhonyMiddleware, 'abc']] }, { middleware: [[ApiSpec::PhonyMiddleware, 'foo']] } ] subject.stub(:settings).and_return(settings) subject.middleware.should eql [ [ApiSpec::PhonyMiddleware, 123], [ApiSpec::PhonyMiddleware, 'abc'], [ApiSpec::PhonyMiddleware, 'foo'] ] end end describe '.use' do it 'adds middleware' do subject.use ApiSpec::PhonyMiddleware, 123 subject.middleware.should eql [[ApiSpec::PhonyMiddleware, 123]] end it 'does not show up outside the namespace' do subject.use ApiSpec::PhonyMiddleware, 123 subject.namespace :awesome do use ApiSpec::PhonyMiddleware, 'abc' middleware.should == [[ApiSpec::PhonyMiddleware, 123], [ApiSpec::PhonyMiddleware, 'abc']] end subject.middleware.should eql [[ApiSpec::PhonyMiddleware, 123]] end it 'calls the middleware' do subject.use ApiSpec::PhonyMiddleware, 'hello' subject.get '/' do env['phony.args'].first.first end get '/' last_response.body.should eql 'hello' end it 'adds a block if one is given' do block = lambda {} subject.use ApiSpec::PhonyMiddleware, &block subject.middleware.should eql [[ApiSpec::PhonyMiddleware, block]] end it 'uses a block if one is given' do block = lambda {} subject.use ApiSpec::PhonyMiddleware, &block subject.get '/' do env['phony.block'].inspect end get '/' last_response.body.should == 'true' end it 'does not destroy the middleware settings on multiple runs' do block = lambda {} subject.use ApiSpec::PhonyMiddleware, &block subject.get '/' do env['phony.block'].inspect end 2.times do get '/' last_response.body.should == 'true' end end it 'mounts behind error middleware' do m = Class.new(Grape::Middleware::Base) do def before throw :error, message: "Caught in the Net", status: 400 end end subject.use m subject.get "/" do end get "/" last_response.status.should == 400 last_response.body.should == "Caught in the Net" end end end describe '.http_basic' do it 'protects any resources on the same scope' do subject.http_basic do |u, p| u == 'allow' end subject.get(:hello) { "Hello, world." } get '/hello' last_response.status.should eql 401 get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') last_response.status.should eql 200 end it 'is scopable' do subject.get(:hello) { "Hello, world." } subject.namespace :admin do http_basic do |u, p| u == 'allow' end get(:hello) { "Hello, world." } end get '/hello' last_response.status.should eql 200 get '/admin/hello' last_response.status.should eql 401 end it 'is callable via .auth as well' do subject.auth :http_basic do |u, p| u == 'allow' end subject.get(:hello) { "Hello, world." } get '/hello' last_response.status.should eql 401 get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') last_response.status.should eql 200 end it 'has access to the current endpoint' do basic_auth_context = nil subject.http_basic do |u, p| basic_auth_context = self u == 'allow' end subject.get(:hello) { "Hello, world." } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') basic_auth_context.should be_an_instance_of(Grape::Endpoint) end it 'has access to helper methods' do subject.helpers do def authorize(u, p) u == 'allow' && p == 'whatever' end end subject.http_basic do |u, p| authorize(u, p) end subject.get(:hello) { "Hello, world." } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') last_response.status.should eql 200 get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('disallow', 'whatever') last_response.status.should eql 401 end it 'can set instance variables accessible to routes' do subject.http_basic do |u, p| @hello = "Hello, world." u == 'allow' end subject.get(:hello) { @hello } get '/hello', {}, 'HTTP_AUTHORIZATION' => encode_basic_auth('allow', 'whatever') last_response.status.should eql 200 last_response.body.should eql "Hello, world." end end describe '.logger' do it 'returns an instance of Logger class by default' do subject.logger.class.should eql Logger end it 'allows setting a custom logger' do mylogger = Class.new subject.logger mylogger mylogger.should_receive(:info).exactly(1).times subject.logger.info "this will be logged" end it "defaults to a standard logger log format" do t = Time.at(100) Time.stub(:now).and_return(t) STDOUT.should_receive(:write).with("I, [#{Logger::Formatter.new.send(:format_datetime, t)}\##{Process.pid}] INFO -- : this will be logged\n") subject.logger.info "this will be logged" end end describe '.helpers' do it 'is accessible from the endpoint' do subject.helpers do def hello "Hello, world." end end subject.get '/howdy' do hello end get '/howdy' last_response.body.should eql 'Hello, world.' end it 'is scopable' do subject.helpers do def generic 'always there' end end subject.namespace :admin do helpers do def secret 'only in admin' end end get '/secret' do [generic, secret].join ':' end end subject.get '/generic' do [generic, respond_to?(:secret)].join ':' end get '/generic' last_response.body.should eql 'always there:false' get '/admin/secret' last_response.body.should eql 'always there:only in admin' end it 'is reopenable' do subject.helpers do def one 1 end end subject.helpers do def two 2 end end subject.get 'howdy' do [one, two] end lambda { get '/howdy' }.should_not raise_error end it 'allows for modules' do mod = Module.new do def hello "Hello, world." end end subject.helpers mod subject.get '/howdy' do hello end get '/howdy' last_response.body.should eql 'Hello, world.' end it 'allows multiple calls with modules and blocks' do subject.helpers Module.new do def one 1 end end subject.helpers Module.new do def two 2 end end subject.helpers do def three 3 end end subject.get 'howdy' do [one, two, three] end lambda { get '/howdy' }.should_not raise_error end end describe '.scope' do # TODO: refactor this to not be tied to versioning. How about a generic # .setting macro? it 'scopes the various settings' do subject.prefix 'new' subject.scope :legacy do prefix 'legacy' get '/abc' do 'abc' end end subject.get '/def' do 'def' end get '/new/abc' last_response.status.should eql 404 get '/legacy/abc' last_response.status.should eql 200 get '/legacy/def' last_response.status.should eql 404 get '/new/def' last_response.status.should eql 200 end end describe '.rescue_from' do it 'does not rescue errors when rescue_from is not set' do subject.get '/exception' do raise "rain!" end lambda { get '/exception' }.should raise_error end it 'rescues all errors if rescue_from :all is called' do subject.rescue_from :all subject.get '/exception' do raise "rain!" end get '/exception' last_response.status.should eql 500 end it 'rescues only certain errors if rescue_from is called with specific errors' do subject.rescue_from ArgumentError subject.get('/rescued') { raise ArgumentError } subject.get('/unrescued') { raise "beefcake" } get '/rescued' last_response.status.should eql 500 lambda { get '/unrescued' }.should raise_error end context 'CustomError subclass of Grape::Exceptions::Base' do before do class CustomError < Grape::Exceptions::Base; end end it 'does not re-raise exceptions of type Grape::Exceptions::Base' do subject.get('/custom_exception') { raise CustomError } lambda { get '/custom_exception' }.should_not raise_error end it 'rescues custom grape exceptions' do subject.rescue_from CustomError do |e| rack_response('New Error', e.status) end subject.get '/custom_error' do raise CustomError, status: 400, message: 'Custom Error' end get '/custom_error' last_response.status.should == 400 last_response.body.should == 'New Error' end end it 'can rescue exceptions raised in the formatter' do formatter = double(:formatter) formatter.stub(:call) { raise StandardError } Grape::Formatter::Base.stub(:formatter_for) { formatter } subject.rescue_from :all do |e| rack_response('Formatter Error', 500) end subject.get('/formatter_exception') { 'Hello world' } get '/formatter_exception' last_response.status.should eql 500 last_response.body.should == 'Formatter Error' end end describe '.rescue_from klass, block' do it 'rescues Exception' do subject.rescue_from RuntimeError do |e| rack_response("rescued from #{e.message}", 202) end subject.get '/exception' do raise "rain!" end get '/exception' last_response.status.should eql 202 last_response.body.should == 'rescued from rain!' end context 'custom errors' do before do class ConnectionError < RuntimeError; end class DatabaseError < RuntimeError; end class CommunicationError < StandardError; end end it 'rescues an error via rescue_from :all' do subject.rescue_from :all do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError end get '/exception' last_response.status.should eql 500 last_response.body.should == 'rescued from ConnectionError' end it 'rescues a specific error' do subject.rescue_from ConnectionError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/exception' do raise ConnectionError end get '/exception' last_response.status.should eql 500 last_response.body.should == 'rescued from ConnectionError' end it 'rescues multiple specific errors' do subject.rescue_from ConnectionError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.rescue_from DatabaseError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/connection' do raise ConnectionError end subject.get '/database' do raise DatabaseError end get '/connection' last_response.status.should eql 500 last_response.body.should == 'rescued from ConnectionError' get '/database' last_response.status.should eql 500 last_response.body.should == 'rescued from DatabaseError' end it 'does not rescue a different error' do subject.rescue_from RuntimeError do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/uncaught' do raise CommunicationError end lambda { get '/uncaught' }.should raise_error(CommunicationError) end end end describe '.rescue_from klass, lambda' do it 'rescues an error with the lambda' do subject.rescue_from ArgumentError, lambda { rack_response("rescued with a lambda", 400) } subject.get('/rescue_lambda') { raise ArgumentError } get '/rescue_lambda' last_response.status.should == 400 last_response.body.should == "rescued with a lambda" end it 'can execute the lambda with an argument' do subject.rescue_from ArgumentError, lambda { |e| rack_response(e.message, 400) } subject.get('/rescue_lambda') { raise ArgumentError, 'lambda takes an argument' } get '/rescue_lambda' last_response.status.should == 400 last_response.body.should == 'lambda takes an argument' end end describe '.rescue_from klass, with: method' do it 'rescues an error with the specified message' do def rescue_arg_error Rack::Response.new('rescued with a method', 400) end subject.rescue_from ArgumentError, with: rescue_arg_error subject.get('/rescue_method') { raise ArgumentError } get '/rescue_method' last_response.status.should == 400 last_response.body.should == 'rescued with a method' end end describe '.rescue_from klass, rescue_subclasses: boolean' do before do module APIErrors class ParentError < StandardError; end class ChildError < ParentError; end end end it 'rescues error as well as subclass errors with rescue_subclasses option set' do subject.rescue_from APIErrors::ParentError, rescue_subclasses: true do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/caught_child' do raise APIErrors::ChildError end subject.get '/caught_parent' do raise APIErrors::ParentError end subject.get '/uncaught_parent' do raise StandardError end get '/caught_child' last_response.status.should eql 500 get '/caught_parent' last_response.status.should eql 500 lambda { get '/uncaught_parent' }.should raise_error(StandardError) end it 'does not rescue child errors if rescue_subclasses is false' do subject.rescue_from APIErrors::ParentError, rescue_subclasses: false do |e| rack_response("rescued from #{e.class.name}", 500) end subject.get '/uncaught' do raise APIErrors::ChildError end lambda { get '/uncaught' }.should raise_error(APIErrors::ChildError) end end describe '.error_format' do it 'rescues all errors and return :txt' do subject.rescue_from :all subject.format :txt subject.get '/exception' do raise "rain!" end get '/exception' last_response.body.should eql "rain!" end it 'rescues all errors and return :txt with backtrace' do subject.rescue_from :all, backtrace: true subject.format :txt subject.get '/exception' do raise "rain!" end get '/exception' last_response.body.start_with?("rain!\r\n").should be true end it 'rescues all errors with a default formatter' do subject.default_format :foo subject.content_type :foo, "text/foo" subject.rescue_from :all subject.get '/exception' do raise "rain!" end get '/exception.foo' last_response.body.should start_with "rain!" end it 'defaults the error formatter to format' do subject.format :json subject.rescue_from :all subject.content_type :json, "application/json" subject.content_type :foo, "text/foo" subject.get '/exception' do raise "rain!" end get '/exception.json' last_response.body.should == '{"error":"rain!"}' get '/exception.foo' last_response.body.should == '{"error":"rain!"}' end context 'class' do before :each do class CustomErrorFormatter def self.call(message, backtrace, options, env) "message: #{message} @backtrace" end end end it 'returns a custom error format' do subject.rescue_from :all, backtrace: true subject.error_formatter :txt, CustomErrorFormatter subject.get '/exception' do raise "rain!" end get '/exception' last_response.body.should == "message: rain! @backtrace" end end describe 'with' do context 'class' do before :each do class CustomErrorFormatter def self.call(message, backtrace, option, env) "message: #{message} @backtrace" end end end it 'returns a custom error format' do subject.rescue_from :all, backtrace: true subject.error_formatter :txt, with: CustomErrorFormatter subject.get('/exception') { raise "rain!" } get '/exception' last_response.body.should == 'message: rain! @backtrace' end end end it 'rescues all errors and return :json' do subject.rescue_from :all subject.format :json subject.get '/exception' do raise "rain!" end get '/exception' last_response.body.should eql '{"error":"rain!"}' end it 'rescues all errors and return :json with backtrace' do subject.rescue_from :all, backtrace: true subject.format :json subject.get '/exception' do raise "rain!" end get '/exception' json = MultiJson.load(last_response.body) json["error"].should eql 'rain!' json["backtrace"].length.should > 0 end it 'rescues error! and return txt' do subject.format :txt subject.get '/error' do error!("Access Denied", 401) end get '/error' last_response.body.should eql "Access Denied" end it 'rescues error! and return json' do subject.format :json subject.get '/error' do error!("Access Denied", 401) end get '/error' last_response.body.should eql '{"error":"Access Denied"}' end end describe '.content_type' do it 'sets additional content-type' do subject.content_type :xls, "application/vnd.ms-excel" subject.get :excel do "some binary content" end get '/excel.xls' last_response.content_type.should == "application/vnd.ms-excel" end it 'allows to override content-type' do subject.get :content do content_type "text/javascript" "var x = 1;" end get '/content' last_response.content_type.should == "text/javascript" end it 'removes existing content types' do subject.content_type :xls, "application/vnd.ms-excel" subject.get :excel do "some binary content" end get '/excel.json' last_response.status.should == 406 last_response.body.should == "The requested format 'txt' is not supported." end end describe '.formatter' do context 'multiple formatters' do before :each do subject.formatter :json, lambda { |object, env| "{\"custom_formatter\":\"#{object[:some] }\"}" } subject.formatter :txt, lambda { |object, env| "custom_formatter: #{object[:some] }" } subject.get :simple do { some: 'hash' } end end it 'sets one formatter' do get '/simple.json' last_response.body.should eql '{"custom_formatter":"hash"}' end it 'sets another formatter' do get '/simple.txt' last_response.body.should eql 'custom_formatter: hash' end end context 'custom formatter' do before :each do subject.content_type :json, 'application/json' subject.content_type :custom, 'application/custom' subject.formatter :custom, lambda { |object, env| "{\"custom_formatter\":\"#{object[:some] }\"}" } subject.get :simple do { some: 'hash' } end end it 'uses json' do get '/simple.json' last_response.body.should eql '{"some":"hash"}' end it 'uses custom formatter' do get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' last_response.body.should eql '{"custom_formatter":"hash"}' end end context 'custom formatter class' do module CustomFormatter def self.call(object, env) "{\"custom_formatter\":\"#{object[:some] }\"}" end end before :each do subject.content_type :json, 'application/json' subject.content_type :custom, 'application/custom' subject.formatter :custom, CustomFormatter subject.get :simple do { some: 'hash' } end end it 'uses json' do get '/simple.json' last_response.body.should eql '{"some":"hash"}' end it 'uses custom formatter' do get '/simple.custom', 'HTTP_ACCEPT' => 'application/custom' last_response.body.should eql '{"custom_formatter":"hash"}' end end end describe '.parser' do it 'parses data in format requested by content-type' do subject.format :json subject.post '/data' do { x: params[:x] } end post "/data", '{"x":42}', 'CONTENT_TYPE' => 'application/json' last_response.status.should == 201 last_response.body.should == '{"x":42}' end context 'lambda parser' do before :each do subject.content_type :txt, "text/plain" subject.content_type :custom, "text/custom" subject.parser :custom, lambda { |object, env| { object.to_sym => object.to_s.reverse } } subject.put :simple do params[:simple] end end ["text/custom", "text/custom; charset=UTF-8"].each do |content_type| it "uses parser for #{content_type}" do put '/simple', "simple", "CONTENT_TYPE" => content_type last_response.status.should == 200 last_response.body.should eql "elpmis" end end end context 'custom parser class' do module CustomParser def self.call(object, env) { object.to_sym => object.to_s.reverse } end end before :each do subject.content_type :txt, "text/plain" subject.content_type :custom, "text/custom" subject.parser :custom, CustomParser subject.put :simple do params[:simple] end end it 'uses custom parser' do put '/simple', "simple", "CONTENT_TYPE" => "text/custom" last_response.status.should == 200 last_response.body.should eql "elpmis" end end context "multi_xml" do it "doesn't parse yaml" do subject.put :yaml do params[:tag] end put '/yaml', 'a123', "CONTENT_TYPE" => "application/xml" last_response.status.should == 400 last_response.body.should eql 'Disallowed type attribute: "symbol"' end end context "none parser class" do before :each do subject.parser :json, nil subject.put "data" do "body: #{env['api.request.body'] }" end end it "does not parse data" do put '/data', 'not valid json', "CONTENT_TYPE" => "application/json" last_response.status.should == 200 last_response.body.should == "body: not valid json" end end end describe '.default_format' do before :each do subject.format :json subject.default_format :json end it 'returns data in default format' do subject.get '/data' do { x: 42 } end get "/data" last_response.status.should == 200 last_response.body.should == '{"x":42}' end it 'parses data in default format' do subject.post '/data' do { x: params[:x] } end post "/data", '{"x":42}', "CONTENT_TYPE" => "" last_response.status.should == 201 last_response.body.should == '{"x":42}' end end describe '.default_error_status' do it 'allows setting default_error_status' do subject.rescue_from :all subject.default_error_status 200 subject.get '/exception' do raise "rain!" end get '/exception' last_response.status.should eql 200 end it 'has a default error status' do subject.rescue_from :all subject.get '/exception' do raise "rain!" end get '/exception' last_response.status.should eql 500 end it 'uses the default error status in error!' do subject.rescue_from :all subject.default_error_status 400 subject.get '/exception' do error! "rain!" end get '/exception' last_response.status.should eql 400 end end context 'routes' do describe 'empty api structure' do it 'returns an empty array of routes' do subject.routes.should == [] end end describe 'single method api structure' do before(:each) do subject.get :ping do 'pong' end end it 'returns one route' do subject.routes.size.should == 1 route = subject.routes[0] route.route_version.should be_nil route.route_path.should == "/ping(.:format)" route.route_method.should == "GET" end end describe 'api structure with two versions and a namespace' do before :each do subject.version 'v1', using: :path subject.get 'version' do api.version end # version v2 subject.version 'v2', using: :path subject.prefix 'p' subject.namespace 'n1' do namespace 'n2' do get 'version' do api.version end end end end it 'returns the latest version set' do subject.version.should == 'v2' end it 'returns versions' do subject.versions.should == ['v1', 'v2'] end it 'sets route paths' do subject.routes.size.should >= 2 subject.routes[0].route_path.should == "/:version/version(.:format)" subject.routes[1].route_path.should == "/p/:version/n1/n2/version(.:format)" end it 'sets route versions' do subject.routes[0].route_version.should == 'v1' subject.routes[1].route_version.should == 'v2' end it 'sets a nested namespace' do subject.routes[1].route_namespace.should == "/n1/n2" end it 'sets prefix' do subject.routes[1].route_prefix.should == 'p' end end describe 'api structure with additional parameters' do before(:each) do subject.get 'split/:string', params: { "token" => "a token" }, optional_params: { "limit" => "the limit" } do params[:string].split(params[:token], (params[:limit] || 0).to_i) end end it 'splits a string' do get "/split/a,b,c.json", token: ',' last_response.body.should == '["a","b","c"]' end it 'splits a string with limit' do get "/split/a,b,c.json", token: ',', limit: '2' last_response.body.should == '["a","b,c"]' end it 'sets route_params' do subject.routes.map { |route| { params: route.route_params, optional_params: route.route_optional_params } }.should eq [ { params: { "string" => "", "token" => "a token" }, optional_params: { "limit" => "the limit" } } ] end end end context 'desc' do it 'empty array of routes' do subject.routes.should == [] end it 'empty array of routes' do subject.desc "grape api" subject.routes.should == [] end it 'describes a method' do subject.desc "first method" subject.get :first do ; end subject.routes.length.should == 1 route = subject.routes.first route.route_description.should == "first method" route.route_foo.should be_nil route.route_params.should == {} end it 'describes methods separately' do subject.desc "first method" subject.get :first do ; end subject.desc "second method" subject.get :second do ; end subject.routes.count.should == 2 subject.routes.map { |route| { description: route.route_description, params: route.route_params } }.should eq [ { description: "first method", params: {} }, { description: "second method", params: {} } ] end it 'resets desc' do subject.desc "first method" subject.get :first do ; end subject.get :second do ; end subject.routes.map { |route| { description: route.route_description, params: route.route_params } }.should eq [ { description: "first method", params: {} }, { description: nil, params: {} } ] end it 'namespaces and describe arbitrary parameters' do subject.namespace 'ns' do desc "ns second", foo: "bar" get 'second' do ; end end subject.routes.map { |route| { description: route.route_description, foo: route.route_foo, params: route.route_params } }.should eq [ { description: "ns second", foo: "bar", params: {} }, ] end it 'includes details' do subject.desc "method", details: "method details" subject.get 'method' do ; end subject.routes.map { |route| { description: route.route_description, details: route.route_details, params: route.route_params } }.should eq [ { description: "method", details: "method details", params: {} }, ] end it 'describes a method with parameters' do subject.desc "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } } subject.get 'reverse' do params[:s].reverse end subject.routes.map { |route| { description: route.route_description, params: route.route_params } }.should eq [ { description: "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } } } ] end it 'merges the parameters of the namespace with the parameters of the method' do subject.desc "namespace" subject.params do requires :ns_param, desc: "namespace parameter" end subject.namespace 'ns' do desc "method" params do optional :method_param, desc: "method parameter" end get 'method' do ; end end subject.routes.map { |route| { description: route.route_description, params: route.route_params } }.should eq [ { description: "method", params: { "ns_param" => { required: true, desc: "namespace parameter" }, "method_param" => { required: false, desc: "method parameter" } } } ] end it 'merges the parameters of nested namespaces' do subject.desc "ns1" subject.params do optional :ns_param, desc: "ns param 1" requires :ns1_param, desc: "ns1 param" end subject.namespace 'ns1' do desc "ns2" params do requires :ns_param, desc: "ns param 2" requires :ns2_param, desc: "ns2 param" end namespace 'ns2' do desc "method" params do optional :method_param, desc: "method param" end get 'method' do ; end end end subject.routes.map { |route| { description: route.route_description, params: route.route_params } }.should eq [ { description: "method", params: { "ns_param" => { required: true, desc: "ns param 2" }, "ns1_param" => { required: true, desc: "ns1 param" }, "ns2_param" => { required: true, desc: "ns2 param" }, "method_param" => { required: false, desc: "method param" } } } ] end it "groups nested params and prevents overwriting of params with same name in different groups" do subject.desc "method" subject.params do group :group1 do optional :param1, desc: "group1 param1 desc" requires :param2, desc: "group1 param2 desc" end group :group2 do optional :param1, desc: "group2 param1 desc" requires :param2, desc: "group2 param2 desc" end end subject.get "method" do ; end subject.routes.map { |route| route.route_params }.should eq [{ "group1" => { required: true, type: "Array" }, "group1[param1]" => { required: false, desc: "group1 param1 desc" }, "group1[param2]" => { required: true, desc: "group1 param2 desc" }, "group2" => { required: true, type: "Array" }, "group2[param1]" => { required: false, desc: "group2 param1 desc" }, "group2[param2]" => { required: true, desc: "group2 param2 desc" } }] end it 'uses full name of parameters in nested groups' do subject.desc "nesting" subject.params do requires :root_param, desc: "root param" group :nested do requires :nested_param, desc: "nested param" end end subject.get 'method' do ; end subject.routes.map { |route| { description: route.route_description, params: route.route_params } }.should eq [ { description: "nesting", params: { "root_param" => { required: true, desc: "root param" }, "nested" => { required: true, type: "Array" }, "nested[nested_param]" => { required: true, desc: "nested param" } } } ] end it 'allows to set the type attribute on :group element' do subject.params do group :foo, type: Array do optional :bar end end end it 'parses parameters when no description is given' do subject.params do requires :one_param, desc: "one param" end subject.get 'method' do ; end subject.routes.map { |route| { description: route.route_description, params: route.route_params } }.should eq [ { description: nil, params: { "one_param" => { required: true, desc: "one param" } } } ] end it 'does not symbolize params' do subject.desc "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } } subject.get 'reverse/:s' do params[:s].reverse end subject.routes.map { |route| { description: route.route_description, params: route.route_params } }.should eq [ { description: "Reverses a string.", params: { "s" => { desc: "string to reverse", type: "string" } } } ] end end describe '.mount' do let(:mounted_app) { lambda { |env| [200, {}, ["MOUNTED"]] } } context 'with a bare rack app' do before do subject.mount mounted_app => '/mounty' end it 'makes a bare Rack app available at the endpoint' do get '/mounty' last_response.body.should == 'MOUNTED' end it 'anchors the routes, passing all subroutes to it' do get '/mounty/awesome' last_response.body.should == 'MOUNTED' end it 'is able to cascade' do subject.mount lambda { |env| headers = {} headers['X-Cascade'] == 'pass' unless env['PATH_INFO'].include?('boo') [200, headers, ["Farfegnugen"]] } => '/' get '/boo' last_response.body.should == 'Farfegnugen' get '/mounty' last_response.body.should == 'MOUNTED' end end context 'without a hash' do it 'calls through setting the route to "/"' do subject.mount mounted_app get '/' last_response.body.should == 'MOUNTED' end end context 'mounting an API' do it 'applies the settings of the mounting api' do subject.version 'v1', using: :path subject.namespace :cool do app = Class.new(Grape::API) app.get('/awesome') do "yo" end mount app end get '/v1/cool/awesome' last_response.body.should == 'yo' end it 'applies the settings to nested mounted apis' do subject.version 'v1', using: :path subject.namespace :cool do inner_app = Class.new(Grape::API) inner_app.get('/awesome') do "yo" end app = Class.new(Grape::API) app.mount inner_app mount app end get '/v1/cool/awesome' last_response.body.should == 'yo' end it 'inherits rescues even when some defined by mounted' do subject.rescue_from :all do |e| rack_response("rescued from #{e.message}", 202) end subject.namespace :mounted do app = Class.new(Grape::API) app.rescue_from ArgumentError app.get('/fail') { raise "doh!" } mount app end get '/mounted/fail' last_response.status.should eql 202 last_response.body.should == 'rescued from doh!' end it 'collects the routes of the mounted api' do subject.namespace :cool do app = Class.new(Grape::API) app.get('/awesome') {} app.post('/sauce') {} mount app end subject.routes.size.should == 2 subject.routes.first.route_path.should =~ %r{\/cool\/awesome} subject.routes.last.route_path.should =~ %r{\/cool\/sauce} end it 'mounts on a path' do subject.namespace :cool do app = Class.new(Grape::API) app.get '/awesome' do "sauce" end mount app => '/mounted' end get "/mounted/cool/awesome" last_response.status.should == 200 last_response.body.should == "sauce" end it 'mounts on a nested path' do app1 = Class.new(Grape::API) app2 = Class.new(Grape::API) app2.get '/nice' do "play" end # note that the reverse won't work, mount from outside-in subject.mount app1 => '/app1' app1.mount app2 => '/app2' get "/app1/app2/nice" last_response.status.should == 200 last_response.body.should == "play" options "/app1/app2/nice" last_response.status.should == 204 end it 'responds to options' do app = Class.new(Grape::API) app.get '/colour' do 'red' end app.namespace :pears do get '/colour' do 'green' end end subject.namespace :apples do mount app end get '/apples/colour' last_response.status.should eql 200 last_response.body.should == 'red' options '/apples/colour' last_response.status.should eql 204 get '/apples/pears/colour' last_response.status.should eql 200 last_response.body.should == 'green' options '/apples/pears/colour' last_response.status.should eql 204 end it 'responds to options with path versioning' do subject.version 'v1', using: :path subject.namespace :apples do app = Class.new(Grape::API) app.get('/colour') do "red" end mount app end get '/v1/apples/colour' last_response.status.should eql 200 last_response.body.should == 'red' options '/v1/apples/colour' last_response.status.should eql 204 end end end describe '.endpoints' do it 'adds one for each route created' do subject.get '/' subject.post '/' subject.endpoints.size.should == 2 end end describe '.compile' do it 'sets the instance' do subject.instance.should be_nil subject.compile subject.instance.should be_kind_of(subject) end end describe '.change!' do it 'invalidates any compiled instance' do subject.compile subject.change! subject.instance.should be_nil end end describe ".endpoint" do before(:each) do subject.format :json subject.get '/endpoint/options' do { path: options[:path], source_location: source.source_location } end end it 'path' do get '/endpoint/options' options = MultiJson.load(last_response.body) options["path"].should == ["/endpoint/options"] options["source_location"][0].should include "api_spec.rb" options["source_location"][1].to_i.should > 0 end end describe '.route' do context 'plain' do before(:each) do subject.get '/' do route.route_path end subject.get '/path' do route.route_path end end it 'provides access to route info' do get '/' last_response.body.should == "/(.:format)" get '/path' last_response.body.should == "/path(.:format)" end end context 'with desc' do before(:each) do subject.desc 'returns description' subject.get '/description' do route.route_description end subject.desc 'returns parameters', params: { "x" => "y" } subject.get '/params/:id' do route.route_params[params[:id]] end end it 'returns route description' do get '/description' last_response.body.should == "returns description" end it 'returns route parameters' do get '/params/x' last_response.body.should == "y" end end end describe '.format' do context ':txt' do before(:each) do subject.format :txt subject.content_type :json, "application/json" subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end it 'forces txt without an extension' do get '/meaning_of_life' last_response.body.should == { meaning_of_life: 42 }.to_s end it 'does not force txt with an extension' do get '/meaning_of_life.json' last_response.body.should == { meaning_of_life: 42 }.to_json end it 'forces txt from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' last_response.body.should == { meaning_of_life: 42 }.to_s end end context ':txt only' do before(:each) do subject.format :txt subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end it 'forces txt without an extension' do get '/meaning_of_life' last_response.body.should == { meaning_of_life: 42 }.to_s end it 'forces txt with the wrong extension' do get '/meaning_of_life.json' last_response.body.should == { meaning_of_life: 42 }.to_s end it 'forces txt from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'application/json' last_response.body.should == { meaning_of_life: 42 }.to_s end end context ':json' do before(:each) do subject.format :json subject.content_type :txt, "text/plain" subject.get '/meaning_of_life' do { meaning_of_life: 42 } end end it 'forces json without an extension' do get '/meaning_of_life' last_response.body.should == { meaning_of_life: 42 }.to_json end it 'does not force json with an extension' do get '/meaning_of_life.txt' last_response.body.should == { meaning_of_life: 42 }.to_s end it 'forces json from a non-accepting header' do get '/meaning_of_life', {}, 'HTTP_ACCEPT' => 'text/html' last_response.body.should == { meaning_of_life: 42 }.to_json end it 'can be overwritten with an explicit content type' do subject.get '/meaning_of_life_with_content_type' do content_type "text/plain" { meaning_of_life: 42 }.to_s end get '/meaning_of_life_with_content_type' last_response.body.should == { meaning_of_life: 42 }.to_s end it 'raised :error from middleware' do middleware = Class.new(Grape::Middleware::Base) do def before throw :error, message: "Unauthorized", status: 42 end end subject.use middleware subject.get do end get "/" last_response.status.should == 42 last_response.body.should == { error: "Unauthorized" }.to_json end end context ':serializable_hash' do before(:each) do class SerializableHashExample def serializable_hash { abc: 'def' } end end subject.format :serializable_hash end it 'instance' do subject.get '/example' do SerializableHashExample.new end get '/example' last_response.body.should == '{"abc":"def"}' end it 'root' do subject.get '/example' do { "root" => SerializableHashExample.new } end get '/example' last_response.body.should == '{"root":{"abc":"def"}}' end it 'array' do subject.get '/examples' do [SerializableHashExample.new, SerializableHashExample.new] end get '/examples' last_response.body.should == '[{"abc":"def"},{"abc":"def"}]' end end context ":xml" do before(:each) do subject.format :xml end it 'string' do subject.get "/example" do "example" end get '/example' last_response.status.should == 500 last_response.body.should == <<-XML cannot convert String to xml XML end it 'hash' do subject.get "/example" do ActiveSupport::OrderedHash[ :example1, "example1", :example2, "example2" ] end get '/example' last_response.status.should == 200 last_response.body.should == <<-XML example1 example2 XML end it 'array' do subject.get "/example" do ["example1", "example2"] end get '/example' last_response.status.should == 200 last_response.body.should == <<-XML example1 example2 XML end it 'raised :error from middleware' do middleware = Class.new(Grape::Middleware::Base) do def before throw :error, message: "Unauthorized", status: 42 end end subject.use middleware subject.get do end get "/" last_response.status.should == 42 last_response.body.should == <<-XML Unauthorized XML end end end context "catch-all" do before do api1 = Class.new(Grape::API) api1.version 'v1', using: :path api1.get "hello" do "v1" end api2 = Class.new(Grape::API) api2.version 'v2', using: :path api2.get "hello" do "v2" end subject.mount api1 subject.mount api2 end [true, false].each do |anchor| it "anchor=#{anchor}" do subject.route :any, '*path', anchor: anchor do error!("Unrecognized request path: #{params[:path] } - #{env['PATH_INFO'] }#{env['SCRIPT_NAME'] }", 404) end get "/v1/hello" last_response.status.should == 200 last_response.body.should == "v1" get "/v2/hello" last_response.status.should == 200 last_response.body.should == "v2" get "/foobar" last_response.status.should == 404 last_response.body.should == "Unrecognized request path: foobar - /foobar" end end end context "cascading" do context "via version" do it "cascades" do subject.version 'v1', using: :path, cascade: true get "/v1/hello" last_response.status.should == 404 last_response.headers["X-Cascade"].should == "pass" end it "does not cascade" do subject.version 'v2', using: :path, cascade: false get "/v2/hello" last_response.status.should == 404 last_response.headers.keys.should_not include "X-Cascade" end end context "via endpoint" do it "cascades" do subject.cascade true get "/hello" last_response.status.should == 404 last_response.headers["X-Cascade"].should == "pass" end it "does not cascade" do subject.cascade false get "/hello" last_response.status.should == 404 last_response.headers.keys.should_not include "X-Cascade" end end end context 'with json default_error_formatter' do it 'returns json error' do subject.content_type :json, "application/json" subject.default_error_formatter :json subject.get '/something' do 'foo' end get '/something' last_response.status.should == 406 last_response.body.should == "{\"error\":\"The requested format 'txt' is not supported.\"}" end end end