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 '.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' 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! self.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 end it 'param versioned APIs' do subject.version 'v1', :using => :param subject.enable_root_route! versioned_get "/", "v1", :using => :param 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) 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 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' do subject.get 'example' do "example" end put '/example' last_response.status.should eql 405 last_response.body.should eql '' 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 and an Allow header' do 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' 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 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 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 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 '.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 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 403 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 403 lambda{ get '/unrescued' }.should raise_error end it 'does not re-raise exceptions of type Grape::Exception::Base' do class CustomError < Grape::Exceptions::Base; end subject.get('/custom_exception'){ raise CustomError } lambda{ get '/custom_exception' }.should_not raise_error end it 'rescues custom grape exceptions' do class CustomError < Grape::Exceptions::Base; end 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 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 it 'rescues an error via rescue_from :all' do class ConnectionError < RuntimeError; end 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 class ConnectionError < RuntimeError; end 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 class ConnectionError < RuntimeError; end class DatabaseError < RuntimeError; end 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 class CommunicationError < RuntimeError; end 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 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 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 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 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 403 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[param1]" => { :required => false, :desc => "group1 param1 desc" }, "group1[param2]" => { :required => true, :desc => "group1 param2 desc" }, "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[nested_param]" => { :required => true, :desc => "nested param" } } } ] 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 '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 =~ /\/cool\/awesome/ subject.routes.last.route_path.should =~ /\/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" 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 '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 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 end