Migrate to RSpec (#228)

This commit is contained in:
Luca Guidi 2017-06-05 12:01:50 +02:00 committed by GitHub
parent 0f44b8525e
commit 1adb006c57
81 changed files with 3558 additions and 3598 deletions

2
.rspec Normal file
View File

@ -0,0 +1,2 @@
--color
--require spec_helper

View File

@ -6,7 +6,6 @@ unless ENV['TRAVIS']
gem 'yard', require: false
end
gem 'minitest', '~> 5.8'
gem 'hanami-utils', '~> 1.0', require: false, git: 'https://github.com/hanami/utils.git', branch: '1.0.x'
gem 'hanami-router', '~> 1.0', require: false, git: 'https://github.com/hanami/router.git', branch: '1.0.x'

View File

@ -1,20 +1,25 @@
require 'rake'
require 'rake/testtask'
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'rake/testtask'
Rake::TestTask.new do |t|
t.test_files = Dir['test/**/*_test.rb'].reject do |path|
path.include?('isolation')
end
t.pattern = 'test/**/*_test.rb'
t.libs.push 'test'
end
namespace :test do
namespace :spec do
RSpec::Core::RakeTask.new(:unit) do |task|
file_list = FileList['spec/**/*_spec.rb']
file_list = file_list.exclude("spec/{integration,isolation}/**/*_spec.rb")
task.pattern = file_list
end
task :coverage do
ENV['COVERALL'] = 'true'
Rake::Task['test'].invoke
ENV['COVERAGE'] = 'true'
Rake::Task['spec:unit'].invoke
end
end
task default: :test
task default: 'spec:unit'

View File

@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md hanami-controller.gemspec`.split($/)
spec.executables = []
spec.test_files = spec.files.grep(%r{^(test)/})
spec.test_files = spec.files.grep(%r{^(spec)/})
spec.require_paths = ['lib']
spec.required_ruby_version = '>= 2.3.0'
@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'bundler', '~> 1.6'
spec.add_development_dependency 'rack-test', '~> 0.6'
spec.add_development_dependency 'rake', '~> 11'
spec.add_development_dependency 'rspec', '~> 3.5'
end

View File

@ -3,14 +3,30 @@ set -euo pipefail
IFS=$'\n\t'
run_unit_tests() {
bundle exec rake test:coverage
bundle exec rake spec:coverage
}
run_isolation_tests() {
local pwd=$PWD
local root="$pwd/spec/isolation"
for test in $(find $root -name '*_spec.rb')
do
run_isolation_test $test
if [ $? -ne 0 ]; then
local exit_code=$?
echo "Failing test: $test"
exit $exit_code
fi
done
}
run_integration_tests() {
local pwd=$PWD
local root="$pwd/test/isolation"
local root="$pwd/spec/integration"
for test in $(find $root -name '*_test.rb')
for test in $(find $root -name '*_spec.rb')
do
run_test $test
@ -22,15 +38,23 @@ run_integration_tests() {
done
}
run_isolation_test() {
local test=$1
printf "\n\n\nRunning: $test\n"
ruby $test --options spec/isolation/.rspec
}
run_test() {
local test=$1
printf "\n\n\nRunning: $test\n"
ruby -Itest $test
COVERAGE=true bundle exec rspec $test
}
main() {
run_unit_tests &&
run_isolation_tests &&
run_integration_tests
}

View File

@ -0,0 +1,377 @@
require 'hanami/router'
require 'hanami/action/cache'
CacheControlRoutes = Hanami::Router.new do
get '/default', to: 'cache_control#default'
get '/overriding', to: 'cache_control#overriding'
get '/symbol', to: 'cache_control#symbol'
get '/symbols', to: 'cache_control#symbols'
get '/hash', to: 'cache_control#hash'
get '/private-and-public', to: 'cache_control#private_public'
end
ExpiresRoutes = Hanami::Router.new do
get '/default', to: 'expires#default'
get '/overriding', to: 'expires#overriding'
get '/symbol', to: 'expires#symbol'
get '/symbols', to: 'expires#symbols'
get '/hash', to: 'expires#hash'
end
ConditionalGetRoutes = Hanami::Router.new do
get '/etag', to: 'conditional_get#etag'
get '/last-modified', to: 'conditional_get#last_modified'
get '/etag-last-modified', to: 'conditional_get#etag_last_modified'
end
module CacheControl
class Default
include Hanami::Action
include Hanami::Action::Cache
cache_control :public, max_age: 600
def call(params)
end
end
class Overriding
include Hanami::Action
include Hanami::Action::Cache
cache_control :public, max_age: 600
def call(_params)
cache_control :private
end
end
class Symbol
include Hanami::Action
include Hanami::Action::Cache
def call(_params)
cache_control :private
end
end
class Symbols
include Hanami::Action
include Hanami::Action::Cache
def call(_params)
cache_control :private, :no_cache, :no_store
end
end
class Hash
include Hanami::Action
include Hanami::Action::Cache
def call(_params)
cache_control :public, :no_store, max_age: 900, s_maxage: 86_400, min_fresh: 500, max_stale: 700
end
end
class PrivatePublic
include Hanami::Action
include Hanami::Action::Cache
def call(_params)
cache_control :private, :public
end
end
end
module Expires
class Default
include Hanami::Action
include Hanami::Action::Cache
expires 900, :public, :no_cache
def call(params)
end
end
class Overriding
include Hanami::Action
include Hanami::Action::Cache
expires 900, :public, :no_cache
def call(_params)
expires 600, :private
end
end
class Symbol
include Hanami::Action
include Hanami::Action::Cache
def call(_params)
expires 900, :private
end
end
class Symbols
include Hanami::Action
include Hanami::Action::Cache
def call(_params)
expires 900, :private, :no_cache, :no_store
end
end
class Hash
include Hanami::Action
include Hanami::Action::Cache
def call(_params)
expires 900, :public, :no_store, s_maxage: 86_400, min_fresh: 500, max_stale: 700
end
end
end
module ConditionalGet
class Etag
include Hanami::Action
include Hanami::Action::Cache
def call(_params)
fresh etag: 'updated'
end
end
class LastModified
include Hanami::Action
include Hanami::Action::Cache
def call(_params)
fresh last_modified: Time.now
end
end
class EtagLastModified
include Hanami::Action
include Hanami::Action::Cache
def call(_params)
fresh etag: 'updated', last_modified: Time.now
end
end
end
RSpec.describe "HTTP Cache" do
describe "Cache control" do
let(:app) { Rack::MockRequest.new(CacheControlRoutes) }
context "default cache control" do
it "returns default Cache-Control headers" do
response = app.get("/default")
expect(response.headers.fetch("Cache-Control")).to eq("public, max-age=600")
end
context "but some action overrides it" do
it "returns more specific Cache-Control headers" do
response = app.get("/overriding")
expect(response.headers.fetch("Cache-Control")).to eq("private")
end
end
end
it "accepts a Symbol" do
response = app.get("/symbol")
expect(response.headers.fetch("Cache-Control")).to eq("private")
end
it "accepts multiple Symbols" do
response = app.get("/symbols")
expect(response.headers.fetch("Cache-Control")).to eq("private, no-cache, no-store")
end
it "accepts a Hash" do
response = app.get("/hash")
expect(response.headers.fetch("Cache-Control")).to eq("public, no-store, max-age=900, s-maxage=86400, min-fresh=500, max-stale=700")
end
context "private and public directives" do
it "ignores public directive" do
response = app.get("/private-and-public")
expect(response.headers.fetch("Cache-Control")).to eq("private")
end
end
end
describe "Expires" do
let(:app) { Rack::MockRequest.new(ExpiresRoutes) }
context "default cache control" do
it "returns default Cache-Control headers" do
response = app.get("/default")
expect(response.headers.fetch("Expires")).to eq((Time.now + 900).httpdate)
expect(response.headers.fetch("Cache-Control")).to eq("public, no-cache, max-age=900")
end
context "but some action overrides it" do
it "returns more specific Cache-Control headers" do
response = app.get("/overriding")
expect(response.headers.fetch("Expires")).to eq((Time.now + 600).httpdate)
expect(response.headers.fetch("Cache-Control")).to eq("private, max-age=600")
end
end
end
it "accepts a Symbol" do
now = Time.now
expect(Time).to receive(:now).and_return(now)
response = app.get("/symbol")
expect(response.headers.fetch("Expires")).to eq((now + 900).httpdate)
expect(response.headers.fetch("Cache-Control")).to eq("private, max-age=900")
end
it "accepts multiple Symbols" do
now = Time.now
expect(Time).to receive(:now).and_return(now)
response = app.get("/symbols")
expect(response.headers.fetch("Expires")).to eq((now + 900).httpdate)
expect(response.headers.fetch("Cache-Control")).to eq("private, no-cache, no-store, max-age=900")
end
it "accepts a Hash" do
now = Time.now
expect(Time).to receive(:now).and_return(now)
response = app.get("/hash")
expect(response.headers.fetch("Expires")).to eq((now + 900).httpdate)
expect(response.headers.fetch("Cache-Control")).to eq("public, no-store, s-maxage=86400, min-fresh=500, max-stale=700, max-age=900")
end
end
describe "Fresh" do
let(:app) { Rack::MockRequest.new(ConditionalGetRoutes) }
describe "#etag" do
context "when etag matches HTTP_IF_NONE_MATCH header" do
it "halts 304 not modified" do
response = app.get("/etag", "HTTP_IF_NONE_MATCH" => "updated")
expect(response.status).to be(304)
end
it "keeps the same etag header" do
response = app.get("/etag", "HTTP_IF_NONE_MATCH" => "outdated")
expect(response.headers.fetch("ETag")).to eq("updated")
end
end
context "when etag does not match HTTP_IF_NONE_MATCH header" do
it "completes request" do
response = app.get("/etag", "HTTP_IF_NONE_MATCH" => "outdated")
expect(response.status).to be(200)
end
it "returns etag header" do
response = app.get("/etag", "HTTP_IF_NONE_MATCH" => "outdated")
expect(response.headers.fetch("ETag")).to eq("updated")
end
end
end
describe "#last_modified" do
let(:modified_since) { Time.new(2014, 1, 8, 0, 0, 0) }
let(:last_modified) { Time.new(2014, 2, 8, 0, 0, 0) }
context "when last modified is less than or equal to HTTP_IF_MODIFIED_SINCE header" do
before do
expect(Time).to receive(:now).at_least(:once).and_return(modified_since)
end
it "halts 304 not modified" do
response = app.get("/last-modified", "HTTP_IF_MODIFIED_SINCE" => modified_since.httpdate)
expect(response.status).to be(304)
end
it "keeps the same IfModifiedSince header" do
response = app.get("/last-modified", "HTTP_IF_MODIFIED_SINCE" => modified_since.httpdate)
expect(response.headers.fetch("Last-Modified")).to eq(modified_since.httpdate)
end
end
context "when last modified is bigger than HTTP_IF_MODIFIED_SINCE header" do
before do
expect(Time).to receive(:now).at_least(:once).and_return(last_modified)
end
it "completes request" do
response = app.get("/last-modified", "HTTP_IF_MODIFIED_SINCE" => modified_since.httpdate)
expect(response.status).to be(200)
end
it "returns etag header" do
response = app.get("/last-modified", "HTTP_IF_MODIFIED_SINCE" => modified_since.httpdate)
expect(response.headers.fetch("Last-Modified")).to eq(last_modified.httpdate)
end
end
context "when last modified is empty string" do
context "and HTTP_IF_MODIFIED_SINCE empty" do
it "completes request" do
response = app.get("/last-modified", "HTTP_IF_MODIFIED_SINCE" => "")
expect(response.status).to be(200)
end
it "stays the Last-Modified header as time" do
expect(Time).to receive(:now).and_return(modified_since)
response = app.get("/last-modified", "HTTP_IF_MODIFIED_SINCE" => "")
expect(response.headers.fetch("Last-Modified")).to eq(modified_since.httpdate)
end
end
context "and HTTP_IF_MODIFIED_SINCE contain space string" do
it "completes request" do
response = app.get("/last-modified", "HTTP_IF_MODIFIED_SINCE" => " ")
expect(response.status).to be(200)
end
it "stays the Last-Modified header as time" do
expect(Time).to receive(:now).and_return(modified_since)
response = app.get("/last-modified", "HTTP_IF_MODIFIED_SINCE" => " ")
expect(response.headers.fetch("Last-Modified")).to eq(modified_since.httpdate)
end
end
context "and HTTP_IF_NONE_MATCH empty" do
it "completes request" do
response = app.get("/last-modified", "HTTP_IF_NONE_MATCH" => "")
expect(response.status).to be(200)
end
it "doesn't send Last-Modified" do
expect(Time).to receive(:now).and_return(modified_since)
response = app.get("/last-modified", "HTTP_IF_NONE_MATCH" => "")
expect(response.headers).to_not have_key("Last-Modified")
end
end
context "and HTTP_IF_NONE_MATCH contain space string" do
it "completes request" do
response = app.get("/last-modified", "HTTP_IF_NONE_MATCH" => " ")
expect(response.status).to be(200)
end
it "doesn't send Last-Modified" do
expect(Time).to receive(:now).and_return(modified_since)
response = app.get("/last-modified", "HTTP_IF_NONE_MATCH" => " ")
expect(response.headers).to_not have_key("Last-Modified")
end
end
end
end
end
end

View File

@ -0,0 +1,78 @@
RSpec.describe "Framework configuration" do
it "keeps separated copies of the configuration" do
hanami_configuration = Hanami::Controller.configuration
music_configuration = MusicPlayer::Controller.configuration
artists_show_config = MusicPlayer::Controllers::Artists::Show.configuration
expect(hanami_configuration).to_not eq(music_configuration)
expect(hanami_configuration).to_not eq(artists_show_config)
end
it "inheriths configurations at the framework level" do
_, _, body = MusicPlayer::Controllers::Dashboard::Index.new.call({})
expect(body).to eq(["Muzic!"])
end
it "catches exception handled at the framework level" do
code, = MusicPlayer::Controllers::Dashboard::Show.new.call({})
expect(code).to be(400)
end
it "catches exception handled at the action level" do
code, = MusicPlayer::Controllers::Artists::Show.new.call({})
expect(code).to be(404)
end
it "allows standalone actions to inherith framework configuration" do
code, = MusicPlayer::StandaloneAction.new.call({})
expect(code).to be(400)
end
it "allows standalone modulized actions to inherith framework configuration" do
expect(Hanami::Controller.configuration.handled_exceptions).to_not include(App::CustomError)
expect(App::StandaloneAction.configuration.handled_exceptions).to include(App::CustomError)
code, = App::StandaloneAction.new.call({})
expect(code).to be(400)
end
it "allows standalone modulized controllers to inherith framework configuration" do
expect(Hanami::Controller.configuration.handled_exceptions).to_not include(App2::CustomError)
expect(App2::Standalone::Index.configuration.handled_exceptions).to include(App2::CustomError)
code, = App2::Standalone::Index.new.call({})
expect(code).to be(400)
end
it "includes modules from configuration" do
modules = MusicPlayer::Controllers::Artists::Show.included_modules
expect(modules).to include(Hanami::Action::Cookies)
expect(modules).to include(Hanami::Action::Session)
end
it "correctly includes user defined modules" do
code, _, body = MusicPlayer::Controllers::Artists::Index.new.call({})
expect(code).to be(200)
expect(body).to eq(["Luca"])
end
describe "default headers" do
it "if default headers aren't setted only content-type header is returned" do
code, headers, = FullStack::Controllers::Home::Index.new.call({})
expect(code).to be(200)
expect(headers).to eq("Content-Type" => "application/octet-stream; charset=utf-8")
end
it "if default headers are setted, default headers are returned" do
code, headers, = MusicPlayer::Controllers::Artists::Index.new.call({})
expect(code).to be(200)
expect(headers).to eq("Content-Type" => "application/octet-stream; charset=utf-8", "X-Frame-Options" => "DENY")
end
it "default headers overrided in action" do
code, headers, = MusicPlayer::Controllers::Dashboard::Index.new.call({})
expect(code).to be(200)
expect(headers).to eq("Content-Type" => "application/octet-stream; charset=utf-8", "X-Frame-Options" => "ALLOW FROM https://example.org")
end
end
end

View File

@ -0,0 +1,33 @@
RSpec.describe "Framework freeze" do
describe "Hanami::Controller" do
before do
Hanami::Controller.load!
end
after do
Hanami::Controller.unload!
end
it "freezes framework configuration" do
expect(Hanami::Controller.configuration).to be_frozen
end
xit "freezes action configuration" do
expect(CallAction.configuration).to be_frozen
end
end
describe "duplicated framework" do
before do
MusicPlayer::Controller.load!
end
it "freezes framework configuration" do
expect(MusicPlayer::Controller.configuration).to be_frozen
end
xit "freezes action configuration" do
expect(MusicPlayer::Controllers::Artists::Index.configuration).to be_frozen
end
end
end

View File

@ -0,0 +1,91 @@
require "rack/test"
RSpec.describe "Full stack application" do
include Rack::Test::Methods
def app
FullStack::Application.new
end
it "passes action inside the Rack env" do
get "/", {}, "HTTP_ACCEPT" => "text/html"
expect(last_response.body).to include("FullStack::Controllers::Home::Index")
expect(last_response.body).to include(':greeting=>"Hello"')
expect(last_response.body).to include(":format=>:html")
end
it "omits the body if the request is HEAD" do
head "/head", {}, "HTTP_ACCEPT" => "text/html"
expect(last_response.body).to be_empty
expect(last_response.headers).to_not have_key("X-Renderable")
end
it "in case of redirect and invalid params, it passes errors in session and then deletes them" do
post "/books", title: ""
follow_redirect!
expect(last_response.body).to include("FullStack::Controllers::Books::Index")
expect(last_response.body).to include("params: {}")
get "/books"
expect(last_response.body).to include("params: {}")
end
it "uses flash to pass informations" do
get "/poll"
follow_redirect!
expect(last_response.body).to include("FullStack::Controllers::Poll::Step1")
expect(last_response.body).to include("Start the poll")
post "/poll/1", {}
follow_redirect!
expect(last_response.body).to include("FullStack::Controllers::Poll::Step2")
expect(last_response.body).to include("Step 1 completed")
end
it "doesn't return stale informations" do
post "/settings", {}
follow_redirect!
expect(last_response.body).to match(/Hanami::Action::Flash:0x[\d\w]* {:message=>"Saved!"}/)
get "/settings"
expect(last_response.body).to match(/Hanami::Action::Flash:0x[\d\w]* {}/)
end
it "can access params with string symbols or methods" do
patch "/books/1", book: {
title: "Hanami in Action",
author: {
name: "Luca"
}
}
result = JSON.parse(last_response.body, symbolize_names: true)
expect(result).to eq(
symbol_access: "Luca",
valid: true,
errors: {}
)
end
it "validates nested params" do
patch "/books/1", book: {
title: "Hanami in Action"
}
result = JSON.parse(last_response.body, symbolize_names: true)
expect(result[:valid]).to be(false)
expect(result[:errors]).to eq(book: { author: ["is missing"] })
end
it "redirect in before action and call action method is not called" do
get "users/1"
expect(last_response.status).to be(302)
expect(last_response.body).to eq("Found") # This message is 302 status
end
end

View File

@ -0,0 +1,121 @@
require 'rack/test'
HeadRoutes = Hanami::Router.new(namespace: HeadTest) do
get '/', to: 'home#index'
get '/code/:code', to: 'home#code'
get '/override', to: 'home#override'
end
HeadApplication = Rack::Builder.new do
use Rack::Session::Cookie, secret: SecureRandom.hex(16)
run HeadRoutes
end.to_app
RSpec.describe "HTTP HEAD" do
include Rack::Test::Methods
def app
HeadApplication
end
def response
last_response
end
it "doesn't send body and default headers" do
head "/"
expect(response.status).to be(200)
expect(response.body).to eq("")
expect(response.headers.to_a).to_not include(["X-Frame-Options", "DENY"])
end
it "allows to bypass restriction on custom headers" do
get "/override"
expect(response.status).to be(204)
expect(response.body).to eq("")
headers = response.headers.to_a
expect(headers).to include(["Last-Modified", "Fri, 27 Nov 2015 13:32:36 GMT"])
expect(headers).to include(["X-Rate-Limit", "4000"])
expect(headers).to_not include(["X-No-Pass", "true"])
expect(headers).to_not include(["Content-Type", "application/octet-stream; charset=utf-8"])
end
HTTP_TEST_STATUSES_WITHOUT_BODY.each do |code|
describe "with: #{code}" do
it "doesn't send body and default headers" do
get "/code/#{code}"
expect(response.status).to be(code)
expect(response.body).to eq("")
expect(response.headers.to_a).to_not include(["X-Frame-Options", "DENY"])
end
it "sends Allow header" do
get "/code/#{code}"
expect(response.status).to be(code)
expect(response.headers["Allow"]).to eq("GET, HEAD")
end
it "sends Content-Encoding header" do
get "/code/#{code}"
expect(response.status).to be(code)
expect(response.headers["Content-Encoding"]).to eq("identity")
end
it "sends Content-Language header" do
get "/code/#{code}"
expect(response.status).to be(code)
expect(response.headers["Content-Language"]).to eq("en")
end
it "doesn't send Content-Length header" do
get "/code/#{code}"
expect(response.status).to be(code)
expect(response.headers).to_not have_key("Content-Length")
end
it "doesn't send Content-Type header" do
get "/code/#{code}"
expect(response.status).to be(code)
expect(response.headers).to_not have_key("Content-Type")
end
it "sends Content-Location header" do
get "/code/#{code}"
expect(response.status).to be(code)
expect(response.headers["Content-Location"]).to eq("relativeURI")
end
it "sends Content-MD5 header" do
get "/code/#{code}"
expect(response.status).to be(code)
expect(response.headers["Content-MD5"]).to eq("c13367945d5d4c91047b3b50234aa7ab")
end
it "sends Expires header" do
get "/code/#{code}"
expect(response.status).to be(code)
expect(response.headers["Expires"]).to eq("Thu, 01 Dec 1994 16:00:00 GMT")
end
it "sends Last-Modified header" do
get "/code/#{code}"
expect(response.status).to be(code)
expect(response.headers["Last-Modified"]).to eq("Wed, 21 Jan 2015 11:32:10 GMT")
end
end
end
end

View File

@ -0,0 +1,330 @@
require 'hanami/router'
MimeRoutes = Hanami::Router.new do
get '/', to: 'mimes#default'
get '/custom', to: 'mimes#custom'
get '/configuration', to: 'mimes#configuration'
get '/accept', to: 'mimes#accept'
get '/restricted', to: 'mimes#restricted'
get '/latin', to: 'mimes#latin'
get '/nocontent', to: 'mimes#no_content'
get '/response', to: 'mimes#default_response'
get '/overwritten_format', to: 'mimes#override_default_response'
get '/custom_from_accept', to: 'mimes#custom_from_accept'
end
module Mimes
class Default
include Hanami::Action
def call(_params)
self.body = format
end
end
class Configuration
include Hanami::Action
configuration.default_request_format :html
configuration.default_charset 'ISO-8859-1'
def call(_params)
self.body = format
end
end
class Custom
include Hanami::Action
def call(_params)
self.format = :xml
self.body = format
end
end
class Latin
include Hanami::Action
def call(_params)
self.charset = 'latin1'
self.format = :html
self.body = format
end
end
class Accept
include Hanami::Action
def call(_params)
headers['X-AcceptDefault'] = accept?('application/octet-stream').to_s
headers['X-AcceptHtml'] = accept?('text/html').to_s
headers['X-AcceptXml'] = accept?('application/xml').to_s
headers['X-AcceptJson'] = accept?('text/json').to_s
self.body = format
end
end
class CustomFromAccept
include Hanami::Action
configuration.format custom: 'application/custom'
accept :json, :custom
def call(_params)
self.body = format
end
end
class Restricted
include Hanami::Action
configuration.format custom: 'application/custom'
accept :html, :json, :custom
def call(_params)
self.body = format.to_s
end
end
class NoContent
include Hanami::Action
def call(_params)
self.status = 204
end
end
class DefaultResponse
include Hanami::Action
configuration.default_request_format :html
configuration.default_response_format :json
def call(_params)
self.body = configuration.default_request_format
end
end
class OverrideDefaultResponse
include Hanami::Action
configuration.default_response_format :json
def call(_params)
self.format = :xml
end
end
end
RSpec.describe 'MIME Type' do
describe "Content type" do
let(:app) { Rack::MockRequest.new(MimeRoutes) }
it 'fallbacks to the default "Content-Type" header when the request is lacking of this information' do
response = app.get("/")
expect(response.headers["Content-Type"]).to eq("application/octet-stream; charset=utf-8")
expect(response.body).to eq("all")
end
it "fallbacks to the default format and charset, set in the configuration" do
response = app.get("/configuration")
expect(response.headers["Content-Type"]).to eq("text/html; charset=ISO-8859-1")
expect(response.body).to eq("html")
end
it 'returns the specified "Content-Type" header' do
response = app.get("/custom")
expect(response.headers["Content-Type"]).to eq("application/xml; charset=utf-8")
expect(response.body).to eq("xml")
end
it "returns the custom charser header" do
response = app.get("/latin")
expect(response.headers["Content-Type"]).to eq("text/html; charset=latin1")
expect(response.body).to eq("html")
end
it "uses default_response_format if set in the configuration regardless of request format" do
response = app.get("/response")
expect(response.headers["Content-Type"]).to eq("application/json; charset=utf-8")
expect(response.body).to eq("html")
end
it "allows to override default_response_format" do
response = app.get("/overwritten_format")
expect(response.headers["Content-Type"]).to eq("application/xml; charset=utf-8")
end
# FIXME: Review if this test must be in place
xit 'does not produce a "Content-Type" header when the request has a 204 No Content status' do
response = app.get("/nocontent")
expect(response.headers).to_not have_key("Content-Type")
expect(response.body).to eq("")
end
context "when Accept is sent" do
it 'sets "Content-Type" header according to wildcard value' do
response = app.get("/", "HTTP_ACCEPT" => "*/*")
expect(response.headers["Content-Type"]).to eq("application/octet-stream; charset=utf-8")
expect(response.body).to eq("all")
end
it 'sets "Content-Type" header according to exact value' do
response = app.get("/custom_from_accept", "HTTP_ACCEPT" => "application/custom")
expect(response.headers["Content-Type"]).to eq("application/custom; charset=utf-8")
expect(response.body).to eq("custom")
end
it 'sets "Content-Type" header according to weighted value' do
response = app.get("/custom_from_accept", "HTTP_ACCEPT" => "application/custom;q=0.9,application/json;q=0.5")
expect(response.headers["Content-Type"]).to eq("application/custom; charset=utf-8")
expect(response.body).to eq("custom")
end
it 'sets "Content-Type" header according to weighted, unordered value' do
response = app.get("/custom_from_accept", "HTTP_ACCEPT" => "application/custom;q=0.1, application/json;q=0.5")
expect(response.headers["Content-Type"]).to eq("application/json; charset=utf-8")
expect(response.body).to eq("json")
end
it 'sets "Content-Type" header according to exact and weighted value' do
response = app.get("/", "HTTP_ACCEPT" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
expect(response.headers["Content-Type"]).to eq("text/html; charset=utf-8")
expect(response.body).to eq("html")
end
it 'sets "Content-Type" header according to quality scale value' do
response = app.get("/", "HTTP_ACCEPT" => "application/json;q=0.6,application/xml;q=0.9,*/*;q=0.8")
expect(response.headers["Content-Type"]).to eq("application/xml; charset=utf-8")
expect(response.body).to eq("xml")
end
end
end
describe "Accept" do
let(:app) { Rack::MockRequest.new(MimeRoutes) }
let(:response) { app.get("/accept", "HTTP_ACCEPT" => accept) }
context "when Accept is missing" do
let(:accept) { nil }
it "accepts all" do
expect(response.headers["X-AcceptDefault"]).to eq("true")
expect(response.headers["X-AcceptHtml"]).to eq("true")
expect(response.headers["X-AcceptXml"]).to eq("true")
expect(response.headers["X-AcceptJson"]).to eq("true")
expect(response.body).to eq("all")
end
end
context "when Accept is sent" do
context 'when "*/*"' do
let(:accept) { "*/*" }
it "accepts all" do
expect(response.headers["X-AcceptDefault"]).to eq("true")
expect(response.headers["X-AcceptHtml"]).to eq("true")
expect(response.headers["X-AcceptXml"]).to eq("true")
expect(response.headers["X-AcceptJson"]).to eq("true")
expect(response.body).to eq("all")
end
end
context 'when "text/html"' do
let(:accept) { "text/html" }
it "accepts selected mime types" do
expect(response.headers["X-AcceptDefault"]).to eq("false")
expect(response.headers["X-AcceptHtml"]).to eq("true")
expect(response.headers["X-AcceptXml"]).to eq("false")
expect(response.headers["X-AcceptJson"]).to eq("false")
expect(response.body).to eq("html")
end
end
context "when weighted" do
let(:accept) { "text/html,application/xhtml+xml,application/xml;q=0.9" }
it "accepts selected mime types" do
expect(response.headers["X-AcceptDefault"]).to eq("false")
expect(response.headers["X-AcceptHtml"]).to eq("true")
expect(response.headers["X-AcceptXml"]).to eq("true")
expect(response.headers["X-AcceptJson"]).to eq("false")
expect(response.body).to eq("html")
end
end
end
end
describe "Restricted Accept" do
let(:app) { Rack::MockRequest.new(MimeRoutes) }
let(:response) { app.get("/restricted", "HTTP_ACCEPT" => accept) }
context "when Accept is missing" do
let(:accept) { nil }
it "returns the mime type according to the application defined policy" do
expect(response.status).to be(200)
expect(response.body).to eq("all")
end
end
context "when Accept is sent" do
context 'when "*/*"' do
let(:accept) { "*/*" }
it "returns the mime type according to the application defined policy" do
expect(response.status).to be(200)
expect(response.body).to eq("all")
end
end
context "when accepted" do
let(:accept) { "text/html" }
it "accepts selected MIME Types" do
expect(response.status).to be(200)
expect(response.body).to eq("html")
end
end
context "when custom MIME Type" do
let(:accept) { "application/custom" }
it "accepts selected mime types" do
expect(response.status).to be(200)
expect(response.body).to eq("custom")
end
end
context "when not accepted" do
let(:accept) { "application/xml" }
it "accepts selected MIME Types" do
expect(response.status).to be(406)
end
end
context "when weighted" do
context "with an accepted format as first choice" do
let(:accept) { "text/html,application/xhtml+xml,application/xml;q=0.9" }
it "accepts selected mime types" do
expect(response.status).to be(200)
expect(response.body).to eq("html")
end
end
context "with an accepted format as last choice" do
let(:accept) { "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,*/*;q=0.5" }
it "accepts selected mime types" do
expect(response.status).to be(200)
expect(response.body).to eq("html")
end
end
end
end
end
end

View File

@ -1,4 +1,3 @@
require 'test_helper'
require 'hanami/router'
ErrorsRoutes = Hanami::Router.new do
@ -29,7 +28,7 @@ module Errors
class WithoutMessage
include Hanami::Action
def call(params)
def call(_params)
raise AuthException
end
end
@ -37,15 +36,15 @@ module Errors
class WithMessage
include Hanami::Action
def call(params)
raise AuthException.new %q{you're not authorized to see this page!}
def call(_params)
raise AuthException, "you're not authorized to see this page!"
end
end
class WithCustomMessage
include Hanami::Action
def call(params)
def call(_params)
raise CustomAuthException, 'plz go away!!'
end
end
@ -54,8 +53,8 @@ module Errors
include Hanami::Action
handle_exception HandledException => 400
def call(params)
raise HandledException.new
def call(_params)
raise HandledException
end
end
@ -63,16 +62,16 @@ module Errors
include Hanami::Action
handle_exception HandledException => 400
def call(params)
raise HandledExceptionSubclass.new
def call(_params)
raise HandledExceptionSubclass
end
end
class FrameworkManaged
include Hanami::Action
def call(params)
raise FrameworkHandledException.new
def call(_params)
raise FrameworkHandledException
end
end
end
@ -94,74 +93,70 @@ module DisabledErrors
include Hanami::Action
handle_exception HandledException => 400
def call(params)
raise HandledException.new
def call(_params)
raise HandledException
end
end
class FrameworkManaged
include Hanami::Action
def call(params)
raise FrameworkHandledException.new
def call(_params)
raise FrameworkHandledException
end
end
end
Hanami::Controller.unload!
describe 'Reference exception in rack.errors' do
before do
@app = Rack::MockRequest.new(ErrorsRoutes)
RSpec.describe 'Reference exception in "rack.errors"' do
let(:app) { Rack::MockRequest.new(ErrorsRoutes) }
it "adds exception to rack.errors" do
response = app.get("/without_message")
expect(response.errors).to include("AuthException")
end
it 'adds exception to rack.errors' do
response = @app.get('/without_message')
response.errors.must_include "AuthException"
it "adds exception message to rack.errors" do
response = app.get("/with_message")
expect(response.errors).to include("AuthException: you're not authorized to see this page!\n")
end
it 'adds exception message to rack.errors' do
response = @app.get('/with_message')
response.errors.must_include "AuthException: you're not authorized to see this page!\n"
end
it 'uses exception string representation' do
response = @app.get('/with_custom_message')
response.errors.must_include "CustomAuthException: plz go away!! :(\n"
it "uses exception string representation" do
response = app.get("/with_custom_message")
expect(response.errors).to include("CustomAuthException: plz go away!! :(\n")
end
it "doesn't dump exception in rack.errors if it's managed by an action" do
response = @app.get('/action_managed')
response.errors.must_be_empty
response = app.get("/action_managed")
expect(response.errors).to be_empty
end
it "doesn't dump exception in rack.errors if it's managed by an action" do
response = @app.get('/action_managed_subclass')
response.errors.must_be_empty
response = app.get("/action_managed_subclass")
expect(response.errors).to be_empty
end
it "doesn't dump exception in rack.errors if it's managed by the framework" do
response = @app.get('/framework_managed')
response.errors.must_be_empty
response = app.get("/framework_managed")
expect(response.errors).to be_empty
end
describe 'when exception management is disabled' do
before do
@app = Rack::MockRequest.new(DisabledErrorsRoutes)
end
context "when exception management is disabled" do
let(:app) { Rack::MockRequest.new(DisabledErrorsRoutes) }
it "dumps the exception in rack.errors even if it's managed by the action" do
-> {
response = @app.get('/action_managed')
expect do
response = app.get("/action_managed")
response.errors.wont_be_empty
}.must_raise(HandledException)
end.to raise_error(HandledException)
end
it "dumps the exception in rack.errors even if it's managed by the framework" do
-> {
response = @app.get('/framework_managed')
expect do
response = app.get("/framework_managed")
response.errors.wont_be_empty
}.must_raise(FrameworkHandledException)
end.to raise_error(FrameworkHandledException)
end
end
end

View File

@ -0,0 +1,24 @@
RSpec.describe "Exception notifiers integration" do
let(:env) { Hash[] }
it 'reference error in rack.exception' do
action = RackExceptionAction.new
action.call(env)
expect(env['rack.exception']).to be_kind_of(RackExceptionAction::TestException)
end
it "doesn't reference error in rack.exception if it's handled" do
action = HandledRackExceptionAction.new
action.call(env)
expect(env).to_not have_key('rack.exception')
end
it "doesn't reference of an error in rack.exception if it's handled" do
action = HandledRackExceptionSubclassAction.new
action.call(env)
expect(env).to_not have_key('rack.exception')
end
end

View File

@ -0,0 +1,148 @@
require 'hanami/router'
Routes = Hanami::Router.new do
get '/', to: 'root'
get '/team', to: 'about#team'
get '/contacts', to: 'about#contacts'
resource :identity
resources :flowers
resources :painters, only: [:update]
end
RSpec.describe 'Hanami::Router integration' do
let(:app) { Rack::MockRequest.new(Routes) }
it "calls simple action" do
response = app.get("/")
expect(response.status).to be(200)
expect(response.body).to eq("{}")
expect(response.headers["X-Test"]).to eq("test")
end
it "calls a controller's class action" do
response = app.get("/team")
expect(response.status).to be(200)
expect(response.body).to eq("{}")
expect(response.headers["X-Test"]).to eq("test")
end
it "calls a controller's action (with DSL)" do
response = app.get("/contacts")
expect(response.status).to be(200)
expect(response.body).to eq("{}")
end
it "returns a 404 for unknown path" do
response = app.get("/unknown")
expect(response.status).to be(404)
end
context "resource" do
it "calls GET show" do
response = app.get("/identity")
expect(response.status).to be(200)
expect(response.body).to eq("{}")
end
it "calls GET new" do
response = app.get("/identity/new")
expect(response.status).to be(200)
expect(response.body).to eq("{}")
end
it "calls POST create" do
response = app.post("/identity", params: { identity: { avatar: { image: "jodosha.png" } } })
expect(response.status).to be(200)
expect(response.body).to eq(%({:identity=>{:avatar=>{:image=>\"jodosha.png\"}}}))
end
it "calls GET edit" do
response = app.get("/identity/edit")
expect(response.status).to be(200)
expect(response.body).to eq("{}")
end
it "calls PATCH update" do
response = app.request("PATCH", "/identity", params: { identity: { avatar: { image: "jodosha-2x.png" } } })
expect(response.status).to be(200)
expect(response.body).to eq(%({:identity=>{:avatar=>{:image=>\"jodosha-2x.png\"}}}))
end
it "calls DELETE destroy" do
response = app.delete("/identity")
expect(response.status).to be(200)
expect(response.body).to eq("{}")
end
end
context "resources" do
it "calls GET index" do
response = app.get("/flowers")
expect(response.status).to be(200)
expect(response.body).to eq("{}")
end
it "calls GET show" do
response = app.get("/flowers/23")
expect(response.status).to be(200)
expect(response.body).to eq(%({:id=>"23"}))
end
it "calls GET new" do
response = app.get("/flowers/new")
expect(response.status).to be(200)
expect(response.body).to eq("{}")
end
it "calls POST create" do
response = app.post("/flowers", params: { flower: { name: "Sakura" } })
expect(response.status).to be(200)
expect(response.body).to eq(%({:flower=>{:name=>"Sakura"}}))
end
it "calls GET edit" do
response = app.get("/flowers/23/edit")
expect(response.status).to be(200)
expect(response.body).to eq(%({:id=>"23"}))
end
it "calls PATCH update" do
response = app.request("PATCH", "/flowers/23", params: { flower: { name: "Sakura!" } })
expect(response.status).to be(200)
expect(response.body).to eq(%({:flower=>{:name=>"Sakura!"}, :id=>"23"}))
end
it "calls DELETE destroy" do
response = app.delete("/flowers/23")
expect(response.status).to be(200)
expect(response.body).to eq(%({:id=>"23"}))
end
context "with validations" do
it "automatically whitelists params from router" do
response = app.request("PATCH", "/painters/23", params: { painter: { first_name: "Gustav", last_name: "Klimt" } })
expect(response.status).to be(200)
expect(response.body).to eq(%({:painter=>{:first_name=>"Gustav", :last_name=>"Klimt"}, :id=>"23"}))
end
end
end
end

View File

@ -0,0 +1,208 @@
require 'rack/test'
SendFileRoutes = Hanami::Router.new(namespace: SendFileTest) do
get '/files/flow', to: 'files#flow'
get '/files/unsafe_local', to: 'files#unsafe_local'
get '/files/unsafe_public', to: 'files#unsafe_public'
get '/files/unsafe_absolute', to: 'files#unsafe_absolute'
get '/files/unsafe_missing_local', to: 'files#unsafe_missing_local'
get '/files/unsafe_missing_absolute', to: 'files#unsafe_missing_absolute'
get '/files/:id(.:format)', to: 'files#show'
get '/files/(*glob)', to: 'files#glob'
end
SendFileApplication = Rack::Builder.new do
use Rack::Lint
run SendFileRoutes
end.to_app
RSpec.describe "Full stack application" do
include Rack::Test::Methods
def app
SendFileApplication
end
def response
last_response
end
context "send files from anywhere in the system" do
it "responds 200 when a local file exists" do
get "/files/unsafe_local", {}
file = Pathname.new("Gemfile")
expect(response.status).to be(200)
expect(response.headers["Content-Length"].to_i).to eq(file.size)
expect(response.headers["Content-Type"]).to eq("text/plain")
expect(response.body.size).to eq(file.size)
end
it "responds 200 when a relative path file exists" do
get "/files/unsafe_public", {}
file = Pathname.new("spec/support/fixtures/test.txt")
expect(response.status).to be(200)
expect(response.headers["Content-Length"].to_i).to eq(file.size)
expect(response.headers["Content-Type"]).to eq("text/plain")
expect(response.body.size).to eq(file.size)
end
it "responds 200 when an absoute path file exists" do
get "/files/unsafe_absolute", {}
file = Pathname.new("Gemfile")
expect(response.status).to be(200)
expect(response.headers["Content-Length"].to_i).to eq(file.size)
expect(response.headers["Content-Type"]).to eq("text/plain")
expect(response.body.size).to eq(file.size)
end
it "responds 404 when a relative path does not exists" do
get "/files/unsafe_missing_local", {}
body = "Not Found"
expect(response.status).to be(404)
expect(response.headers["Content-Length"].to_i).to eq(body.bytesize)
expect(response.headers["Content-Type"]).to eq("text/plain")
expect(response.body).to eq(body)
end
it "responds 404 when an absolute path does not exists" do
get "/files/unsafe_missing_absolute", {}
body = "Not Found"
expect(response.status).to be(404)
expect(response.headers["Content-Length"].to_i).to eq(body.bytesize)
expect(response.headers["Content-Type"]).to eq("text/plain")
expect(response.body).to eq(body)
end
end
context "when file exists, app responds 200" do
it "sets Content-Type according to file type" do
get "/files/1", {}
file = Pathname.new("spec/support/fixtures/test.txt")
expect(response.status).to be(200)
expect(response.headers["Content-Length"].to_i).to eq(file.size)
expect(response.headers["Content-Type"]).to eq("text/plain")
expect(response.body.size).to eq(file.size)
end
it "sets Content-Type according to file type (ignoring HTTP_ACCEPT)" do
get "/files/2", {}, "HTTP_ACCEPT" => "text/html"
file = Pathname.new("spec/support/fixtures/hanami.png")
expect(response.status).to be(200)
expect(response.headers["Content-Length"].to_i).to eq(file.size)
expect(response.headers["Content-Type"]).to eq("image/png")
expect(response.body.size).to eq(file.size)
end
it "doesn't send file in case of HEAD request" do
head "/files/1", {}
expect(response.status).to be(200)
expect(response.headers).to_not have_key("Content-Length")
expect(response.headers).to_not have_key("Content-Type")
expect(response.body).to be_empty
end
it "doesn't send file outside of public directory" do
get "/files/3", {}
expect(response.status).to be(404)
end
end
context "if file doesn't exist" do
it "responds 404" do
get "/files/100", {}
expect(response.status).to be(404)
expect(response.body).to eq("Not Found")
end
end
context "using conditional glob routes and :format" do
it "serves up json" do
get "/files/500.json", {}
file = Pathname.new("spec/support/fixtures/resource-500.json")
expect(response.status).to be(200)
expect(response.headers["Content-Length"].to_i).to eq(file.size)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(response.body.size).to eq(file.size)
end
it "fails on an unknown format" do
get "/files/500.xml", {}
expect(response.status).to be(406)
end
it "serves up html" do
get "/files/500.html", {}
file = Pathname.new("spec/support/fixtures/resource-500.html")
expect(response.status).to be(200)
expect(response.headers["Content-Length"].to_i).to eq(file.size)
expect(response.headers["Content-Type"]).to eq("text/html; charset=utf-8")
expect(response.body.size).to eq(file.size)
end
it "works without a :format" do
get "/files/500", {}
file = Pathname.new("spec/support/fixtures/resource-500.json")
expect(response.status).to be(200)
expect(response.headers["Content-Length"].to_i).to eq(file.size)
expect(response.headers["Content-Type"]).to eq("application/json")
expect(response.body.size).to eq(file.size)
end
it "returns 400 when I give a bogus id" do
get "/files/not-an-id.json", {}
expect(response.status).to be(400)
end
it "blows up when :format is sent as an :id" do
get "/files/501.json", {}
expect(response.status).to be(404)
end
end
context "Conditional GET request" do
it "shouldn't send file" do
if_modified_since = File.mtime("spec/support/fixtures/test.txt").httpdate
get "/files/1", {}, "HTTP_ACCEPT" => "text/html", "HTTP_IF_MODIFIED_SINCE" => if_modified_since
expect(response.status).to be(304)
expect(response.headers).to_not have_key("Content-Length")
expect(response.headers).to_not have_key("Content-Type")
expect(response.body).to be_empty
end
end
context 'bytes range' do
it "sends ranged contents" do
get '/files/1', {}, 'HTTP_RANGE' => 'bytes=5-13'
expect(response.status).to be(206)
expect(response.headers["Content-Length"]).to eq("9")
expect(response.headers["Content-Range"]).to eq("bytes 5-13/69")
expect(response.body).to eq("Text File")
end
end
it "interrupts the control flow" do
get "/files/flow", {}
expect(response.status).to be(200)
end
end

View File

@ -1,4 +1,3 @@
require 'test_helper'
require 'rack/test'
require 'hanami/router'
@ -18,47 +17,58 @@ StandaloneSessionApplication = Rack::Builder.new do
run StandaloneSession.new
end
describe 'Sessions' do
RSpec.describe "HTTP sessions" do
include Rack::Test::Methods
def app
SessionApplication
end
def response
last_response
end
it "denies access if user isn't loggedin" do
get '/'
last_response.status.must_equal 401
get "/"
expect(response.status).to be(401)
end
it 'grant access after login' do
post '/login'
it "grant access after login" do
post "/login"
follow_redirect!
last_response.status.must_equal 200
last_response.body.must_equal "User ID from session: 23"
expect(response.status).to be(200)
expect(response.body).to eq("User ID from session: 23")
end
it 'logs out' do
post '/login'
it "logs out" do
post "/login"
follow_redirect!
last_response.status.must_equal 200
delete '/logout'
expect(response.status).to be(200)
get '/'
last_response.status.must_equal 401
delete "/logout"
get "/"
expect(response.status).to be(401)
end
end
describe 'Standalone Sessions' do
RSpec.describe "HTTP Standalone Sessions" do
include Rack::Test::Methods
def app
StandaloneSessionApplication
end
it 'sets the session value' do
get '/'
last_response.status.must_equal 200
last_response.headers.fetch('Set-Cookie').must_match(/\Arack\.session/)
def response
last_response
end
it "sets the session value" do
get "/"
expect(response.status).to be(200)
expect(response.headers.fetch("Set-Cookie")).to match(/\Arack\.session/)
end
end

View File

@ -1,7 +1,6 @@
require 'test_helper'
require 'rack/test'
describe 'Sessions with cookies application' do
RSpec.describe "Sessions with cookies application" do
include Rack::Test::Methods
def app
@ -12,14 +11,14 @@ describe 'Sessions with cookies application' do
last_response
end
it 'Set-Cookie with rack.session value is sent only one time' do
get '/', {}, 'HTTP_ACCEPT' => 'text/html'
it "Set-Cookie with rack.session value is sent only one time" do
get "/", {}, "HTTP_ACCEPT" => "text/html"
set_cookie_value = response.headers["Set-Cookie"]
rack_session = /(rack.session=.+);/i.match(set_cookie_value).captures.first.gsub("; path=/", "")
get '/', {}, {'HTTP_ACCEPT' => 'text/html', 'Cookie' => rack_session}
get "/", {}, "HTTP_ACCEPT" => "text/html", "Cookie" => rack_session
response.headers["Set-Cookie"].must_include rack_session
expect(response.headers["Set-Cookie"]).to include(rack_session)
end
end

View File

@ -1,7 +1,6 @@
require 'test_helper'
require 'rack/test'
describe 'Sessions without cookies application' do
RSpec.describe "Sessions without cookies application" do
include Rack::Test::Methods
def app
@ -12,14 +11,14 @@ describe 'Sessions without cookies application' do
last_response
end
it 'Set-Cookie with rack.session value is sent only one time' do
get '/', {}, 'HTTP_ACCEPT' => 'text/html'
it "Set-Cookie with rack.session value is sent only one time" do
get "/", {}, "HTTP_ACCEPT" => "text/html"
set_cookie_value = response.headers["Set-Cookie"]
rack_session = /(rack.session=.+);/i.match(set_cookie_value).captures.first.gsub("; path=/", "")
get '/', {}, {'HTTP_ACCEPT' => 'text/html', 'Cookie' => rack_session}
get "/", {}, "HTTP_ACCEPT" => "text/html", "Cookie" => rack_session
response.headers["Set-Cookie"].must_be_nil
expect(response.headers).to_not have_key("Set-Cookie")
end
end

View File

@ -0,0 +1,67 @@
require 'rack/test'
RSpec.describe 'Rack middleware integration' do
include Rack::Test::Methods
def response
last_response
end
context "when an action mounts a Rack middleware" do
let(:app) { UseActionApplication }
it "uses the specified Rack middleware" do
router = Hanami::Router.new do
get "/", to: "use_action#index"
get "/show", to: "use_action#show"
get "/edit", to: "use_action#edit"
end
UseActionApplication = Rack::Builder.new do
run router
end.to_app
get "/"
expect(response.status).to be(200)
expect(response.headers.fetch("X-Middleware")).to eq("OK")
expect(response.headers).to_not have_key("Y-Middleware")
expect(response.body).to eq("Hello from UseAction::Index")
get "/show"
expect(response.status).to be(200)
expect(response.headers.fetch("Y-Middleware")).to eq("OK")
expect(response.headers).to_not have_key("X-Middleware")
expect(response.body).to eq("Hello from UseAction::Show")
get "/edit"
expect(response.status).to be(200)
expect(response.headers.fetch("Z-Middleware")).to eq("OK")
expect(response.headers).to_not have_key("X-Middleware")
expect(response.headers).to_not have_key("Y-Middleware")
expect(response.body).to eq("Hello from UseAction::Edit")
end
end
context "not an action doesn't mount a Rack middleware" do
let(:app) { NoUseActionApplication }
it "action doens't use a middleware" do
router = Hanami::Router.new do
get "/", to: "no_use_action#index"
end
NoUseActionApplication = Rack::Builder.new do
run router
end.to_app
get "/"
expect(response.status).to be(200)
expect(response.headers).to_not have_key("X-Middleware")
expect(response.body).to eq("Hello from NoUseAction::Index")
end
end
end

1
spec/isolation/.rspec Normal file
View File

@ -0,0 +1 @@
--color

View File

@ -1,26 +1,20 @@
require 'rubygems'
require 'bundler'
Bundler.require(:default)
require_relative '../support/isolation_spec_helper'
require 'minitest/autorun'
$LOAD_PATH.unshift 'lib'
require 'hanami/controller'
describe 'Without validations' do
RSpec.describe 'Without validations' do
it "doesn't load Hanami::Validations" do
assert !defined?(Hanami::Validations), 'Expected Hanami::Validations to NOT be defined'
expect(defined?(Hanami::Validations)).to be(nil)
end
it "doesn't load Hanami::Action::Validatable" do
assert !defined?(Hanami::Action::Validatable), 'Expected Hanami::Action::Validatable to NOT be defined'
expect(defined?(Hanami::Action::Validatable)).to be(nil)
end
it "doesn't load Hanami::Action::Params" do
assert !defined?(Hanami::Action::Params), 'Expected Hanami::Action::Params to NOT be defined'
expect(defined?(Hanami::Action::Params)).to be(nil)
end
it "doesn't have params DSL" do
exception = lambda do
expect do
Class.new do
include Hanami::Action
@ -28,9 +22,7 @@ describe 'Without validations' do
required(:id).filled
end
end
end.must_raise NoMethodError
exception.message.must_match "undefined method `params' for"
end.to raise_error(NoMethodError, /undefined method `params' for/)
end
it "has params that don't respond to .valid?" do
@ -43,7 +35,7 @@ describe 'Without validations' do
end
_, _, body = action.new.call({})
body.must_equal [true, true]
expect(body).to eq([true, true])
end
it "has params that don't respond to .errors" do
@ -56,6 +48,8 @@ describe 'Without validations' do
end
_, _, body = action.new.call({})
body.must_equal [false]
expect(body).to eq([false])
end
end
RSpec::Support::Runner.run

12
spec/spec_helper.rb Normal file
View File

@ -0,0 +1,12 @@
if ENV['COVERALL']
require 'coveralls'
Coveralls.wear!
end
require 'hanami/utils'
$LOAD_PATH.unshift 'lib'
require 'hanami/controller'
require 'hanami/action/cookies'
require 'hanami/action/session'
Hanami::Utils.require!("spec/support")

View File

@ -1,17 +1,3 @@
require 'rubygems'
require 'bundler/setup'
if ENV['COVERALL']
require 'coveralls'
Coveralls.wear!
end
require 'minitest/autorun'
$:.unshift 'lib'
require 'hanami/controller'
require 'hanami/action/cookies'
require 'hanami/action/session'
Hanami::Controller.class_eval do
def self.unload!
self.configuration = configuration.duplicate
@ -19,8 +5,6 @@ Hanami::Controller.class_eval do
end
end
require 'fixtures'
Hanami::Controller::Configuration.class_eval do
def ==(other)
other.kind_of?(self.class) &&
@ -31,3 +15,12 @@ Hanami::Controller::Configuration.class_eval do
public :handled_exceptions
end
if defined?(Hanami::Action::CookieJar)
Hanami::Action::CookieJar.class_eval do
def include?(hash)
key, value = *hash
@cookies[key] == value
end
end
end

View File

@ -1,3 +1,4 @@
require 'json'
require 'digest/md5'
require 'hanami/router'
require 'hanami/utils/escape'
@ -941,10 +942,10 @@ module Glued
class SendFile
include Hanami::Action
include Hanami::Action::Glue
configuration.public_directory "test"
configuration.public_directory "spec/support/fixtures"
def call(params)
send_file "assets/test.txt"
send_file "test.txt"
end
end
end
@ -1099,7 +1100,7 @@ end
module SendFileTest
Controller = Hanami::Controller.duplicate(self) do
handle_exceptions false
public_directory "test"
public_directory "spec/support/fixtures"
end
module Files
@ -1108,16 +1109,16 @@ module SendFileTest
def call(params)
id = params[:id]
# This if statement is only for testing purpose
if id == "1"
send_file Pathname.new('assets/test.txt')
send_file Pathname.new('test.txt')
elsif id == "2"
send_file Pathname.new('assets/hanami.png')
send_file Pathname.new('hanami.png')
elsif id == "3"
send_file Pathname.new('Gemfile')
elsif id == "100"
send_file Pathname.new('assets/unknown.txt')
send_file Pathname.new('unknown.txt')
else
# a more realistic example of globbing ':id(.:format)'
@ -1130,7 +1131,7 @@ module SendFileTest
when 'html'
# in reality we'd render a template here, but as a test fixture, we'll simulate that answer
# we should have also checked #accept? but w/e
self.body = ::File.read(Pathname.new("test/#{@resource.asset_path}.html"))
self.body = ::File.read(Pathname.new("spec/support/fixtures/#{@resource.asset_path}.html"))
self.status = 200
self.format = :html
when 'json', nil
@ -1148,7 +1149,7 @@ module SendFileTest
def repository_dot_find_by_id(id)
return nil unless id =~ /^\d+$/
return Model.new(id.to_i, "assets/resource-#{id}")
return Model.new(id.to_i, "resource-#{id}")
end
end
@ -1164,7 +1165,7 @@ module SendFileTest
include SendFileTest::Action
def call(params)
unsafe_send_file "test/assets/test.txt"
unsafe_send_file "spec/support/fixtures/test.txt"
end
end
@ -1196,7 +1197,7 @@ module SendFileTest
include SendFileTest::Action
def call(params)
send_file Pathname.new('assets/test.txt')
send_file Pathname.new('test.txt')
redirect_to '/'
end
end
@ -1349,7 +1350,7 @@ module FullStack
valid = params.valid?
self.status = 201
self.body = Marshal.dump({
self.body = JSON.generate({
symbol_access: params[:book][:author] && params[:book][:author][:name],
valid: valid,
errors: params.errors.to_h

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 675 B

After

Width:  |  Height:  |  Size: 675 B

View File

@ -0,0 +1,17 @@
require 'rubygems'
require 'bundler'
Bundler.setup(:default, :development)
$LOAD_PATH.unshift 'lib'
require 'hanami/controller'
require_relative './rspec'
module RSpec
module Support
module Runner
def self.run
Core::Runner.autorun
end
end
end
end

25
spec/support/rspec.rb Normal file
View File

@ -0,0 +1,25 @@
require 'rspec'
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.filter_run_when_matching :focus
config.disable_monkey_patching!
config.warnings = true
config.default_formatter = 'doc' if config.files_to_run.one?
config.profile_examples = 10
config.order = :random
Kernel.srand config.seed
end

View File

@ -0,0 +1,59 @@
RSpec.describe Hanami::Action do
describe '.after' do
it 'invokes the method(s) from the given symbol(s) after the action is run' do
action = AfterMethodAction.new
action.call({})
expect(action.egg).to eq('gE!g')
expect(action.logger.join(' ')).to eq('Mrs. Jane Dixit')
end
it 'invokes the given block after the action is run' do
action = AfterBlockAction.new
action.call({})
expect(action.egg).to eq('Coque'.reverse)
end
it 'inherits callbacks from superclass' do
action = SubclassAfterMethodAction.new
action.call({})
expect(action.egg).to eq('gE!g'.upcase)
end
it 'can optionally have params in method signature' do
action = ParamsAfterMethodAction.new
action.call(question: '?')
expect(action.egg).to eq('gE!g?')
end
it 'yields params when the callback is a block' do
action = YieldAfterBlockAction.new
action.call('fortytwo' => '42')
expect(action.meaning_of_life_params.to_h).to eq(fortytwo: '42')
end
describe 'on error' do
it 'stops the callbacks execution and returns an HTTP 500 status' do
action = ErrorAfterMethodAction.new
response = action.call({})
expect(response[0]).to be(500)
expect(action.egg).to be(nil)
end
end
describe 'on handled error' do
it 'stops the callbacks execution and passes the control on exception handling' do
action = HandledErrorAfterMethodAction.new
response = action.call({})
expect(response[0]).to be(404)
expect(action.egg).to be(nil)
end
end
end
end

View File

@ -0,0 +1,46 @@
RSpec.describe Hanami::Action::BaseParams do
let(:action) { Test::Index.new }
describe '#initialize' do
it 'creates params without changing the raw request params' do
env = { 'router.params' => { 'some' => { 'hash' => 'value' } } }
action.call(env)
expect(env['router.params']).to eq('some' => { 'hash' => 'value' })
end
end
describe '#valid?' do
it 'always returns true' do
action.call({})
expect(action.params).to be_valid
end
end
describe '#each' do
it 'iterates through params' do
params = described_class.new(expected = { song: 'Break The Habit' })
actual = {}
params.each do |key, value|
actual[key] = value
end
expect(actual).to eq(expected)
end
end
describe '#get' do
let(:params) { described_class.new(delivery: { address: { city: 'Rome' } }) }
it 'returns value if present' do
expect(params.get(:delivery, :address, :city)).to eq('Rome')
end
it 'returns nil if not present' do
expect(params.get(:delivery, :address, :foo)).to be(nil)
end
it 'is aliased as dig' do
expect(params.dig(:delivery, :address, :city)).to eq('Rome')
end
end
end

View File

@ -0,0 +1,61 @@
RSpec.describe Hanami::Action do
describe '.before' do
it 'invokes the method(s) from the given symbol(s) before the action is run' do
action = BeforeMethodAction.new
action.call({})
expect(action.article).to eq('Bonjour!'.reverse)
expect(action.logger.join(' ')).to eq('Mr. John Doe')
end
it 'invokes the given block before the action is run' do
action = BeforeBlockAction.new
action.call({})
expect(action.article).to eq('Good morning!'.reverse)
end
it 'inherits callbacks from superclass' do
action = SubclassBeforeMethodAction.new
action.call({})
expect(action.article).to eq('Bonjour!'.reverse.upcase)
end
it 'can optionally have params in method signature' do
action = ParamsBeforeMethodAction.new
action.call('bang' => '!')
expect(action.article).to eq('Bonjour!!'.reverse)
expect(action.exposed_params.to_h).to eq(bang: '!')
end
it 'yields params when the callback is a block' do
action = YieldBeforeBlockAction.new
response = action.call('twentythree' => '23')
expect(response[0]).to be(200)
expect(action.yielded_params.to_h).to eq(twentythree: '23')
end
describe 'on error' do
it 'stops the callbacks execution and returns an HTTP 500 status' do
action = ErrorBeforeMethodAction.new
response = action.call({})
expect(response[0]).to be(500)
expect(action.article).to be(nil)
end
end
describe 'on handled error' do
it 'stops the callbacks execution and passes the control on exception handling' do
action = HandledErrorBeforeMethodAction.new
response = action.call({})
expect(response[0]).to be(404)
expect(action.article).to be(nil)
end
end
end
end

View File

@ -0,0 +1,105 @@
RSpec.describe Hanami::Action::Cache::Directives do
describe "#directives" do
context "non value directives" do
it "accepts public symbol" do
subject = described_class.new(:public)
expect(subject.values.size).to eq(1)
end
it "accepts private symbol" do
subject = described_class.new(:private)
expect(subject.values.size).to eq(1)
end
it "accepts no_cache symbol" do
subject = described_class.new(:no_cache)
expect(subject.values.size).to eq(1)
end
it "accepts no_store symbol" do
subject = described_class.new(:no_store)
expect(subject.values.size).to eq(1)
end
it "accepts no_transform symbol" do
subject = described_class.new(:no_transform)
expect(subject.values.size).to eq(1)
end
it "accepts must_revalidate symbol" do
subject = described_class.new(:must_revalidate)
expect(subject.values.size).to eq(1)
end
it "accepts proxy_revalidate symbol" do
subject = described_class.new(:proxy_revalidate)
expect(subject.values.size).to eq(1)
end
it "does not accept weird symbol" do
subject = described_class.new(:weird)
expect(subject.values.size).to eq(0)
end
context "multiple symbols" do
it "creates one directive for each valid symbol" do
subject = described_class.new(:private, :proxy_revalidate)
expect(subject.values.size).to eq(2)
end
end
context "private and public at the same time" do
it "ignores public directive" do
subject = described_class.new(:private, :public)
expect(subject.values.size).to eq(1)
end
it "creates one private directive" do
subject = described_class.new(:private, :public)
expect(subject.values.first.name).to eq(:private)
end
end
end
describe "value directives" do
it "accepts max_age symbol" do
subject = described_class.new(max_age: 600)
expect(subject.values.size).to eq(1)
end
it "accepts s_maxage symbol" do
subject = described_class.new(s_maxage: 600)
expect(subject.values.size).to eq(1)
end
it "accepts min_fresh symbol" do
subject = described_class.new(min_fresh: 600)
expect(subject.values.size).to eq(1)
end
it "accepts max_stale symbol" do
subject = described_class.new(max_stale: 600)
expect(subject.values.size).to eq(1)
end
it "does not accept weird symbol" do
subject = described_class.new(weird: 600)
expect(subject.values.size).to eq(0)
end
context "multiple symbols" do
it "creates one directive for each valid symbol" do
subject = described_class.new(max_age: 600, max_stale: 600)
expect(subject.values.size).to eq(2)
end
end
end
describe "value and non value directives" do
it "creates one directive for each valid symbol" do
subject = described_class.new(:public, max_age: 600, max_stale: 600)
expect(subject.values.size).to eq(3)
end
end
end
end

View File

@ -0,0 +1,8 @@
RSpec.describe Hanami::Action::Cache::NonValueDirective do
describe "#to_str" do
it "returns as http cache format" do
subject = described_class.new(:no_cache)
expect(subject.to_str).to eq("no-cache")
end
end
end

View File

@ -0,0 +1,8 @@
RSpec.describe Hanami::Action::Cache::ValueDirective do
describe "#to_str" do
it "returns as http cache format" do
subject = described_class.new(:max_age, 600)
expect(subject.to_str).to eq("max-age=600")
end
end
end

View File

@ -0,0 +1,75 @@
RSpec.describe Hanami::Action do
describe "#cookies" do
it "gets cookies" do
action = GetCookiesAction.new
_, headers, body = action.call("HTTP_COOKIE" => "foo=bar")
expect(action.send(:cookies)).to include(foo: "bar")
expect(headers).to eq("Content-Type" => "application/octet-stream; charset=utf-8")
expect(body).to eq(["bar"])
end
it "change cookies" do
action = ChangeCookiesAction.new
_, headers, body = action.call("HTTP_COOKIE" => "foo=bar")
expect(action.send(:cookies)).to include(foo: "bar")
expect(headers).to eq("Content-Type" => "application/octet-stream; charset=utf-8", "Set-Cookie" => "foo=baz")
expect(body).to eq(["bar"])
end
it "sets cookies" do
action = SetCookiesAction.new
_, headers, body = action.call({})
expect(body).to eq(["yo"])
expect(headers).to eq("Content-Type" => "application/octet-stream; charset=utf-8", "Set-Cookie" => "foo=yum%21")
end
it "sets cookies with options" do
tomorrow = Time.now + 60 * 60 * 24
action = SetCookiesWithOptionsAction.new(expires: tomorrow)
_, headers, = action.call({})
expect(headers).to eq("Content-Type" => "application/octet-stream; charset=utf-8", "Set-Cookie" => "kukki=yum%21; domain=hanamirb.org; path=/controller; expires=#{tomorrow.gmtime.rfc2822}; secure; HttpOnly")
end
it "removes cookies" do
action = RemoveCookiesAction.new
_, headers, = action.call("HTTP_COOKIE" => "foo=bar;rm=me")
expect(headers).to eq("Content-Type" => "application/octet-stream; charset=utf-8", "Set-Cookie" => "rm=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000")
end
describe "with default cookies" do
it "gets default cookies" do
action = GetDefaultCookiesAction.new
action.class.configuration.cookies(domain: "hanamirb.org", path: "/controller", secure: true, httponly: true)
_, headers, = action.call({})
expect(headers).to eq("Content-Type" => "application/octet-stream; charset=utf-8", "Set-Cookie" => "bar=foo; domain=hanamirb.org; path=/controller; secure; HttpOnly")
end
it "overwritten cookies values are respected" do
action = GetOverwrittenCookiesAction.new
action.class.configuration.cookies(domain: "hanamirb.org", path: "/controller", secure: true, httponly: true)
_, headers, = action.call({})
expect(headers).to eq("Content-Type" => "application/octet-stream; charset=utf-8", "Set-Cookie" => "bar=foo; domain=hanamirb.com; path=/action")
end
end
describe "with max_age option and without expires option" do
it "automatically set expires option" do
now = Time.now
expect(Time).to receive(:now).at_least(2).and_return(now)
action = GetAutomaticallyExpiresCookiesAction.new
_, headers, = action.call({})
max_age = 120
expect(headers["Set-Cookie"]).to include("max-age=#{max_age}")
expect(headers["Set-Cookie"]).to include("expires=#{(Time.now + max_age).gmtime.rfc2822}")
end
end
end
end

View File

@ -1,22 +1,20 @@
require 'test_helper'
describe Hanami::Action::Exposable do
describe '#expose' do
RSpec.describe Hanami::Action do
describe '.expose' do
it 'creates a getter for the given ivar' do
action = ExposeAction.new
response = action.call({})
response[0].must_equal 200
expect(response[0]).to be(200)
action.exposures.fetch(:film).must_equal '400 ASA'
action.exposures.fetch(:time).must_equal nil
expect(action.exposures.fetch(:film)).to eq('400 ASA')
expect(action.exposures.fetch(:time)).to be(nil)
end
describe 'when reserved word is used' do
subject { ExposeReservedWordAction.expose_reserved_word }
it 'should raise an exception' do
->() { subject }.must_raise Hanami::Controller::IllegalExposureError
expect { subject }.to raise_error(Hanami::Controller::IllegalExposureError)
end
end
@ -34,7 +32,7 @@ describe Hanami::Action::Exposable do
subject { action_class.new.exposures }
it 'adds a key to exposures list' do
subject.must_include :flash
expect(subject).to include(:flash)
end
end
end
@ -47,7 +45,7 @@ describe Hanami::Action::Exposable do
action = ExposeReservedWordAction.new
action.call({})
action.exposures.must_include :flash
expect(action.exposures).to include(:flash)
end
end
end

View File

@ -0,0 +1,127 @@
RSpec.describe Hanami::Action do
class FormatController
class Lookup
include Hanami::Action
configuration.handle_exceptions = false
def call(params)
end
end
class Custom
include Hanami::Action
configuration.handle_exceptions = false
def call(params)
self.format = params[:format]
end
end
class Configuration
include Hanami::Action
configuration.default_request_format :jpg
def call(_params)
self.body = format
end
end
end
describe '#format' do
let(:action) { FormatController::Lookup.new }
it 'lookup to #content_type if was not explicitly set (default: application/octet-stream)' do
status, headers, = action.call({})
expect(action.format).to eq(:all)
expect(headers['Content-Type']).to eq('application/octet-stream; charset=utf-8')
expect(status).to be(200)
end
it "accepts 'text/html' and returns :html" do
status, headers, = action.call('HTTP_ACCEPT' => 'text/html')
expect(action.format).to eq(:html)
expect(headers['Content-Type']).to eq('text/html; charset=utf-8')
expect(status).to be(200)
end
it "accepts unknown mime type and returns :all" do
status, headers, = action.call('HTTP_ACCEPT' => 'application/unknown')
expect(action.format).to eq(:all)
expect(headers['Content-Type']).to eq('application/octet-stream; charset=utf-8')
expect(status).to be(200)
end
# Bug
# See https://github.com/hanami/controller/issues/104
it "accepts 'text/html, application/xhtml+xml, image/jxr, */*' and returns :html" do
status, headers, = action.call('HTTP_ACCEPT' => 'text/html, application/xhtml+xml, image/jxr, */*')
expect(action.format).to eq(:html)
expect(headers['Content-Type']).to eq('text/html; charset=utf-8')
expect(status).to be(200)
end
# Bug
# See https://github.com/hanami/controller/issues/167
it "accepts '*/*' and returns configured default format" do
action = FormatController::Configuration.new
status, headers, = action.call('HTTP_ACCEPT' => '*/*')
expect(action.format).to eq(:jpg)
expect(headers['Content-Type']).to eq('image/jpeg; charset=utf-8')
expect(status).to be(200)
end
Hanami::Action::Mime::MIME_TYPES.each do |format, mime_type|
it "accepts '#{mime_type}' and returns :#{format}" do
status, headers, = action.call('HTTP_ACCEPT' => mime_type)
expect(action.format).to eq(format)
expect(headers['Content-Type']).to eq("#{mime_type}; charset=utf-8")
expect(status).to be(200)
end
end
end
describe '#format=' do
let(:action) { FormatController::Custom.new }
it "sets :all and returns 'application/octet-stream'" do
status, headers, = action.call(format: 'all')
expect(action.format).to eq(:all)
expect(headers['Content-Type']).to eq('application/octet-stream; charset=utf-8')
expect(status).to be(200)
end
it "sets nil and raises an error" do
expect { action.call(format: nil) }.to raise_error(TypeError)
end
it "sets '' and raises an error" do
expect { action.call(format: '') }.to raise_error(TypeError)
end
it "sets an unknown format and raises an error" do
begin
action.call(format: :unknown)
rescue => exception
expect(exception).to be_kind_of(Hanami::Controller::UnknownFormatError)
expect(exception.message).to eq("Cannot find a corresponding Mime type for 'unknown'. Please configure it with Hanami::Controller::Configuration#format.")
end
end
Hanami::Action::Mime::MIME_TYPES.each do |format, mime_type|
it "sets #{format} and returns '#{mime_type}'" do
_, headers, = action.call(format: format)
expect(action.format).to eq(format)
expect(headers['Content-Type']).to eq("#{mime_type}; charset=utf-8")
end
end
end
end

View File

@ -1,14 +1,11 @@
require 'test_helper'
describe Hanami::Action::Glue do
RSpec.describe Hanami::Action::Glue do
describe "#renderable?" do
describe "when sending file" do
let(:action) { Glued::SendFile.new }
it "isn't renderable while sending file" do
action.call('REQUEST_METHOD' => 'GET')
action.wont_be :renderable?
expect(action).to_not be_renderable
end
end
end

View File

@ -0,0 +1,9 @@
RSpec.describe Hanami::Action do
describe "#content_type" do
it "exposes MIME type" do
action = CallAction.new
action.call({})
expect(action.content_type).to eq("application/octet-stream")
end
end
end

View File

@ -0,0 +1,448 @@
require 'rack'
RSpec.describe Hanami::Action::Params do
xit 'is frozen'
# This is temporary suspended.
# We need to get the dependency Hanami::Validations, more stable before to enable this back.
#
# it 'is frozen' do
# params = Hanami::Action::Params.new({id: '23'})
# params.must_be :frozen?
# end
describe "#raw" do
let(:params) { Class.new(Hanami::Action::Params) }
context "when this feature isn't enabled" do
let(:action) { ParamsAction.new }
it "raw gets all params" do
File.open('spec/support/fixtures/multipart-upload.png', 'rb') do |upload|
action.call('id' => '1', 'unknown' => '2', 'upload' => upload, '_csrf_token' => '3')
expect(action.params[:id]).to eq('1')
expect(action.params[:unknown]).to eq('2')
expect(FileUtils.cmp(action.params[:upload], upload)).to be(true)
expect(action.params[:_csrf_token]).to eq('3')
expect(action.params.raw.fetch('id')).to eq('1')
expect(action.params.raw.fetch('unknown')).to eq('2')
expect(action.params.raw.fetch('upload')).to eq(upload)
expect(action.params.raw.fetch('_csrf_token')).to eq('3')
end
end
end
context "when this feature is enabled" do
let(:action) { WhitelistedUploadDslAction.new }
it "raw gets all params" do
Tempfile.create('multipart-upload') do |upload|
action.call('id' => '1', 'unknown' => '2', 'upload' => upload, '_csrf_token' => '3')
expect(action.params[:id]).to eq('1')
expect(action.params[:unknown]).to be(nil)
expect(action.params[:upload]).to eq(upload)
expect(action.params[:_csrf_token]).to eq('3')
expect(action.params.raw.fetch('id')).to eq('1')
expect(action.params.raw.fetch('unknown')).to eq('2')
expect(action.params.raw.fetch('upload')).to eq(upload)
expect(action.params.raw.fetch('_csrf_token')).to eq('3')
end
end
end
end
describe "whitelisting" do
let(:params) { Class.new(Hanami::Action::Params) }
context "when this feature isn't enabled" do
let(:action) { ParamsAction.new }
it "creates a Params innerclass" do
expect(defined?(ParamsAction::Params)).to eq('constant')
expect(ParamsAction::Params.ancestors).to include(Hanami::Action::Params)
end
context "in testing mode" do
it "returns all the params as they are" do
# For unit tests in Hanami projects, developers may want to define
# params with symbolized keys.
_, _, body = action.call(a: '1', b: '2', c: '3')
expect(body).to eq([%({:a=>"1", :b=>"2", :c=>"3"})])
end
end
context "in a Rack context" do
it "returns all the params as they are" do
# Rack params are always stringified
response = Rack::MockRequest.new(action).request('PATCH', '?id=23', params: { 'x' => { 'foo' => 'bar' } })
expect(response.body).to match(%({:id=>"23", :x=>{:foo=>"bar"}}))
end
end
context "with Hanami::Router" do
it "returns all the params as they are" do
# Hanami::Router params are always symbolized
_, _, body = action.call('router.params' => { id: '23' })
expect(body).to eq([%({:id=>"23"})])
end
end
end
context "when this feature is enabled" do
context "with an explicit class" do
let(:action) { WhitelistedParamsAction.new }
# For unit tests in Hanami projects, developers may want to define
# params with symbolized keys.
context "in testing mode" do
it "returns only the listed params" do
_, _, body = action.call(id: 23, unknown: 4, article: { foo: 'bar', tags: [:cool] })
expect(body).to eq([%({:id=>23, :article=>{:tags=>[:cool]}})])
end
it "doesn't filter _csrf_token" do
_, _, body = action.call(_csrf_token: 'abc')
expect(body).to eq( [%({:_csrf_token=>"abc"})])
end
end
context "in a Rack context" do
it "returns only the listed params" do
response = Rack::MockRequest.new(action).request('PATCH', "?id=23", params: { x: { foo: 'bar' } })
expect(response.body).to match(%({:id=>"23"}))
end
it "doesn't filter _csrf_token" do
response = Rack::MockRequest.new(action).request('PATCH', "?id=1", params: { _csrf_token: 'def', x: { foo: 'bar' } })
expect(response.body).to match(%(:_csrf_token=>"def", :id=>"1"))
end
end
context "with Hanami::Router" do
it "returns all the params coming from the router, even if NOT whitelisted" do
_, _, body = action.call('router.params' => { id: 23, another: 'x' })
expect(body).to eq([%({:id=>23, :another=>"x"})])
end
end
end
context "with an anoymous class" do
let(:action) { WhitelistedDslAction.new }
it "creates a Params innerclass" do
expect(defined?(WhitelistedDslAction::Params)).to eq('constant')
expect(WhitelistedDslAction::Params.ancestors).to include(Hanami::Action::Params)
end
context "in testing mode" do
it "returns only the listed params" do
_, _, body = action.call(username: 'jodosha', unknown: 'field')
expect(body).to eq([%({:username=>"jodosha"})])
end
end
context "in a Rack context" do
it "returns only the listed params" do
response = Rack::MockRequest.new(action).request('PATCH', "?username=jodosha", params: { x: { foo: 'bar' } })
expect(response.body).to match(%({:username=>"jodosha"}))
end
end
context "with Hanami::Router" do
it "returns all the router params, even if NOT whitelisted" do
_, _, body = action.call('router.params' => { username: 'jodosha', y: 'x' })
expect(body).to eq([%({:username=>"jodosha", :y=>"x"})])
end
end
end
end
end
describe 'validations' do
it "isn't valid with empty params" do
params = TestParams.new({})
expect(params.valid?).to be(false)
expect(params.errors.fetch(:email)).to eq(['is missing'])
expect(params.errors.fetch(:name)).to eq(['is missing'])
expect(params.errors.fetch(:tos)).to eq(['is missing'])
expect(params.errors.fetch(:address)).to eq(['is missing'])
expect(params.error_messages).to eq(['Email is missing', 'Name is missing', 'Tos is missing', 'Age is missing', 'Address is missing'])
end
it "isn't valid with empty nested params" do
params = NestedParams.new(signup: {})
expect(params.valid?).to be(false)
expect(params.errors.fetch(:signup).fetch(:name)).to eq(['is missing'])
expect(params.error_messages).to eq(['Name is missing', 'Age is missing', 'Age must be greater than or equal to 18'])
end
it "is it valid when all the validation criteria are met" do
params = TestParams.new(email: 'test@hanamirb.org',
password: '123456',
password_confirmation: '123456',
name: 'Luca',
tos: '1',
age: '34',
address: {
line_one: '10 High Street',
deep: {
deep_attr: 'blue'
}
})
expect(params.valid?).to be(true)
expect(params.errors).to be_empty
expect(params.error_messages).to be_empty
end
it "has input available through the hash accessor" do
params = TestParams.new(name: 'John', age: '1', address: { line_one: '10 High Street' })
expect(params[:name]).to eq('John')
expect(params[:age]).to be(1)
expect(params[:address][:line_one]).to eq('10 High Street')
end
it "allows nested hash access via symbols" do
params = TestParams.new(name: 'John', address: { line_one: '10 High Street', deep: { deep_attr: 1 } })
expect(params[:name]).to eq('John')
expect(params[:address][:line_one]).to eq('10 High Street')
expect(params[:address][:deep][:deep_attr]).to be(1)
end
end
describe "#get" do
context "with data" do
let(:params) do
TestParams.new(
name: 'John',
address: { line_one: '10 High Street', deep: { deep_attr: 1 } },
array: [{ name: 'Lennon' }, { name: 'Wayne' }]
)
end
it "returns nil for nil argument" do
expect(params.get(nil)).to be(nil)
end
it "returns nil for unknown param" do
expect(params.get(:unknown)).to be(nil)
end
it "allows to read top level param" do
expect(params.get(:name)).to eq('John')
end
it "allows to read nested param" do
expect(params.get(:address, :line_one)).to eq('10 High Street')
end
it "returns nil for uknown nested param" do
expect(params.get(:address, :unknown)).to be(nil)
end
it "allows to read datas under arrays" do
expect(params.get(:array, 0, :name)).to eq('Lennon')
expect(params.get(:array, 1, :name)).to eq('Wayne')
end
end
context "without data" do
let(:params) { TestParams.new({}) }
it "returns nil for nil argument" do
expect(params.get(nil)).to be(nil)
end
it "returns nil for unknown param" do
expect(params.get(:unknown)).to be(nil)
end
it "returns nil for top level param" do
expect(params.get(:name)).to be(nil)
end
it "returns nil for nested param" do
expect(params.get(:address, :line_one)).to be(nil)
end
it "returns nil for uknown nested param" do
expect(params.get(:address, :unknown)).to be(nil)
end
end
end
describe "#to_h" do
let(:params) { TestParams.new(name: 'Jane') }
it "returns a ::Hash" do
expect(params.to_h).to be_kind_of(::Hash)
end
it "returns unfrozen Hash" do
expect(params.to_h).to_not be_frozen
end
it "prevents informations escape"
# it "prevents informations escape" do
# hash = params.to_h
# hash.merge!({name: 'L'})
# params.to_h).to eq((Hash['id' => '23'])
# end
it "handles nested params" do
input = {
'address' => {
'deep' => {
'deep_attr' => 'foo'
}
}
}
expected = {
address: {
deep: {
deep_attr: 'foo'
}
}
}
actual = TestParams.new(input).to_h
expect(actual).to eq(expected)
expect(actual).to be_kind_of(::Hash)
expect(actual[:address]).to be_kind_of(::Hash)
expect(actual[:address][:deep]).to be_kind_of(::Hash)
end
context "when whitelisting" do
# This is bug 113.
it "handles nested params" do
input = {
'name' => 'John',
'age' => 1,
'address' => {
'line_one' => '10 High Street',
'deep' => {
'deep_attr' => 'hello'
}
}
}
expected = {
name: 'John',
age: 1,
address: {
line_one: '10 High Street',
deep: {
deep_attr: 'hello'
}
}
}
actual = TestParams.new(input).to_h
expect(actual).to eq(expected)
expect(actual).to be_kind_of(::Hash)
expect(actual[:address]).to be_kind_of(::Hash)
expect(actual[:address][:deep]).to be_kind_of(::Hash)
end
end
end
describe "#to_hash" do
let(:params) { TestParams.new(name: 'Jane') }
it "returns a ::Hash" do
expect(params.to_hash).to be_kind_of(::Hash)
end
it "returns unfrozen Hash" do
expect(params.to_hash).to_not be_frozen
end
it "prevents informations escape"
# it "prevents informations escape" do
# hash = params.to_hash
# hash.merge!({name: 'L'})
# params.to_hash).to eq((Hash['id' => '23'])
# end
it "handles nested params" do
input = {
'address' => {
'deep' => {
'deep_attr' => 'foo'
}
}
}
expected = {
address: {
deep: {
deep_attr: 'foo'
}
}
}
actual = TestParams.new(input).to_hash
expect(actual).to eq(expected)
expect(actual).to be_kind_of(::Hash)
expect(actual[:address]).to be_kind_of(::Hash)
expect(actual[:address][:deep]).to be_kind_of(::Hash)
end
context "when whitelisting" do
# This is bug 113.
it "handles nested params" do
input = {
'name' => 'John',
'age' => 1,
'address' => {
'line_one' => '10 High Street',
'deep' => {
'deep_attr' => 'hello'
}
}
}
expected = {
name: 'John',
age: 1,
address: {
line_one: '10 High Street',
deep: {
deep_attr: 'hello'
}
}
}
actual = TestParams.new(input).to_hash
expect(actual).to eq(expected)
expect(actual).to be_kind_of(::Hash)
expect(actual[:address]).to be_kind_of(::Hash)
expect(actual[:address][:deep]).to be_kind_of(::Hash)
end
it 'does not stringify values' do
input = { 'name' => 123 }
params = TestParams.new(input)
expect(params[:name]).to be(123)
end
end
end
end

View File

@ -1,15 +1,13 @@
require 'test_helper'
describe Hanami::Action::Rack::File do
RSpec.describe Hanami::Action::Rack::File do
describe "#call" do
it "doesn't mutate given env" do
env = Rack::MockRequest.env_for("/download", method: "GET")
expected = env.dup
file = Hanami::Action::Rack::File.new("/report.pdf", __dir__)
file = described_class.new("/report.pdf", __dir__)
file.call(env)
env.must_equal expected
expect(env).to eq(expected)
end
end
end

View File

@ -0,0 +1,12 @@
RSpec.describe Hanami::Action::Rack do
let(:action) { MethodInspectionAction.new }
%w(GET POST PATCH PUT DELETE TRACE OPTIONS).each do |verb|
it "returns current request method (#{verb})" do
env = Rack::MockRequest.env_for('/', method: verb)
_, _, body = action.call(env)
expect(body).to eq([verb])
end
end
end

View File

@ -0,0 +1,25 @@
RSpec.describe Hanami::Action do
describe "#redirect" do
it "redirects to the given path" do
action = RedirectAction.new
response = action.call({})
expect(response[0]).to be(302)
expect(response[1]).to eq("Location" => "/destination", "Content-Type" => "application/octet-stream; charset=utf-8")
end
it "redirects with custom status code" do
action = StatusRedirectAction.new
response = action.call({})
expect(response[0]).to be(301)
end
# Bug
# See: https://github.com/hanami/hanami/issues/196
it "corces location to a ::String" do
response = SafeStringRedirectAction.new.call({})
expect(response[1]["Location"].class).to eq(::String)
end
end
end

View File

@ -1,79 +1,72 @@
require 'test_helper'
require 'hanami/action/request'
describe Hanami::Action::Request do
def build_request(attributes = {})
url = 'http://example.com/foo?q=bar'
env = Rack::MockRequest.env_for(url, attributes)
Hanami::Action::Request.new(env)
end
RSpec.describe Hanami::Action::Request do
describe '#body' do
it 'exposes the raw body of the request' do
body = build_request(input: 'This is the body').body
content = body.read
content.must_equal('This is the body')
expect(content).to eq('This is the body')
end
end
describe '#script_name' do
it 'gets the script name of a mounted app' do
build_request(script_name: '/app').script_name.must_equal('/app')
expect(build_request(script_name: '/app').script_name).to eq('/app')
end
end
describe '#path_info' do
it 'gets the requested path' do
build_request.path_info.must_equal('/foo')
expect(build_request.path_info).to eq('/foo')
end
end
describe '#request_method' do
it 'gets the request method' do
build_request.request_method.must_equal('GET')
expect(build_request.request_method).to eq('GET')
end
end
describe '#query_string' do
it 'gets the raw query string' do
build_request.query_string.must_equal('q=bar')
expect(build_request.query_string).to eq('q=bar')
end
end
describe '#content_length' do
it 'gets the length of the body' do
build_request(input: '123').content_length.must_equal('3')
expect(build_request(input: '123').content_length).to eq('3')
end
end
describe '#scheme' do
it 'gets the request scheme' do
build_request.scheme.must_equal('http')
expect(build_request.scheme).to eq('http')
end
end
describe '#ssl?' do
it 'answers if ssl is used' do
build_request.ssl?.must_equal false
expect(build_request.ssl?).to be(false)
end
end
describe '#host_with_port' do
it 'gets host and port' do
build_request.host_with_port.must_equal('example.com:80')
expect(build_request.host_with_port).to eq('example.com:80')
end
end
describe '#port' do
it 'gets the port' do
build_request.port.must_equal(80)
expect(build_request.port).to be(80)
end
end
describe '#host' do
it 'gets the host' do
build_request.host.must_equal('example.com')
expect(build_request.host).to eq('example.com')
end
end
@ -81,68 +74,68 @@ describe Hanami::Action::Request do
it 'answers correctly' do
request = build_request
%i(delete? head? options? patch? post? put? trace? xhr?).each do |method|
request.send(method).must_equal(false)
expect(request.send(method)).to be(false)
end
request.get?.must_equal(true)
expect(request.get?).to be(true)
end
end
describe '#referer' do
it 'gets the HTTP_REFERER' do
request = build_request('HTTP_REFERER' => 'http://host.com/path')
request.referer.must_equal('http://host.com/path')
expect(request.referer).to eq('http://host.com/path')
end
end
describe '#user_agent' do
it 'gets the value of HTTP_USER_AGENT' do
request = build_request('HTTP_USER_AGENT' => 'Chrome')
request.user_agent.must_equal('Chrome')
expect(request.user_agent).to eq('Chrome')
end
end
describe '#base_url' do
it 'gets the base url' do
build_request.base_url.must_equal('http://example.com')
expect(build_request.base_url).to eq('http://example.com')
end
end
describe '#url' do
it 'gets the full request url' do
build_request.url.must_equal('http://example.com/foo?q=bar')
expect(build_request.url).to eq('http://example.com/foo?q=bar')
end
end
describe '#path' do
it 'gets the request path' do
build_request.path.must_equal('/foo')
expect(build_request.path).to eq('/foo')
end
end
describe '#fullpath' do
it 'gets the path and query' do
build_request.fullpath.must_equal('/foo?q=bar')
expect(build_request.fullpath).to eq('/foo?q=bar')
end
end
describe '#accept_encoding' do
it 'gets the value and quality of accepted encodings' do
request = build_request('HTTP_ACCEPT_ENCODING' => 'gzip, deflate;q=0.6')
request.accept_encoding.must_equal([['gzip', 1], ['deflate', 0.6]])
expect(request.accept_encoding).to eq([['gzip', 1], ['deflate', 0.6]])
end
end
describe '#accept_language' do
it 'gets the value and quality of accepted languages' do
request = build_request('HTTP_ACCEPT_LANGUAGE' => 'da, en;q=0.6')
request.accept_language.must_equal([['da', 1], ['en', 0.6]])
expect(request.accept_language).to eq([['da', 1], ['en', 0.6]])
end
end
describe '#ip' do
it 'gets the request ip' do
request = build_request('REMOTE_ADDR' => '123.123.123.123')
request.ip.must_equal('123.123.123.123')
expect(request.ip).to eq('123.123.123.123')
end
end
@ -159,10 +152,18 @@ describe Hanami::Action::Request do
[]=
values_at
)
request = Hanami::Action::Request.new({})
request = described_class.new({})
methods.each do |method|
proc { request.send(method) }.must_raise(NotImplementedError)
expect { request.send(method) }.to raise_error(NotImplementedError)
end
end
end
private
def build_request(attributes = {})
url = 'http://example.com/foo?q=bar'
env = Rack::MockRequest.env_for(url, attributes)
described_class.new(env)
end
end

View File

@ -0,0 +1,43 @@
RSpec.describe Hanami::Action do
describe "#session" do
it "captures session from Rack env" do
action = SessionAction.new
action.call("rack.session" => session = { "user_id" => "23" })
expect(action.session).to eq(session)
end
it "returns empty hash when it is missing" do
action = SessionAction.new
action.call({})
expect(action.session).to eq({})
end
it "exposes session" do
action = SessionAction.new
action.call("rack.session" => session = { "foo" => "bar" })
expect(action.exposures[:session]).to eq(session)
end
it "allows value access via symbols" do
action = SessionAction.new
action.call("rack.session" => { "foo" => "bar" })
expect(action.session[:foo]).to eq("bar")
end
end
describe "flash" do
it "exposes flash" do
action = FlashAction.new
action.call({})
flash = action.exposures[:flash]
expect(flash).to be_kind_of(Hanami::Action::Flash)
expect(flash[:error]).to eq("ouch")
end
end
end

View File

@ -0,0 +1,90 @@
RSpec.describe Hanami::Action do
before do
Hanami::Controller.unload!
end
describe ".handle_exception" do
it "handle an exception with the given status" do
response = HandledExceptionAction.new.call({})
expect(response[0]).to be(404)
end
it "returns a 500 if an action isn't handled" do
response = UnhandledExceptionAction.new.call({})
expect(response[0]).to be(500)
end
describe "with global handled exceptions" do
it "handles raised exception" do
response = GlobalHandledExceptionAction.new.call({})
expect(response[0]).to be(400)
end
end
end
describe "#throw" do
HTTP_TEST_STATUSES.each do |code, body|
next if HTTP_TEST_STATUSES_WITHOUT_BODY.include?(code)
it "throws an HTTP status code: #{code}" do
response = ThrowCodeAction.new.call(status: code)
expect(response[0]).to be(code)
expect(response[2]).to eq([body])
end
end
it "throws an HTTP status code with given message" do
response = ThrowCodeAction.new.call(status: 401, message: "Secret Sauce")
expect(response[0]).to be(401)
expect(response[2]).to eq(["Secret Sauce"])
end
it "throws the code as it is, when not recognized" do
response = ThrowCodeAction.new.call(status: 2_131_231)
expect(response[0]).to be(500)
expect(response[2]).to eq(["Internal Server Error"])
end
it "stops execution of before filters (method)" do
response = ThrowBeforeMethodAction.new.call({})
expect(response[0]).to be(401)
expect(response[2]).to eq(["Unauthorized"])
end
it "stops execution of before filters (block)" do
response = ThrowBeforeBlockAction.new.call({})
expect(response[0]).to be(401)
expect(response[2]).to eq(["Unauthorized"])
end
it "stops execution of after filters (method)" do
response = ThrowAfterMethodAction.new.call({})
expect(response[0]).to be(408)
expect(response[2]).to eq(["Request Timeout"])
end
it "stops execution of after filters (block)" do
response = ThrowAfterBlockAction.new.call({})
expect(response[0]).to be(408)
expect(response[2]).to eq(["Request Timeout"])
end
end
describe "using Kernel#throw in an action" do
it "should work" do
response = CatchAndThrowSymbolAction.new.call({})
expect(response[0]).to be(200)
end
end
end

View File

@ -0,0 +1,157 @@
RSpec.describe Hanami::Action do
describe ".configuration" do
after do
CallAction.configuration.reset!
end
it "has the same defaults of Hanami::Controller" do
expected = Hanami::Controller.configuration
actual = CallAction.configuration
expect(actual.handle_exceptions).to eq(expected.handle_exceptions)
end
it "doesn't interfer with other action's configurations" do
CallAction.configuration.handle_exceptions = false
expect(Hanami::Controller.configuration.handle_exceptions).to be(true)
expect(ErrorCallAction.configuration.handle_exceptions).to be(true)
end
end
describe "#call" do
it "calls an action" do
response = CallAction.new.call({})
expect(response[0]).to eq(201)
expect(response[1]).to eq('Content-Type' => 'application/octet-stream; charset=utf-8', 'X-Custom' => 'OK')
expect(response[2]).to eq(['Hi from TestAction!'])
end
context "when exception handling code is enabled" do
it "returns an HTTP 500 status code when an exception is raised" do
response = ErrorCallAction.new.call({})
expect(response[0]).to eq(500)
expect(response[2]).to eq(['Internal Server Error'])
end
it "handles inherited exception with specified method" do
response = ErrorCallFromInheritedErrorClass.new.call({})
expect(response[0]).to eq(501)
expect(response[2]).to eq(['An inherited exception occurred!'])
end
it "handles exception with specified method" do
response = ErrorCallFromInheritedErrorClassStack.new.call({})
expect(response[0]).to eq(501)
expect(response[2]).to eq(['MyCustomError was thrown'])
end
it "handles exception with specified method (symbol)" do
response = ErrorCallWithSymbolMethodNameAsHandlerAction.new.call({})
expect(response[0]).to eq(501)
expect(response[2]).to eq(['Please go away!'])
end
it "handles exception with specified method (string)" do
response = ErrorCallWithStringMethodNameAsHandlerAction.new.call({})
expect(response[0]).to eq(502)
expect(response[2]).to eq(['StandardError'])
end
it "handles exception with specified status code" do
response = ErrorCallWithSpecifiedStatusCodeAction.new.call({})
expect(response[0]).to eq(422)
expect(response[2]).to eq(['Unprocessable Entity'])
end
it "returns a successful response if the code and status aren't set" do
response = ErrorCallWithUnsetStatusResponse.new.call({})
expect(response[0]).to eq(200)
expect(response[2]).to eq([])
end
end
context "when exception handling code is disabled" do
before do
ErrorCallAction.configuration.handle_exceptions = false
end
after do
ErrorCallAction.configuration.reset!
end
it "should raise an actual exception" do
expect { ErrorCallAction.new.call({}) }.to raise_error(RuntimeError)
end
end
end
describe "#request" do
it "gets a Rack-like request object" do
action_class = Class.new do
include Hanami::Action
expose :req
def call(_)
@req = request
end
end
action = action_class.new
env = Rack::MockRequest.env_for('http://example.com/foo')
action.call(env)
request = action.req
expect(request.path).to eq('/foo')
end
end
describe "#parsed_request_body" do
it "exposes the body of the request parsed by router body parsers" do
action_class = Class.new do
include Hanami::Action
expose :request_body
def call(_)
@request_body = parsed_request_body
end
end
action = action_class.new
env = Rack::MockRequest.env_for('http://example.com/foo',
'router.parsed_body' => { 'a' => 'foo' })
action.call(env)
parsed_request_body = action.request_body
expect(parsed_request_body).to eq('a' => 'foo')
end
end
describe "Method visibility" do
let(:action) { VisibilityAction.new }
it "ensures that protected and private methods can be safely invoked by developers" do
status, headers, body = action.call({})
expect(status).to be(201)
expect(headers.fetch('X-Custom')).to eq('OK')
expect(headers.fetch('Y-Custom')).to eq('YO')
expect(body).to eq(['x'])
end
it "has a public errors method" do
expect(action.public_methods).to include(:errors)
end
end
end

View File

@ -0,0 +1,504 @@
RSpec.describe Hanami::Controller::Configuration do
before do
module CustomAction
end
end
let(:configuration) { Hanami::Controller::Configuration.new }
after do
Object.send(:remove_const, :CustomAction)
end
describe 'handle exceptions' do
it 'returns true by default' do
expect(configuration.handle_exceptions).to be(true)
end
it 'allows to set the value with a writer' do
configuration.handle_exceptions = false
expect(configuration.handle_exceptions).to be(false)
end
it 'allows to set the value with a dsl' do
configuration.handle_exceptions(false)
expect(configuration.handle_exceptions).to be(false)
end
it 'ignores nil' do
configuration.handle_exceptions(nil)
expect(configuration.handle_exceptions).to be(true)
end
end
describe 'handled exceptions' do
it 'returns an empty hash by default' do
expect(configuration.handled_exceptions).to eq({})
end
it 'allows to set an exception' do
configuration.handle_exception ArgumentError => 400
expect(configuration.handled_exceptions).to include(ArgumentError)
end
end
describe 'exception_handler' do
describe 'when the given error is unknown' do
it 'returns the default value' do
expect(configuration.exception_handler(Exception)).to be(500)
end
end
describe 'when the given error was registered' do
before do
configuration.handle_exception NotImplementedError => 400
end
it 'returns configured value when an exception instance is given' do
expect(configuration.exception_handler(NotImplementedError.new)).to be(400)
end
end
end
describe 'action_module' do
describe 'when not previously configured' do
it 'returns the default value' do
expect(configuration.action_module).to eq(::Hanami::Action)
end
end
describe 'when previously configured' do
before do
configuration.action_module(CustomAction)
end
it 'returns the value' do
expect(configuration.action_module).to eq(CustomAction)
end
end
end
describe 'modules' do
before do
unless defined?(FakeAction)
class FakeAction
end
end
unless defined?(FakeCallable)
module FakeCallable
def call(_)
[status, {}, ['Callable']]
end
def status
200
end
end
end
unless defined?(FakeStatus)
module FakeStatus
def status
318
end
end
end
end
after do
Object.send(:remove_const, :FakeAction)
Object.send(:remove_const, :FakeCallable)
Object.send(:remove_const, :FakeStatus)
end
describe 'when not previously configured' do
it 'is empty' do
expect(configuration.modules).to be_empty
end
end
describe 'when prepare with no block' do
it 'raises error' do
expect { configuration.prepare }.to raise_error(ArgumentError, 'Please provide a block')
end
end
describe 'when previously configured' do
before do
configuration.prepare do
include FakeCallable
end
end
it 'allows to configure additional modules to include' do
configuration.prepare do
include FakeStatus
end
configuration.modules.each do |mod|
FakeAction.class_eval(&mod)
end
code, _, body = FakeAction.new.call({})
expect(code).to be(318)
expect(body).to eq(['Callable'])
end
end
it 'allows to configure modules to include' do
configuration.prepare do
include FakeCallable
end
configuration.modules.each do |mod|
FakeAction.class_eval(&mod)
end
code, _, body = FakeAction.new.call({})
expect(code).to be(200)
expect(body).to eq(['Callable'])
end
end
describe '#format' do
before do
configuration.format custom: 'custom/format'
BaseObject = Class.new(BasicObject) do
def hash
23
end
end
end
after do
Object.send(:remove_const, :BaseObject)
end
it 'registers the given format' do
expect(configuration.format_for('custom/format')).to eq(:custom)
end
it 'raises an error if the given format cannot be coerced into symbol' do
expect { configuration.format(23 => 'boom') }.to raise_error(TypeError)
end
it 'raises an error if the given mime type cannot be coerced into string' do
expect { configuration.format(boom: BaseObject.new) }.to raise_error(TypeError)
end
end
describe '#mime_types' do
before do
configuration.format custom: 'custom/format'
end
it 'returns all known MIME types' do
all = ["custom/format"]
expect(configuration.mime_types).to eq(all + Hanami::Action::Mime::MIME_TYPES.values)
end
it 'returns correct values even after the value is cached' do
configuration.mime_types
configuration.format electroneering: 'custom/electroneering'
all = ["custom/format", "custom/electroneering"]
expect(configuration.mime_types).to eq(all + Hanami::Action::Mime::MIME_TYPES.values)
end
end
describe '#default_request_format' do
describe "when not previously set" do
it 'returns nil' do
expect(configuration.default_request_format).to be(nil)
end
end
describe "when set" do
before do
configuration.default_request_format :html
end
it 'returns the value' do
expect(configuration.default_request_format).to eq(:html)
end
end
it 'raises an error if the given format cannot be coerced into symbol' do
expect { configuration.default_request_format(23) }.to raise_error(TypeError)
end
end
describe '#default_response_format' do
describe "when not previously set" do
it 'returns nil' do
expect(configuration.default_response_format).to be(nil)
end
end
describe "when set" do
before do
configuration.default_response_format :json
end
it 'returns the value' do
expect(configuration.default_response_format).to eq(:json)
end
end
it 'raises an error if the given format cannot be coerced into symbol' do
expect { configuration.default_response_format(23) }.to raise_error(TypeError)
end
end
describe '#default_charset' do
describe "when not previously set" do
it 'returns nil' do
expect(configuration.default_charset).to be(nil)
end
end
describe "when set" do
before do
configuration.default_charset 'latin1'
end
it 'returns the value' do
expect(configuration.default_charset).to eq('latin1')
end
end
end
describe '#format_for' do
it 'returns a symbol from the given mime type' do
expect(configuration.format_for('*/*')).to eq(:all)
expect(configuration.format_for('application/octet-stream')).to eq(:all)
expect(configuration.format_for('text/html')).to eq(:html)
end
describe 'with custom defined formats' do
before do
configuration.format htm: 'text/html'
end
after do
configuration.reset!
end
it 'returns the custom defined mime type, which takes the precedence over the builtin value' do
expect(configuration.format_for('text/html')).to eq(:htm)
end
end
end
describe '#mime_type_for' do
it 'returns a mime type from the given symbol' do
expect(configuration.mime_type_for(:all)).to eq('application/octet-stream')
expect(configuration.mime_type_for(:html)).to eq('text/html')
end
describe 'with custom defined formats' do
before do
configuration.format htm: 'text/html'
end
after do
configuration.reset!
end
it 'returns the custom defined format, which takes the precedence over the builtin value' do
expect(configuration.mime_type_for(:htm)).to eq('text/html')
end
end
end
describe '#default_headers' do
after do
configuration.reset!
end
describe "when not previously set" do
it 'returns default value' do
expect(configuration.default_headers).to eq({})
end
end
describe "when set" do
let(:headers) { { 'X-Frame-Options' => 'DENY' } }
before do
configuration.default_headers(headers)
end
it 'returns the value' do
expect(configuration.default_headers).to eq(headers)
end
describe "multiple times" do
before do
configuration.default_headers(headers)
configuration.default_headers('X-Foo' => 'BAR')
end
it 'returns the value' do
expect(configuration.default_headers).to eq(
'X-Frame-Options' => 'DENY',
'X-Foo' => 'BAR'
)
end
end
describe "with nil values" do
before do
configuration.default_headers(headers)
configuration.default_headers('X-NIL' => nil)
end
it 'rejects those' do
expect(configuration.default_headers).to eq(headers)
end
end
end
end
describe "#public_directory" do
describe "when not previously set" do
it "returns default value" do
expected = ::File.join(Dir.pwd, 'public')
actual = configuration.public_directory
# NOTE: For Rack compatibility it's important to have a string as public directory
expect(actual).to be_kind_of(String)
expect(actual).to eq(expected)
end
end
describe "when set with relative path" do
before do
configuration.public_directory 'static'
end
it "returns the value" do
expected = ::File.join(Dir.pwd, 'static')
actual = configuration.public_directory
# NOTE: For Rack compatibility it's important to have a string as public directory
expect(actual).to be_kind_of(String)
expect(actual).to eq(expected)
end
end
describe "when set with absolute path" do
before do
configuration.public_directory ::File.join(Dir.pwd, 'absolute')
end
it "returns the value" do
expected = ::File.join(Dir.pwd, 'absolute')
actual = configuration.public_directory
# NOTE: For Rack compatibility it's important to have a string as public directory
expect(actual).to be_kind_of(String)
expect(actual).to eq(expected)
end
end
end
describe 'duplicate' do
before do
configuration.reset!
configuration.prepare { include Kernel }
configuration.format custom: 'custom/format'
configuration.default_request_format :html
configuration.default_response_format :html
configuration.default_charset 'latin1'
configuration.default_headers({ 'X-Frame-Options' => 'DENY' })
configuration.public_directory 'static'
end
let(:config) { configuration.duplicate }
it 'returns a copy of the configuration' do
expect(config.handle_exceptions).to eq(configuration.handle_exceptions)
expect(config.handled_exceptions).to eq(configuration.handled_exceptions)
expect(config.action_module).to eq(configuration.action_module)
expect(config.modules).to eq(configuration.modules)
expect(config.send(:formats)).to eq(configuration.send(:formats))
expect(config.mime_types).to eq(configuration.mime_types)
expect(config.default_request_format).to eq(configuration.default_request_format)
expect(config.default_response_format).to eq(configuration.default_response_format)
expect(config.default_charset).to eq(configuration.default_charset)
expect(config.default_headers).to eq(configuration.default_headers)
expect(config.public_directory).to eq(configuration.public_directory)
end
it "doesn't affect the original configuration" do
config.handle_exceptions = false
config.handle_exception ArgumentError => 400
config.action_module CustomAction
config.prepare { include Comparable }
config.format another: 'another/format'
config.default_request_format :json
config.default_response_format :json
config.default_charset 'utf-8'
config.default_headers({ 'X-Frame-Options' => 'ALLOW ALL' })
config.public_directory 'pub'
expect(config.handle_exceptions).to be(false)
expect(config.handled_exceptions).to eq(ArgumentError => 400)
expect(config.action_module).to eq(CustomAction)
expect(config.modules.size).to be(2)
expect(config.format_for('another/format')).to eq(:another)
expect(config.mime_types).to include('another/format')
expect(config.default_request_format).to eq(:json)
expect(config.default_response_format).to eq(:json)
expect(config.default_charset).to eq('utf-8')
expect(config.default_headers).to eq('X-Frame-Options' => 'ALLOW ALL')
expect(config.public_directory).to eq(::File.join(Dir.pwd, 'pub'))
expect(configuration.handle_exceptions).to be(true)
expect(configuration.handled_exceptions).to eq({})
expect(configuration.action_module).to eq(::Hanami::Action)
expect(configuration.modules.size).to be(1)
expect(configuration.format_for('another/format')).to be(nil)
expect(configuration.mime_types).to_not include('another/format')
expect(configuration.default_request_format).to eq(:html)
expect(configuration.default_response_format).to eq(:html)
expect(configuration.default_charset).to eq('latin1')
expect(configuration.default_headers).to eq('X-Frame-Options' => 'DENY')
expect(configuration.public_directory).to eq(::File.join(Dir.pwd, 'static'))
end
end
describe 'reset!' do
before do
configuration.handle_exceptions = false
configuration.handle_exception ArgumentError => 400
configuration.action_module CustomAction
configuration.modules { include Kernel }
configuration.format another: 'another/format'
configuration.default_request_format :another
configuration.default_response_format :another
configuration.default_charset 'kor-1'
configuration.default_headers({ 'X-Frame-Options' => 'ALLOW DENY' })
configuration.public_directory 'files'
configuration.reset!
end
it 'resets to the defaults' do
expect(configuration.handle_exceptions).to be(true)
expect(configuration.handled_exceptions).to eq({})
expect(configuration.action_module).to eq(::Hanami::Action)
expect(configuration.modules).to eq([])
expect(configuration.send(:formats)).to eq(Hanami::Controller::Configuration::DEFAULT_FORMATS)
expect(configuration.mime_types).to eq(Hanami::Action::Mime::MIME_TYPES.values)
expect(configuration.default_request_format).to be(nil)
expect(configuration.default_response_format).to be(nil)
expect(configuration.default_charset).to be(nil)
expect(configuration.default_headers).to eq({})
expect(configuration.public_directory).to eq(::File.join(Dir.pwd, 'public'))
end
end
end

View File

@ -0,0 +1,5 @@
RSpec.describe Hanami::Controller::Error do
it 'inherits from ::StandardError' do
expect(described_class.superclass).to eq(StandardError)
end
end

View File

@ -0,0 +1,5 @@
RSpec.describe Hanami::Controller::UnknownFormatError do
it 'inheriths from Hanami::Controller::Error' do
expect(Hanami::Controller::UnknownFormatError.superclass).to eq(Hanami::Controller::Error)
end
end

View File

@ -0,0 +1,5 @@
RSpec.describe "Hanami::Controller::VERSION" do
it "returns current version" do
expect(Hanami::Controller::VERSION).to eq("1.0.0")
end
end

View File

@ -1,7 +1,5 @@
require 'test_helper'
describe Hanami::Controller do
describe '.configuration' do
RSpec.describe Hanami::Controller do
describe ".configuration" do
before do
Hanami::Controller.unload!
@ -14,23 +12,23 @@ describe Hanami::Controller do
Object.send(:remove_const, :ConfigurationAction)
end
it 'exposes class configuration' do
Hanami::Controller.configuration.must_be_kind_of(Hanami::Controller::Configuration)
it "exposes class configuration" do
expect(Hanami::Controller.configuration).to be_kind_of(Hanami::Controller::Configuration)
end
it 'handles exceptions by default' do
Hanami::Controller.configuration.handle_exceptions.must_equal(true)
it "handles exceptions by default" do
expect(Hanami::Controller.configuration.handle_exceptions).to be(true)
end
it 'inheriths the configuration from the framework' do
it "inheriths the configuration from the framework" do
expected = Hanami::Controller.configuration
actual = ConfigurationAction.configuration
actual.must_equal(expected)
expect(actual).to eq(expected)
end
end
describe '.configure' do
describe ".configure" do
before do
Hanami::Controller.unload!
end
@ -39,17 +37,17 @@ describe Hanami::Controller do
Hanami::Controller.unload!
end
it 'allows to configure the framework' do
it "allows to configure the framework" do
Hanami::Controller.class_eval do
configure do
handle_exceptions false
end
end
Hanami::Controller.configuration.handle_exceptions.must_equal(false)
expect(Hanami::Controller.configuration.handle_exceptions).to be(false)
end
it 'allows to override one value' do
it "allows to override one value" do
Hanami::Controller.class_eval do
configure do
handle_exception ArgumentError => 400
@ -60,11 +58,11 @@ describe Hanami::Controller do
end
end
Hanami::Controller.configuration.handled_exceptions.must_include(ArgumentError)
expect(Hanami::Controller.configuration.handled_exceptions).to include(ArgumentError)
end
end
describe '.duplicate' do
describe ".duplicate" do
before do
Hanami::Controller.configure { handle_exception ArgumentError => 400 }
@ -73,7 +71,7 @@ describe Hanami::Controller do
end
module DuplicatedCustom
Controller = Hanami::Controller.duplicate(self, 'Controllerz')
Controller = Hanami::Controller.duplicate(self, "Controllerz")
end
module DuplicatedWithoutNamespace
@ -97,39 +95,39 @@ describe Hanami::Controller do
Object.send(:remove_const, :DuplicatedConfigure)
end
it 'duplicates the configuration of the framework' do
it "duplicates the configuration of the framework" do
actual = Duplicated::Controller.configuration
expected = Hanami::Controller.configuration
actual.handled_exceptions.must_equal expected.handled_exceptions
expect(actual.handled_exceptions).to eq(expected.handled_exceptions)
end
it 'duplicates a namespace for controllers' do
assert defined?(Duplicated::Controllers), 'Duplicated::Controllers expected'
it "duplicates a namespace for controllers" do
expect(defined?(Duplicated::Controllers)).to eq("constant")
end
it 'generates a custom namespace for controllers' do
assert defined?(DuplicatedCustom::Controllerz), 'DuplicatedCustom::Controllerz expected'
it "generates a custom namespace for controllers" do
expect(defined?(DuplicatedCustom::Controllerz)).to eq("constant")
end
it 'does not create a custom namespace for controllers' do
assert !defined?(DuplicatedWithoutNamespace::Controllers), "DuplicatedWithoutNamespace::Controllers wasn't expected"
it "does not create a custom namespace for controllers" do
expect(defined?(DuplicatedWithoutNamespace::Controllers)).to be(nil)
end
it 'duplicates Action' do
assert defined?(Duplicated::Action), 'Duplicated::Action expected'
it "duplicates Action" do
expect(defined?(Duplicated::Action)).to eq("constant")
end
it 'sets action_module' do
it "sets action_module" do
configuration = Duplicated::Controller.configuration
configuration.action_module.must_equal Duplicated::Action
expect(configuration.action_module).to eq(Duplicated::Action)
end
it 'optionally accepts a block to configure the duplicated module' do
it "optionally accepts a block to configure the duplicated module" do
configuration = DuplicatedConfigure::Controller.configuration
configuration.handled_exceptions.wont_include(ArgumentError)
configuration.handled_exceptions.must_include(StandardError)
expect(configuration.handled_exceptions).to_not include(ArgumentError)
expect(configuration.handled_exceptions).to include(StandardError)
end
end
end

View File

@ -1,50 +0,0 @@
require 'test_helper'
describe Hanami::Action::BaseParams do
before do
@action = Test::Index.new
end
describe '#initialize' do
it 'creates params without changing the raw request params' do
env = { 'router.params' => { 'some' => { 'hash' => 'value' } } }
@action.call(env)
env['router.params'].must_equal({ 'some' => { 'hash' => 'value' } })
end
end
describe '#valid?' do
it 'always returns true' do
@action.call({})
@action.params.must_be :valid?
end
end
describe '#each' do
it 'iterates through params' do
params = Hanami::Action::BaseParams.new(expected = { song: 'Break The Habit' })
actual = Hash[]
params.each do |key, value|
actual[key] = value
end
actual.must_equal(expected)
end
end
describe '#get' do
let(:params) { Hanami::Action::BaseParams.new(delivery: { address: { city: 'Rome' } }) }
it 'returns value if present' do
params.get(:delivery, :address, :city).must_equal 'Rome'
end
it 'returns nil if not present' do
params.get(:delivery, :address, :foo).must_equal nil
end
it 'is aliased as dig' do
params.dig(:delivery, :address, :city).must_equal 'Rome'
end
end
end

View File

@ -1,121 +0,0 @@
require 'test_helper'
describe Hanami::Action do
describe '#before' do
it 'invokes the method(s) from the given symbol(s) before the action is run' do
action = BeforeMethodAction.new
action.call({})
action.article.must_equal 'Bonjour!'.reverse
action.logger.join(' ').must_equal 'Mr. John Doe'
end
it 'invokes the given block before the action is run' do
action = BeforeBlockAction.new
action.call({})
action.article.must_equal 'Good morning!'.reverse
end
it 'inherits callbacks from superclass' do
action = SubclassBeforeMethodAction.new
action.call({})
action.article.must_equal 'Bonjour!'.reverse.upcase
end
it 'can optionally have params in method signature' do
action = ParamsBeforeMethodAction.new
action.call(params = {'bang' => '!'})
action.article.must_equal 'Bonjour!!'.reverse
action.exposed_params.to_h.must_equal({bang: '!'})
end
it 'yields params when the callback is a block' do
action = YieldBeforeBlockAction.new
response = action.call(params = { 'twentythree' => '23' })
response[0].must_equal 200
action.yielded_params.to_h.must_equal({twentythree: '23'})
end
describe 'on error' do
it 'stops the callbacks execution and returns an HTTP 500 status' do
action = ErrorBeforeMethodAction.new
response = action.call({})
response[0].must_equal 500
action.article.must_be_nil
end
end
describe 'on handled error' do
it 'stops the callbacks execution and passes the control on exception handling' do
action = HandledErrorBeforeMethodAction.new
response = action.call({})
response[0].must_equal 404
action.article.must_be_nil
end
end
end
describe '#after' do
it 'invokes the method(s) from the given symbol(s) after the action is run' do
action = AfterMethodAction.new
action.call({})
action.egg.must_equal 'gE!g'
action.logger.join(' ').must_equal 'Mrs. Jane Dixit'
end
it 'invokes the given block after the action is run' do
action = AfterBlockAction.new
action.call({})
action.egg.must_equal 'Coque'.reverse
end
it 'inherits callbacks from superclass' do
action = SubclassAfterMethodAction.new
action.call({})
action.egg.must_equal 'gE!g'.upcase
end
it 'can optionally have params in method signature' do
action = ParamsAfterMethodAction.new
action.call(question: '?')
action.egg.must_equal 'gE!g?'
end
it 'yields params when the callback is a block' do
action = YieldAfterBlockAction.new
action.call(params = { 'fortytwo' => '42' })
action.meaning_of_life_params.to_h.must_equal(fortytwo: '42')
end
describe 'on error' do
it 'stops the callbacks execution and returns an HTTP 500 status' do
action = ErrorAfterMethodAction.new
response = action.call({})
response[0].must_equal 500
action.egg.must_be_nil
end
end
describe 'on handled error' do
it 'stops the callbacks execution and passes the control on exception handling' do
action = HandledErrorAfterMethodAction.new
response = action.call({})
response[0].must_equal 404
action.egg.must_be_nil
end
end
end
end

View File

@ -1,133 +0,0 @@
require 'test_helper'
describe Hanami::Action do
class FormatController
class Lookup
include Hanami::Action
configuration.handle_exceptions = false
def call(params)
end
end
class Custom
include Hanami::Action
configuration.handle_exceptions = false
def call(params)
self.format = params[:format]
end
end
class Configuration
include Hanami::Action
configuration.default_request_format :jpg
def call(params)
self.body = format
end
end
end
describe '#format' do
before do
@action = FormatController::Lookup.new
end
it 'lookup to #content_type if was not explicitly set (default: application/octet-stream)' do
status, headers, _ = @action.call({})
@action.format.must_equal :all
headers['Content-Type'].must_equal 'application/octet-stream; charset=utf-8'
status.must_equal 200
end
it "accepts 'text/html' and returns :html" do
status, headers, _ = @action.call({ 'HTTP_ACCEPT' => 'text/html' })
@action.format.must_equal :html
headers['Content-Type'].must_equal 'text/html; charset=utf-8'
status.must_equal 200
end
it "accepts unknown mime type and returns :all" do
status, headers, _ = @action.call({ 'HTTP_ACCEPT' => 'application/unknown' })
@action.format.must_equal :all
headers['Content-Type'].must_equal 'application/octet-stream; charset=utf-8'
status.must_equal 200
end
# Bug
# See https://github.com/hanami/controller/issues/104
it "accepts 'text/html, application/xhtml+xml, image/jxr, */*' and returns :html" do
status, headers, _ = @action.call({ 'HTTP_ACCEPT' => 'text/html, application/xhtml+xml, image/jxr, */*' })
@action.format.must_equal :html
headers['Content-Type'].must_equal 'text/html; charset=utf-8'
status.must_equal 200
end
# Bug
# See https://github.com/hanami/controller/issues/167
it "accepts '*/*' and returns configured default format" do
action = FormatController::Configuration.new
status, headers, _ = action.call({ 'HTTP_ACCEPT' => '*/*' })
action.format.must_equal :jpg
headers['Content-Type'].must_equal 'image/jpeg; charset=utf-8'
status.must_equal 200
end
Hanami::Action::Mime::MIME_TYPES.each do |format, mime_type|
it "accepts '#{ mime_type }' and returns :#{ format }" do
status, headers, _ = @action.call({ 'HTTP_ACCEPT' => mime_type })
@action.format.must_equal format
headers['Content-Type'].must_equal "#{mime_type}; charset=utf-8"
status.must_equal 200
end
end
end
describe '#format=' do
before do
@action = FormatController::Custom.new
end
it "sets :all and returns 'application/octet-stream'" do
status, headers, _ = @action.call({ format: 'all' })
@action.format.must_equal :all
headers['Content-Type'].must_equal 'application/octet-stream; charset=utf-8'
status.must_equal 200
end
it "sets nil and raises an error" do
-> { @action.call({ format: nil }) }.must_raise TypeError
end
it "sets '' and raises an error" do
-> { @action.call({ format: '' }) }.must_raise TypeError
end
it "sets an unknown format and raises an error" do
begin
@action.call({ format: :unknown })
rescue => e
e.must_be_kind_of(Hanami::Controller::UnknownFormatError)
e.message.must_equal "Cannot find a corresponding Mime type for 'unknown'. Please configure it with Hanami::Controller::Configuration#format."
end
end
Hanami::Action::Mime::MIME_TYPES.each do |format, mime_type|
it "sets #{ format } and returns '#{ mime_type }'" do
_, headers, _ = @action.call({ format: format })
@action.format.must_equal format
headers['Content-Type'].must_equal "#{mime_type}; charset=utf-8"
end
end
end
end

View File

@ -1,470 +0,0 @@
require 'test_helper'
require 'rack'
describe Hanami::Action::Params do
it 'is frozen'
# This is temporary suspended.
# We need to get the dependency Hanami::Validations, more stable before to enable this back.
#
# it 'is frozen' do
# params = Hanami::Action::Params.new({id: '23'})
# params.must_be :frozen?
# end
describe 'raw params' do
before do
@params = Class.new(Hanami::Action::Params)
end
describe "when this feature isn't enabled" do
before do
@action = ParamsAction.new
end
it 'raw gets all params' do
File.open('test/assets/multipart-upload.png', 'rb') do |upload|
@action.call('id' => '1', 'unknown' => '2', 'upload' => upload, '_csrf_token' => '3')
@action.params[:id].must_equal '1'
@action.params[:unknown].must_equal '2'
FileUtils.cmp(@action.params[:upload], upload).must_equal true
@action.params[:_csrf_token].must_equal '3'
@action.params.raw.fetch('id').must_equal '1'
@action.params.raw.fetch('unknown').must_equal '2'
@action.params.raw.fetch('upload').must_equal upload
@action.params.raw.fetch('_csrf_token').must_equal '3'
end
end
end
describe 'when this feature is enabled' do
before do
@action = WhitelistedUploadDslAction.new
end
it 'raw gets all params' do
Tempfile.create('multipart-upload') do |upload|
@action.call('id' => '1', 'unknown' => '2', 'upload' => upload, '_csrf_token' => '3')
@action.params[:id].must_equal '1'
@action.params[:unknown].must_equal nil
@action.params[:upload].must_equal upload
@action.params[:_csrf_token].must_equal '3'
@action.params.raw.fetch('id').must_equal '1'
@action.params.raw.fetch('unknown').must_equal '2'
@action.params.raw.fetch('upload').must_equal upload
@action.params.raw.fetch('_csrf_token').must_equal '3'
end
end
end
end
describe 'whitelisting' do
before do
@params = Class.new(Hanami::Action::Params)
end
describe "when this feature isn't enabled" do
before do
@action = ParamsAction.new
end
it 'creates a Params innerclass' do
assert defined?(ParamsAction::Params),
'expected ParamsAction::Params to be defined'
assert ParamsAction::Params.ancestors.include?(Hanami::Action::Params),
'expected ParamsAction::Params to be a Hanami::Action::Params subclass'
end
describe 'in testing mode' do
it 'returns all the params as they are' do
# For unit tests in Hanami projects, developers may want to define
# params with symbolized keys.
_, _, body = @action.call(a: '1', b: '2', c: '3')
body.must_equal [%({:a=>"1", :b=>"2", :c=>"3"})]
end
end
describe 'in a Rack context' do
it 'returns all the params as they are' do
# Rack params are always stringified
response = Rack::MockRequest.new(@action).request('PATCH', '?id=23', params: { 'x' => { 'foo' => 'bar' } })
response.body.must_match %({:id=>"23", :x=>{:foo=>"bar"}})
end
end
describe 'with Hanami::Router' do
it 'returns all the params as they are' do
# Hanami::Router params are always symbolized
_, _, body = @action.call('router.params' => { id: '23' })
body.must_equal [%({:id=>"23"})]
end
end
end
describe 'when this feature is enabled' do
describe 'with an explicit class' do
before do
@action = WhitelistedParamsAction.new
end
# For unit tests in Hanami projects, developers may want to define
# params with symbolized keys.
describe 'in testing mode' do
it 'returns only the listed params' do
_, _, body = @action.call(id: 23, unknown: 4, article: { foo: 'bar', tags: [:cool] })
body.must_equal [%({:id=>23, :article=>{:tags=>[:cool]}})]
end
it "doesn't filter _csrf_token" do
_, _, body = @action.call(_csrf_token: 'abc')
body.must_equal [%({:_csrf_token=>"abc"})]
end
end
describe "in a Rack context" do
it 'returns only the listed params' do
response = Rack::MockRequest.new(@action).request('PATCH', "?id=23", params: { x: { foo: 'bar' } })
response.body.must_match %({:id=>"23"})
end
it "doesn't filter _csrf_token" do
response = Rack::MockRequest.new(@action).request('PATCH', "?id=1", params: { _csrf_token: 'def', x: { foo: 'bar' } })
response.body.must_match %(:_csrf_token=>"def", :id=>"1")
end
end
describe "with Hanami::Router" do
it 'returns all the params coming from the router, even if NOT whitelisted' do
_, _, body = @action.call({ 'router.params' => {id: 23, another: 'x'}})
body.must_equal [%({:id=>23, :another=>"x"})]
end
end
end
describe "with an anoymous class" do
before do
@action = WhitelistedDslAction.new
end
it 'creates a Params innerclass' do
assert defined?(WhitelistedDslAction::Params),
"expected WhitelistedDslAction::Params to be defined"
assert WhitelistedDslAction::Params.ancestors.include?(Hanami::Action::Params),
"expected WhitelistedDslAction::Params to be a Hanami::Action::Params subclass"
end
describe "in testing mode" do
it 'returns only the listed params' do
_, _, body = @action.call({username: 'jodosha', unknown: 'field'})
body.must_equal [%({:username=>"jodosha"})]
end
end
describe "in a Rack context" do
it 'returns only the listed params' do
response = Rack::MockRequest.new(@action).request('PATCH', "?username=jodosha", params: { x: { foo: 'bar' } })
response.body.must_match %({:username=>"jodosha"})
end
end
describe "with Hanami::Router" do
it 'returns all the router params, even if NOT whitelisted' do
_, _, body = @action.call({ 'router.params' => {username: 'jodosha', y: 'x'}})
body.must_equal [%({:username=>"jodosha", :y=>"x"})]
end
end
end
end
end
describe 'validations' do
it "isn't valid with empty params" do
params = TestParams.new({})
params.valid?.must_equal false
params.errors.fetch(:email).must_equal ['is missing']
params.errors.fetch(:name).must_equal ['is missing']
params.errors.fetch(:tos).must_equal ['is missing']
params.errors.fetch(:address).must_equal ['is missing']
params.error_messages.must_equal ['Email is missing', 'Name is missing', 'Tos is missing', 'Age is missing', 'Address is missing']
end
it "isn't valid with empty nested params" do
params = NestedParams.new(signup: {})
params.valid?.must_equal false
params.errors.fetch(:signup).fetch(:name).must_equal ['is missing']
params.error_messages.must_equal ['Name is missing', 'Age is missing', 'Age must be greater than or equal to 18']
end
it "is it valid when all the validation criteria are met" do
params = TestParams.new(email: 'test@hanamirb.org',
password: '123456',
password_confirmation: '123456',
name: 'Luca',
tos: '1',
age: '34',
address: {
line_one: '10 High Street',
deep: {
deep_attr: 'blue'
}
}
)
params.valid?.must_equal true
params.errors.must_be_empty
params.error_messages.must_be_empty
end
it "has input available through the hash accessor" do
params = TestParams.new(name: 'John', age: '1', address: { line_one: '10 High Street' })
params[:name].must_equal('John')
params[:age].must_equal(1)
params[:address][:line_one].must_equal('10 High Street')
end
it "allows nested hash access via symbols" do
params = TestParams.new(name: 'John', address: { line_one: '10 High Street', deep: { deep_attr: 1 } })
params[:name].must_equal 'John'
params[:address][:line_one].must_equal '10 High Street'
params[:address][:deep][:deep_attr].must_equal 1
end
end
describe '#get' do
describe 'with data' do
before do
@params = TestParams.new(
name: 'John',
address: { line_one: '10 High Street', deep: { deep_attr: 1 } },
array: [{ name: 'Lennon' }, { name: 'Wayne' }]
)
end
it 'returns nil for nil argument' do
@params.get(nil).must_be_nil
end
it 'returns nil for unknown param' do
@params.get(:unknown).must_be_nil
end
it 'allows to read top level param' do
@params.get(:name).must_equal 'John'
end
it 'allows to read nested param' do
@params.get(:address, :line_one).must_equal '10 High Street'
end
it 'returns nil for uknown nested param' do
@params.get(:address, :unknown).must_be_nil
end
it 'allows to read datas under arrays' do
@params.get(:array, 0, :name).must_equal 'Lennon'
@params.get(:array, 1, :name).must_equal 'Wayne'
end
end
describe 'without data' do
before do
@params = TestParams.new({})
end
it 'returns nil for nil argument' do
@params.get(nil).must_be_nil
end
it 'returns nil for unknown param' do
@params.get(:unknown).must_be_nil
end
it 'returns nil for top level param' do
@params.get(:name).must_be_nil
end
it 'returns nil for nested param' do
@params.get(:address, :line_one).must_be_nil
end
it 'returns nil for uknown nested param' do
@params.get(:address, :unknown).must_be_nil
end
end
end
describe '#to_h' do
let(:params) { TestParams.new(name: 'Jane') }
it "returns a ::Hash" do
params.to_h.must_be_kind_of ::Hash
end
it "returns unfrozen Hash" do
params.to_h.wont_be :frozen?
end
it "prevents informations escape"
# it "prevents informations escape" do
# hash = params.to_h
# hash.merge!({name: 'L'})
# params.to_h.must_equal(Hash['id' => '23'])
# end
it 'handles nested params' do
input = {
'address' => {
'deep' => {
'deep_attr' => 'foo'
}
}
}
expected = {
address: {
deep: {
deep_attr: 'foo'
}
}
}
actual = TestParams.new(input).to_h
actual.must_equal(expected)
actual.must_be_kind_of(::Hash)
actual[:address].must_be_kind_of(::Hash)
actual[:address][:deep].must_be_kind_of(::Hash)
end
describe 'when whitelisting' do
# This is bug 113.
it 'handles nested params' do
input = {
'name' => 'John',
'age' => 1,
'address' => {
'line_one' => '10 High Street',
'deep' => {
'deep_attr' => 'hello'
}
}
}
expected = {
name: 'John',
age: 1,
address: {
line_one: '10 High Street',
deep: {
deep_attr: 'hello'
}
}
}
actual = TestParams.new(input).to_h
actual.must_equal(expected)
actual.must_be_kind_of(::Hash)
actual[:address].must_be_kind_of(::Hash)
actual[:address][:deep].must_be_kind_of(::Hash)
end
end
end
describe '#to_hash' do
let(:params) { TestParams.new(name: 'Jane') }
it "returns a ::Hash" do
params.to_hash.must_be_kind_of ::Hash
end
it "returns unfrozen Hash" do
params.to_hash.wont_be :frozen?
end
it "prevents informations escape"
# it "prevents informations escape" do
# hash = params.to_hash
# hash.merge!({name: 'L'})
# params.to_hash.must_equal(Hash['id' => '23'])
# end
it 'handles nested params' do
input = {
'address' => {
'deep' => {
'deep_attr' => 'foo'
}
}
}
expected = {
address: {
deep: {
deep_attr: 'foo'
}
}
}
actual = TestParams.new(input).to_hash
actual.must_equal(expected)
actual.must_be_kind_of(::Hash)
actual[:address].must_be_kind_of(::Hash)
actual[:address][:deep].must_be_kind_of(::Hash)
end
describe 'when whitelisting' do
# This is bug 113.
it 'handles nested params' do
input = {
'name' => 'John',
'age' => 1,
'address' => {
'line_one' => '10 High Street',
'deep' => {
'deep_attr' => 'hello'
}
}
}
expected = {
name: 'John',
age: 1,
address: {
line_one: '10 High Street',
deep: {
deep_attr: 'hello'
}
}
}
actual = TestParams.new(input).to_hash
actual.must_equal(expected)
actual.must_be_kind_of(::Hash)
actual[:address].must_be_kind_of(::Hash)
actual[:address][:deep].must_be_kind_of(::Hash)
end
it 'does not stringify values' do
input = { 'name' => 123 }
params = TestParams.new(input)
params[:name].must_equal(123)
end
end
end
end

View File

@ -1,142 +0,0 @@
require 'test_helper'
describe Hanami::Action do
describe '.configuration' do
after do
CallAction.configuration.reset!
end
it 'has the same defaults of Hanami::Controller' do
expected = Hanami::Controller.configuration
actual = CallAction.configuration
actual.handle_exceptions.must_equal(expected.handle_exceptions)
end
it "doesn't interfer with other action's configurations" do
CallAction.configuration.handle_exceptions = false
Hanami::Controller.configuration.handle_exceptions.must_equal(true)
ErrorCallAction.configuration.handle_exceptions.must_equal(true)
end
end
describe '#call' do
it 'calls an action' do
response = CallAction.new.call({})
response[0].must_equal 201
response[1].must_equal({'Content-Type' => 'application/octet-stream; charset=utf-8', 'X-Custom' => 'OK'})
response[2].must_equal ['Hi from TestAction!']
end
describe 'when exception handling code is enabled' do
it 'returns an HTTP 500 status code when an exception is raised' do
response = ErrorCallAction.new.call({})
response[0].must_equal 500
response[2].must_equal ['Internal Server Error']
end
it 'handles inherited exception with specified method' do
response = ErrorCallFromInheritedErrorClass.new.call({})
response[0].must_equal 501
response[2].must_equal ['An inherited exception occurred!']
end
it 'handles exception with specified method' do
response = ErrorCallFromInheritedErrorClassStack.new.call({})
response[0].must_equal 501
response[2].must_equal ['MyCustomError was thrown']
end
it 'handles exception with specified method (symbol)' do
response = ErrorCallWithSymbolMethodNameAsHandlerAction.new.call({})
response[0].must_equal 501
response[2].must_equal ['Please go away!']
end
it 'handles exception with specified method (string)' do
response = ErrorCallWithStringMethodNameAsHandlerAction.new.call({})
response[0].must_equal 502
response[2].must_equal ['StandardError']
end
it 'handles exception with specified status code' do
response = ErrorCallWithSpecifiedStatusCodeAction.new.call({})
response[0].must_equal 422
response[2].must_equal ['Unprocessable Entity']
end
it "returns a successful response if the code and status aren't set" do
response = ErrorCallWithUnsetStatusResponse.new.call({})
response[0].must_equal 200
response[2].must_equal []
end
end
describe 'when exception handling code is disabled' do
before do
ErrorCallAction.configuration.handle_exceptions = false
end
after do
ErrorCallAction.configuration.reset!
end
it 'should raise an actual exception' do
proc {
ErrorCallAction.new.call({})
}.must_raise RuntimeError
end
end
end
describe '#request' do
it 'gets a Rack-like request object' do
action_class = Class.new do
include Hanami::Action
expose :req
def call(params)
@req = request
end
end
action = action_class.new
env = Rack::MockRequest.env_for('http://example.com/foo')
action.call(env)
request = action.req
request.path.must_equal('/foo')
end
end
describe '#parsed_request_body' do
it 'exposes the body of the request parsed by router body parsers' do
action_class = Class.new do
include Hanami::Action
expose :request_body
def call(params)
@request_body = parsed_request_body
end
end
action = action_class.new
env = Rack::MockRequest.env_for('http://example.com/foo',
'router.parsed_body' => { 'a' => 'foo' })
action.call(env)
parsed_request_body = action.request_body
parsed_request_body.must_equal({ 'a' => 'foo' })
end
end
end

View File

@ -1,501 +0,0 @@
require 'test_helper'
describe Hanami::Controller::Configuration do
before do
module CustomAction
end
@configuration = Hanami::Controller::Configuration.new
end
after do
Object.send(:remove_const, :CustomAction)
end
describe 'handle exceptions' do
it 'returns true by default' do
@configuration.handle_exceptions.must_equal(true)
end
it 'allows to set the value with a writer' do
@configuration.handle_exceptions = false
@configuration.handle_exceptions.must_equal(false)
end
it 'allows to set the value with a dsl' do
@configuration.handle_exceptions(false)
@configuration.handle_exceptions.must_equal(false)
end
it 'ignores nil' do
@configuration.handle_exceptions(nil)
@configuration.handle_exceptions.must_equal(true)
end
end
describe 'handled exceptions' do
it 'returns an empty hash by default' do
@configuration.handled_exceptions.must_equal({})
end
it 'allows to set an exception' do
@configuration.handle_exception ArgumentError => 400
@configuration.handled_exceptions.must_include(ArgumentError)
end
end
describe 'exception_handler' do
describe 'when the given error is unknown' do
it 'returns the default value' do
@configuration.exception_handler(Exception).must_equal 500
end
end
describe 'when the given error was registered' do
before do
@configuration.handle_exception NotImplementedError => 400
end
it 'returns configured value when an exception instance is given' do
@configuration.exception_handler(NotImplementedError.new).must_equal 400
end
end
end
describe 'action_module' do
describe 'when not previously configured' do
it 'returns the default value' do
@configuration.action_module.must_equal(::Hanami::Action)
end
end
describe 'when previously configured' do
before do
@configuration.action_module(CustomAction)
end
it 'returns the value' do
@configuration.action_module.must_equal(CustomAction)
end
end
end
describe 'modules' do
before do
class FakeAction
end unless defined?(FakeAction)
module FakeCallable
def call(params)
[status, {}, ['Callable']]
end
def status
200
end
end unless defined?(FakeCallable)
module FakeStatus
def status
318
end
end unless defined?(FakeStatus)
end
after do
Object.send(:remove_const, :FakeAction)
Object.send(:remove_const, :FakeCallable)
Object.send(:remove_const, :FakeStatus)
end
describe 'when not previously configured' do
it 'is empty' do
@configuration.modules.must_be_empty
end
end
describe 'when prepare with no block' do
it 'raises error' do
exception = -> { @configuration.prepare }.must_raise(ArgumentError)
exception.message.must_equal 'Please provide a block'
end
end
describe 'when previously configured' do
before do
@configuration.prepare do
include FakeCallable
end
end
it 'allows to configure additional modules to include' do
@configuration.prepare do
include FakeStatus
end
@configuration.modules.each do |mod|
FakeAction.class_eval(&mod)
end
code, _, body = FakeAction.new.call({})
code.must_equal 318
body.must_equal ['Callable']
end
end
it 'allows to configure modules to include' do
@configuration.prepare do
include FakeCallable
end
@configuration.modules.each do |mod|
FakeAction.class_eval(&mod)
end
code, _, body = FakeAction.new.call({})
code.must_equal 200
body.must_equal ['Callable']
end
end
describe '#format' do
before do
@configuration.format custom: 'custom/format'
BaseObject = Class.new(BasicObject) do
def hash
23
end
end
end
after do
Object.send(:remove_const, :BaseObject)
end
it 'registers the given format' do
@configuration.format_for('custom/format').must_equal :custom
end
it 'raises an error if the given format cannot be coerced into symbol' do
-> { @configuration.format(23 => 'boom') }.must_raise TypeError
end
it 'raises an error if the given mime type cannot be coerced into string' do
-> { @configuration.format(boom: BaseObject.new) }.must_raise TypeError
end
end
describe '#mime_types' do
before do
@configuration.format custom: 'custom/format'
end
it 'returns all known MIME types' do
all = ["custom/format"]
@configuration.mime_types.must_equal(all + Hanami::Action::Mime::MIME_TYPES.values)
end
it 'returns correct values even after the value is cached' do
@configuration.mime_types
@configuration.format electroneering: 'custom/electroneering'
all = ["custom/format", "custom/electroneering"]
@configuration.mime_types.must_equal(all + Hanami::Action::Mime::MIME_TYPES.values)
end
end
describe '#default_request_format' do
describe "when not previously set" do
it 'returns nil' do
@configuration.default_request_format.must_be_nil
end
end
describe "when set" do
before do
@configuration.default_request_format :html
end
it 'returns the value' do
@configuration.default_request_format.must_equal :html
end
end
it 'raises an error if the given format cannot be coerced into symbol' do
-> { @configuration.default_request_format(23) }.must_raise TypeError
end
end
describe '#default_response_format' do
describe "when not previously set" do
it 'returns nil' do
@configuration.default_response_format.must_be_nil
end
end
describe "when set" do
before do
@configuration.default_response_format :json
end
it 'returns the value' do
@configuration.default_response_format.must_equal :json
end
end
it 'raises an error if the given format cannot be coerced into symbol' do
-> { @configuration.default_response_format(23) }.must_raise TypeError
end
end
describe '#default_charset' do
describe "when not previously set" do
it 'returns nil' do
@configuration.default_charset.must_be_nil
end
end
describe "when set" do
before do
@configuration.default_charset 'latin1'
end
it 'returns the value' do
@configuration.default_charset.must_equal 'latin1'
end
end
end
describe '#format_for' do
it 'returns a symbol from the given mime type' do
@configuration.format_for('*/*').must_equal :all
@configuration.format_for('application/octet-stream').must_equal :all
@configuration.format_for('text/html').must_equal :html
end
describe 'with custom defined formats' do
before do
@configuration.format htm: 'text/html'
end
after do
@configuration.reset!
end
it 'returns the custom defined mime type, which takes the precedence over the builtin value' do
@configuration.format_for('text/html').must_equal :htm
end
end
end
describe '#mime_type_for' do
it 'returns a mime type from the given symbol' do
@configuration.mime_type_for(:all).must_equal 'application/octet-stream'
@configuration.mime_type_for(:html).must_equal 'text/html'
end
describe 'with custom defined formats' do
before do
@configuration.format htm: 'text/html'
end
after do
@configuration.reset!
end
it 'returns the custom defined format, which takes the precedence over the builtin value' do
@configuration.mime_type_for(:htm).must_equal 'text/html'
end
end
end
describe '#default_headers' do
after do
@configuration.reset!
end
describe "when not previously set" do
it 'returns default value' do
@configuration.default_headers.must_equal({})
end
end
describe "when set" do
let(:headers) { {'X-Frame-Options' => 'DENY'} }
before do
@configuration.default_headers(headers)
end
it 'returns the value' do
@configuration.default_headers.must_equal headers
end
describe "multiple times" do
before do
@configuration.default_headers(headers)
@configuration.default_headers('X-Foo' => 'BAR')
end
it 'returns the value' do
@configuration.default_headers.must_equal({
'X-Frame-Options' => 'DENY',
'X-Foo' => 'BAR'
})
end
end
describe "with nil values" do
before do
@configuration.default_headers(headers)
@configuration.default_headers('X-NIL' => nil)
end
it 'rejects those' do
@configuration.default_headers.must_equal headers
end
end
end
end
describe "#public_directory" do
describe "when not previously set" do
it "returns default value" do
expected = ::File.join(Dir.pwd, 'public')
actual = @configuration.public_directory
# NOTE: For Rack compatibility it's important to have a string as public directory
actual.must_be_kind_of(String)
actual.must_equal(expected)
end
end
describe "when set with relative path" do
before do
@configuration.public_directory 'static'
end
it "returns the value" do
expected = ::File.join(Dir.pwd, 'static')
actual = @configuration.public_directory
# NOTE: For Rack compatibility it's important to have a string as public directory
actual.must_be_kind_of(String)
actual.must_equal(expected)
end
end
describe "when set with absolute path" do
before do
@configuration.public_directory ::File.join(Dir.pwd, 'absolute')
end
it "returns the value" do
expected = ::File.join(Dir.pwd, 'absolute')
actual = @configuration.public_directory
# NOTE: For Rack compatibility it's important to have a string as public directory
actual.must_be_kind_of(String)
actual.must_equal(expected)
end
end
end
describe 'duplicate' do
before do
@configuration.reset!
@configuration.prepare { include Kernel }
@configuration.format custom: 'custom/format'
@configuration.default_request_format :html
@configuration.default_response_format :html
@configuration.default_charset 'latin1'
@configuration.default_headers({ 'X-Frame-Options' => 'DENY' })
@configuration.public_directory 'static'
@config = @configuration.duplicate
end
it 'returns a copy of the configuration' do
@config.handle_exceptions.must_equal @configuration.handle_exceptions
@config.handled_exceptions.must_equal @configuration.handled_exceptions
@config.action_module.must_equal @configuration.action_module
@config.modules.must_equal @configuration.modules
@config.send(:formats).must_equal @configuration.send(:formats)
@config.mime_types.must_equal @configuration.mime_types
@config.default_request_format.must_equal @configuration.default_request_format
@config.default_response_format.must_equal @configuration.default_response_format
@config.default_charset.must_equal @configuration.default_charset
@config.default_headers.must_equal @configuration.default_headers
@config.public_directory.must_equal @configuration.public_directory
end
it "doesn't affect the original configuration" do
@config.handle_exceptions = false
@config.handle_exception ArgumentError => 400
@config.action_module CustomAction
@config.prepare { include Comparable }
@config.format another: 'another/format'
@config.default_request_format :json
@config.default_response_format :json
@config.default_charset 'utf-8'
@config.default_headers({ 'X-Frame-Options' => 'ALLOW ALL' })
@config.public_directory 'pub'
@config.handle_exceptions.must_equal false
@config.handled_exceptions.must_equal Hash[ArgumentError => 400]
@config.action_module.must_equal CustomAction
@config.modules.size.must_equal 2
@config.format_for('another/format').must_equal :another
@config.mime_types.must_include 'another/format'
@config.default_request_format.must_equal :json
@config.default_response_format.must_equal :json
@config.default_charset.must_equal 'utf-8'
@config.default_headers.must_equal ({ 'X-Frame-Options' => 'ALLOW ALL' })
@config.public_directory.must_equal ::File.join(Dir.pwd, 'pub')
@configuration.handle_exceptions.must_equal true
@configuration.handled_exceptions.must_equal Hash[]
@configuration.action_module.must_equal ::Hanami::Action
@configuration.modules.size.must_equal 1
@configuration.format_for('another/format').must_be_nil
@configuration.mime_types.wont_include 'another/format'
@configuration.default_request_format.must_equal :html
@configuration.default_response_format.must_equal :html
@configuration.default_charset.must_equal 'latin1'
@configuration.default_headers.must_equal ({ 'X-Frame-Options' => 'DENY' })
@configuration.public_directory.must_equal ::File.join(Dir.pwd, 'static')
end
end
describe 'reset!' do
before do
@configuration.handle_exceptions = false
@configuration.handle_exception ArgumentError => 400
@configuration.action_module CustomAction
@configuration.modules { include Kernel }
@configuration.format another: 'another/format'
@configuration.default_request_format :another
@configuration.default_response_format :another
@configuration.default_charset 'kor-1'
@configuration.default_headers({ 'X-Frame-Options' => 'ALLOW DENY' })
@configuration.public_directory 'files'
@configuration.reset!
end
it 'resets to the defaults' do
@configuration.handle_exceptions.must_equal(true)
@configuration.handled_exceptions.must_equal({})
@configuration.action_module.must_equal(::Hanami::Action)
@configuration.modules.must_equal([])
@configuration.send(:formats).must_equal(Hanami::Controller::Configuration::DEFAULT_FORMATS)
@configuration.mime_types.must_equal(Hanami::Action::Mime::MIME_TYPES.values)
@configuration.default_request_format.must_be_nil
@configuration.default_response_format.must_be_nil
@configuration.default_charset.must_be_nil
@configuration.default_headers.must_equal({})
@configuration.public_directory.must_equal(::File.join(Dir.pwd, 'public'))
end
end
end

View File

@ -1,87 +0,0 @@
require 'test_helper'
Hanami::Action::CookieJar.class_eval do
def include?(hash)
key, value = *hash
@cookies[key] == value
end
end
describe Hanami::Action do
describe 'cookies' do
it 'gets cookies' do
action = GetCookiesAction.new
_, headers, body = action.call({'HTTP_COOKIE' => 'foo=bar'})
action.send(:cookies).must_include({foo: 'bar'})
headers.must_equal({'Content-Type' => 'application/octet-stream; charset=utf-8'})
body.must_equal ['bar']
end
it 'change cookies' do
action = ChangeCookiesAction.new
_, headers, body = action.call({'HTTP_COOKIE' => 'foo=bar'})
action.send(:cookies).must_include({foo: 'bar'})
headers.must_equal({'Content-Type' => 'application/octet-stream; charset=utf-8', 'Set-Cookie' => 'foo=baz'})
body.must_equal ['bar']
end
it 'sets cookies' do
action = SetCookiesAction.new
_, headers, body = action.call({})
body.must_equal(['yo'])
headers.must_equal({'Content-Type' => 'application/octet-stream; charset=utf-8', 'Set-Cookie' => 'foo=yum%21'})
end
it 'sets cookies with options' do
tomorrow = Time.now + 60 * 60 * 24
action = SetCookiesWithOptionsAction.new(expires: tomorrow)
_, headers, _ = action.call({})
headers.must_equal({'Content-Type' => 'application/octet-stream; charset=utf-8', 'Set-Cookie' => "kukki=yum%21; domain=hanamirb.org; path=/controller; expires=#{ tomorrow.gmtime.rfc2822 }; secure; HttpOnly"})
end
it 'removes cookies' do
action = RemoveCookiesAction.new
_, headers, _ = action.call({'HTTP_COOKIE' => 'foo=bar;rm=me'})
headers.must_equal({'Content-Type' => 'application/octet-stream; charset=utf-8', 'Set-Cookie' => "rm=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"})
end
describe 'with default cookies' do
it 'gets default cookies' do
action = GetDefaultCookiesAction.new
action.class.configuration.cookies({
domain: 'hanamirb.org', path: '/controller', secure: true, httponly: true
})
_, headers, _ = action.call({})
headers.must_equal({'Content-Type' => 'application/octet-stream; charset=utf-8', 'Set-Cookie' => 'bar=foo; domain=hanamirb.org; path=/controller; secure; HttpOnly'})
end
it "overwritten cookies' values are respected" do
action = GetOverwrittenCookiesAction.new
action.class.configuration.cookies({
domain: 'hanamirb.org', path: '/controller', secure: true, httponly: true
})
_, headers, _ = action.call({})
headers.must_equal({'Content-Type' => 'application/octet-stream; charset=utf-8', 'Set-Cookie' => 'bar=foo; domain=hanamirb.com; path=/action'})
end
end
describe 'with max_age option and without expires option' do
it 'automatically set expires option' do
Time.stub :now, Time.now do
action = GetAutomaticallyExpiresCookiesAction.new
_, headers, _ = action.call({})
max_age = 120
headers["Set-Cookie"].must_include("max-age=#{max_age}")
headers["Set-Cookie"].must_include("expires=#{(Time.now + max_age).gmtime.rfc2822}")
end
end
end
end
end

View File

@ -1,11 +0,0 @@
require 'test_helper'
describe Hanami::Controller::Error do
it 'inherits from ::StandardError' do
Hanami::Controller::Error.superclass.must_equal StandardError
end
it 'is parent to UnknownFormatError' do
Hanami::Controller::UnknownFormatError.superclass.must_equal Hanami::Controller::Error
end
end

View File

@ -1,390 +0,0 @@
require 'test_helper'
require 'hanami/router'
require 'hanami/action/cache'
CacheControlRoutes = Hanami::Router.new do
get '/default', to: 'cache_control#default'
get '/overriding', to: 'cache_control#overriding'
get '/symbol', to: 'cache_control#symbol'
get '/symbols', to: 'cache_control#symbols'
get '/hash', to: 'cache_control#hash'
get '/private-and-public', to: 'cache_control#private_public'
end
ExpiresRoutes = Hanami::Router.new do
get '/default', to: 'expires#default'
get '/overriding', to: 'expires#overriding'
get '/symbol', to: 'expires#symbol'
get '/symbols', to: 'expires#symbols'
get '/hash', to: 'expires#hash'
end
ConditionalGetRoutes = Hanami::Router.new do
get '/etag', to: 'conditional_get#etag'
get '/last-modified', to: 'conditional_get#last_modified'
get '/etag-last-modified', to: 'conditional_get#etag_last_modified'
end
module CacheControl
class Default
include Hanami::Action
include Hanami::Action::Cache
cache_control :public, max_age: 600
def call(params)
end
end
class Overriding
include Hanami::Action
include Hanami::Action::Cache
cache_control :public, max_age: 600
def call(params)
cache_control :private
end
end
class Symbol
include Hanami::Action
include Hanami::Action::Cache
def call(params)
cache_control :private
end
end
class Symbols
include Hanami::Action
include Hanami::Action::Cache
def call(params)
cache_control :private, :no_cache, :no_store
end
end
class Hash
include Hanami::Action
include Hanami::Action::Cache
def call(params)
cache_control :public, :no_store, max_age: 900, s_maxage: 86400, min_fresh: 500, max_stale: 700
end
end
class PrivatePublic
include Hanami::Action
include Hanami::Action::Cache
def call(params)
cache_control :private, :public
end
end
end
module Expires
class Default
include Hanami::Action
include Hanami::Action::Cache
expires 900, :public, :no_cache
def call(params)
end
end
class Overriding
include Hanami::Action
include Hanami::Action::Cache
expires 900, :public, :no_cache
def call(params)
expires 600, :private
end
end
class Symbol
include Hanami::Action
include Hanami::Action::Cache
def call(params)
expires 900, :private
end
end
class Symbols
include Hanami::Action
include Hanami::Action::Cache
def call(params)
expires 900, :private, :no_cache, :no_store
end
end
class Hash
include Hanami::Action
include Hanami::Action::Cache
def call(params)
expires 900, :public, :no_store, s_maxage: 86400, min_fresh: 500, max_stale: 700
end
end
end
module ConditionalGet
class Etag
include Hanami::Action
include Hanami::Action::Cache
def call(params)
fresh etag: 'updated'
end
end
class LastModified
include Hanami::Action
include Hanami::Action::Cache
def call(params)
fresh last_modified: Time.now
end
end
class EtagLastModified
include Hanami::Action
include Hanami::Action::Cache
def call(params)
fresh etag: 'updated', last_modified: Time.now
end
end
end
describe 'Cache control' do
before do
@app = Rack::MockRequest.new(CacheControlRoutes)
end
describe 'default cache control' do
it 'returns default Cache-Control headers' do
response = @app.get('/default')
response.headers.fetch('Cache-Control').split(', ').must_equal %w(public max-age=600)
end
describe 'but some action overrides it' do
it 'returns more specific Cache-Control headers' do
response = @app.get('/overriding')
response.headers.fetch('Cache-Control').split(', ').must_equal %w(private)
end
end
end
it 'accepts a Symbol' do
response = @app.get('/symbol')
response.headers.fetch('Cache-Control').must_equal('private')
end
it 'accepts multiple Symbols' do
response = @app.get('/symbols')
response.headers.fetch('Cache-Control').split(', ').must_equal %w(private no-cache no-store)
end
it 'accepts a Hash' do
Time.stub(:now, Time.now) do
response = @app.get('/hash')
response.headers.fetch('Cache-Control').split(', ').must_equal %w(public no-store max-age=900 s-maxage=86400 min-fresh=500 max-stale=700)
end
end
describe "private and public directives" do
it "ignores public directive" do
response = @app.get('/private-and-public')
response.headers.fetch('Cache-Control').must_equal('private')
end
end
end
describe 'Expires' do
before do
@app = Rack::MockRequest.new(ExpiresRoutes)
end
describe 'default cache control' do
it 'returns default Cache-Control headers' do
response = @app.get('/default')
response.headers.fetch('Expires').must_equal (Time.now + 900).httpdate
response.headers.fetch('Cache-Control').split(', ').must_equal %w(public no-cache max-age=900)
end
describe 'but some action overrides it' do
it 'returns more specific Cache-Control headers' do
response = @app.get('/overriding')
response.headers.fetch('Expires').must_equal (Time.now + 600).httpdate
response.headers.fetch('Cache-Control').split(', ').must_equal %w(private max-age=600)
end
end
end
it 'accepts a Symbol' do
Time.stub(:now, Time.now) do
response = @app.get('/symbol')
response.headers.fetch('Expires').must_equal (Time.now + 900).httpdate
response.headers.fetch('Cache-Control').split(', ').must_equal %w(private max-age=900)
end
end
it 'accepts multiple Symbols' do
Time.stub(:now, Time.now) do
response = @app.get('/symbols')
response.headers.fetch('Expires').must_equal (Time.now + 900).httpdate
response.headers.fetch('Cache-Control').split(', ').must_equal %w(private no-cache no-store max-age=900)
end
end
it 'accepts a Hash' do
Time.stub(:now, Time.now) do
response = @app.get('/hash')
response.headers.fetch('Expires').must_equal (Time.now + 900).httpdate
response.headers.fetch('Cache-Control').split(', ').must_equal %w(public no-store s-maxage=86400 min-fresh=500 max-stale=700 max-age=900)
end
end
end
describe 'Fresh' do
before do
@app = Rack::MockRequest.new(ConditionalGetRoutes)
end
describe 'etag' do
describe 'when etag matches HTTP_IF_NONE_MATCH header' do
it 'halts 304 not modified' do
response = @app.get('/etag', {'HTTP_IF_NONE_MATCH' => 'updated'})
response.status.must_equal 304
end
it 'keeps the same etag header' do
response = @app.get('/etag', {'HTTP_IF_NONE_MATCH' => 'outdated'})
response.headers.fetch('ETag').must_equal 'updated'
end
end
describe 'when etag does not match HTTP_IF_NONE_MATCH header' do
it 'completes request' do
response = @app.get('/etag', {'HTTP_IF_NONE_MATCH' => 'outdated'})
response.status.must_equal 200
end
it 'returns etag header' do
response = @app.get('/etag', {'HTTP_IF_NONE_MATCH' => 'outdated'})
response.headers.fetch('ETag').must_equal 'updated'
end
end
end
describe 'last_modified' do
describe 'when last modified is less than or equal to HTTP_IF_MODIFIED_SINCE header' do
before { @modified_since = Time.new(2014, 1, 8, 0, 0, 0) }
it 'halts 304 not modified' do
Time.stub(:now, @modified_since) do
response = @app.get('/last-modified', {'HTTP_IF_MODIFIED_SINCE' => @modified_since.httpdate})
response.status.must_equal 304
end
end
it 'keeps the same IfModifiedSince header' do
Time.stub(:now, @modified_since) do
response = @app.get('/last-modified', {'HTTP_IF_MODIFIED_SINCE' => @modified_since.httpdate})
response.headers.fetch('Last-Modified').must_equal @modified_since.httpdate
end
end
end
describe 'when last modified is bigger than HTTP_IF_MODIFIED_SINCE header' do
before do
@modified_since = Time.new(2014, 1, 8, 0, 0, 0)
@last_modified = Time.new(2014, 2, 8, 0, 0, 0)
end
it 'completes request' do
Time.stub(:now, @last_modified) do
response = @app.get('/last-modified', {'HTTP_IF_MODIFIED_SINCE' => @modified_since.httpdate})
response.status.must_equal 200
end
end
it 'returns etag header' do
Time.stub(:now, @last_modified) do
response = @app.get('/last-modified', {'HTTP_IF_MODIFIED_SINCE' => @modified_since.httpdate})
response.headers.fetch('Last-Modified').must_equal @last_modified.httpdate
end
end
end
describe 'when last modified is empty string' do
before do
@modified_since = Time.new(2014, 1, 8, 0, 0, 0)
@last_modified = Time.new(2014, 2, 8, 0, 0, 0)
end
describe 'and HTTP_IF_MODIFIED_SINCE empty' do
it 'completes request' do
response = @app.get('/last-modified', {'HTTP_IF_MODIFIED_SINCE' => ''})
response.status.must_equal 200
end
it 'stays the Last-Modified header as time' do
Time.stub(:now, @modified_since) do
response = @app.get('/last-modified', {'HTTP_IF_MODIFIED_SINCE' => ''})
response.headers.fetch('Last-Modified').must_equal @modified_since.httpdate
end
end
end
describe 'and HTTP_IF_MODIFIED_SINCE contain space string' do
it 'completes request' do
response = @app.get('/last-modified', {'HTTP_IF_MODIFIED_SINCE' => ' '})
response.status.must_equal 200
end
it 'stays the Last-Modified header as time' do
Time.stub(:now, @modified_since) do
response = @app.get('/last-modified', {'HTTP_IF_MODIFIED_SINCE' => ' '})
response.headers.fetch('Last-Modified').must_equal @modified_since.httpdate
end
end
end
describe 'and HTTP_IF_NONE_MATCH empty' do
it 'completes request' do
response = @app.get('/last-modified', {'HTTP_IF_NONE_MATCH' => ''})
response.status.must_equal 200
end
it "doesn't send Last-Modified" do
Time.stub(:now, @modified_since) do
response = @app.get('/last-modified', {'HTTP_IF_NONE_MATCH' => ''})
assert !response.headers.key?('Last-Modified')
end
end
end
describe 'and HTTP_IF_NONE_MATCH contain space string' do
it 'completes request' do
response = @app.get('/last-modified', {'HTTP_IF_NONE_MATCH' => ' '})
response.status.must_equal 200
end
it "doesn't send Last-Modified" do
Time.stub(:now, @modified_since) do
response = @app.get('/last-modified', {'HTTP_IF_NONE_MATCH' => ' '})
assert !response.headers.key?('Last-Modified')
end
end
end
end
end
end

View File

@ -1,80 +0,0 @@
require 'test_helper'
describe 'Framework configuration' do
it 'keeps separated copies of the configuration' do
hanami_configuration = Hanami::Controller.configuration
music_configuration = MusicPlayer::Controller.configuration
artists_show_config = MusicPlayer::Controllers::Artists::Show.configuration
hanami_configuration.wont_equal(music_configuration)
hanami_configuration.wont_equal(artists_show_config)
end
it 'inheriths configurations at the framework level' do
_, _, body = MusicPlayer::Controllers::Dashboard::Index.new.call({})
body.must_equal ['Muzic!']
end
it 'catches exception handled at the framework level' do
code, _, _ = MusicPlayer::Controllers::Dashboard::Show.new.call({})
code.must_equal 400
end
it 'catches exception handled at the action level' do
code, _, _ = MusicPlayer::Controllers::Artists::Show.new.call({})
code.must_equal 404
end
it 'allows standalone actions to inherith framework configuration' do
code, _, _ = MusicPlayer::StandaloneAction.new.call({})
code.must_equal 400
end
it 'allows standalone modulized actions to inherith framework configuration' do
Hanami::Controller.configuration.handled_exceptions.wont_include App::CustomError
App::StandaloneAction.configuration.handled_exceptions.must_include App::CustomError
code, _, _ = App::StandaloneAction.new.call({})
code.must_equal 400
end
it 'allows standalone modulized controllers to inherith framework configuration' do
Hanami::Controller.configuration.handled_exceptions.wont_include App2::CustomError
App2::Standalone::Index.configuration.handled_exceptions.must_include App2::CustomError
code, _, _ = App2::Standalone::Index.new.call({})
code.must_equal 400
end
it 'includes modules from configuration' do
modules = MusicPlayer::Controllers::Artists::Show.included_modules
modules.must_include(Hanami::Action::Cookies)
modules.must_include(Hanami::Action::Session)
end
it 'correctly includes user defined modules' do
code, _, body = MusicPlayer::Controllers::Artists::Index.new.call({})
code.must_equal 200
body.must_equal ['Luca']
end
describe 'default headers' do
it "if default headers aren't setted only content-type header is returned" do
code, headers, _ = FullStack::Controllers::Home::Index.new.call({})
code.must_equal 200
headers.must_equal({"Content-Type"=>"application/octet-stream; charset=utf-8"})
end
it "if default headers are setted, default headers are returned" do
code, headers, _ = MusicPlayer::Controllers::Artists::Index.new.call({})
code.must_equal 200
headers.must_equal({"Content-Type" => "application/octet-stream; charset=utf-8", "X-Frame-Options" => "DENY"})
end
it "default headers overrided in action" do
code, headers, _ = MusicPlayer::Controllers::Dashboard::Index.new.call({})
code.must_equal 200
headers.must_equal({"Content-Type" => "application/octet-stream; charset=utf-8", "X-Frame-Options" => "ALLOW FROM https://example.org"})
end
end
end

View File

@ -1,35 +0,0 @@
require 'test_helper'
describe 'Framework freeze' do
describe 'Hanami::Controller' do
before do
Hanami::Controller.load!
end
after do
Hanami::Controller.unload!
end
it 'freezes framework configuration' do
Hanami::Controller.configuration.must_be :frozen?
end
# it 'freezes action configuration' do
# CallAction.configuration.must_be :frozen?
# end
end
describe 'duplicated framework' do
before do
MusicPlayer::Controller.load!
end
it 'freezes framework configuration' do
MusicPlayer::Controller.configuration.must_be :frozen?
end
# it 'freezes action configuration' do
# MusicPlayer::Controllers::Artists::Index.configuration.must_be :frozen?
# end
end
end

View File

@ -1,96 +0,0 @@
require 'test_helper'
require 'rack/test'
describe 'Full stack application' do
include Rack::Test::Methods
def app
FullStack::Application.new
end
it 'passes action inside the Rack env' do
get '/', {}, 'HTTP_ACCEPT' => 'text/html'
last_response.body.must_include 'FullStack::Controllers::Home::Index'
last_response.body.must_include ':greeting=>"Hello"'
last_response.body.must_include ':format=>:html'
end
it 'omits the body if the request is HEAD' do
head '/head', {}, 'HTTP_ACCEPT' => 'text/html'
last_response.body.must_be_empty
last_response.headers['X-Renderable'].must_be_nil
end
it 'in case of redirect and invalid params, it passes errors in session and then deletes them' do
post '/books', { title: '' }
follow_redirect!
last_response.body.must_include 'FullStack::Controllers::Books::Index'
last_response.body.must_include %(params: {})
get '/books'
last_response.body.must_include %(params: {})
end
it 'uses flash to pass informations' do
get '/poll'
follow_redirect!
last_response.body.must_include 'FullStack::Controllers::Poll::Step1'
last_response.body.must_include %(Start the poll)
post '/poll/1', {}
follow_redirect!
last_response.body.must_include 'FullStack::Controllers::Poll::Step2'
last_response.body.must_include %(Step 1 completed)
end
it "doesn't return stale informations" do
post '/settings', {}
follow_redirect!
last_response.body.must_match %r{Hanami::Action::Flash:0x[\d\w]* {:message=>"Saved!"}}
get '/settings'
last_response.body.must_match %r{Hanami::Action::Flash:0x[\d\w]* {}}
end
it 'can access params with string symbols or methods' do
patch '/books/1', {
book: {
title: 'Hanami in Action',
author: {
name: 'Luca'
}
}
}
result = Marshal.load(last_response.body)
result.must_equal({
symbol_access: 'Luca',
valid: true,
errors: {}
})
end
it 'validates nested params' do
patch '/books/1', {
book: {
title: 'Hanami in Action',
}
}
result = Marshal.load(last_response.body)
result[:valid].must_equal false
result[:errors].must_equal(book: { author: ['is missing'] })
end
it "redirect in before action and call action method is not called" do
get 'users/1'
last_response.status.must_equal 302
last_response.body.must_equal 'Found' # This message is 302 status
end
end

View File

@ -1,122 +0,0 @@
require 'test_helper'
require 'rack/test'
HeadRoutes = Hanami::Router.new(namespace: HeadTest) do
get '/', to: 'home#index'
get '/code/:code', to: 'home#code'
get '/override', to: 'home#override'
end
HeadApplication = Rack::Builder.new do
use Rack::Session::Cookie, secret: SecureRandom.hex(16)
run HeadRoutes
end.to_app
describe 'HEAD' do
include Rack::Test::Methods
def app
HeadApplication
end
def response
last_response
end
it "doesn't send body and default headers" do
head '/'
response.status.must_equal(200)
response.body.must_equal ""
response.headers.to_a.wont_include ['X-Frame-Options','DENY']
end
it "allows to bypass restriction on custom headers" do
get '/override'
response.status.must_equal(204)
response.body.must_equal ""
headers = response.headers.to_a
headers.must_include ['Last-Modified','Fri, 27 Nov 2015 13:32:36 GMT']
headers.must_include ['X-Rate-Limit', '4000']
headers.wont_include ['X-No-Pass', 'true']
headers.wont_include ['Content-Type','application/octet-stream; charset=utf-8']
end
HTTP_TEST_STATUSES_WITHOUT_BODY.each do |code|
describe "with: #{ code }" do
it "doesn't send body and default headers" do
get "/code/#{ code }"
response.status.must_equal(code)
response.body.must_equal ""
response.headers.to_a.wont_include ['X-Frame-Options','DENY']
end
it "sends Allow header" do
get "/code/#{ code }"
response.status.must_equal(code)
response.headers['Allow'].must_equal 'GET, HEAD'
end
it "sends Content-Encoding header" do
get "/code/#{ code }"
response.status.must_equal(code)
response.headers['Content-Encoding'].must_equal 'identity'
end
it "sends Content-Language header" do
get "/code/#{ code }"
response.status.must_equal(code)
response.headers['Content-Language'].must_equal 'en'
end
it "doesn't send Content-Length header" do
get "/code/#{ code }"
response.status.must_equal(code)
response.headers.key?('Content-Length').must_equal false
end
it "doesn't send Content-Type header" do
get "/code/#{ code }"
response.status.must_equal(code)
response.headers.key?('Content-Type').must_equal false
end
it "sends Content-Location header" do
get "/code/#{ code }"
response.status.must_equal(code)
response.headers['Content-Location'].must_equal 'relativeURI'
end
it "sends Content-MD5 header" do
get "/code/#{ code }"
response.status.must_equal(code)
response.headers['Content-MD5'].must_equal 'c13367945d5d4c91047b3b50234aa7ab'
end
it "sends Expires header" do
get "/code/#{ code }"
response.status.must_equal(code)
response.headers['Expires'].must_equal 'Thu, 01 Dec 1994 16:00:00 GMT'
end
it "sends Last-Modified header" do
get "/code/#{ code }"
response.status.must_equal(code)
response.headers['Last-Modified'].must_equal 'Wed, 21 Jan 2015 11:32:10 GMT'
end
end
end
end

View File

@ -1,350 +0,0 @@
require 'test_helper'
require 'hanami/router'
MimeRoutes = Hanami::Router.new do
get '/', to: 'mimes#default'
get '/custom', to: 'mimes#custom'
get '/configuration', to: 'mimes#configuration'
get '/accept', to: 'mimes#accept'
get '/restricted', to: 'mimes#restricted'
get '/latin', to: 'mimes#latin'
get '/nocontent', to: 'mimes#no_content'
get '/response', to: 'mimes#default_response'
get '/overwritten_format', to: 'mimes#override_default_response'
get '/custom_from_accept', to: 'mimes#custom_from_accept'
end
module Mimes
class Default
include Hanami::Action
def call(params)
self.body = format
end
end
class Configuration
include Hanami::Action
configuration.default_request_format :html
configuration.default_charset 'ISO-8859-1'
def call(params)
self.body = format
end
end
class Custom
include Hanami::Action
def call(params)
self.format = :xml
self.body = format
end
end
class Latin
include Hanami::Action
def call(params)
self.charset = 'latin1'
self.format = :html
self.body = format
end
end
class Accept
include Hanami::Action
def call(params)
self.headers.merge!({'X-AcceptDefault' => accept?('application/octet-stream').to_s })
self.headers.merge!({'X-AcceptHtml' => accept?('text/html').to_s })
self.headers.merge!({'X-AcceptXml' => accept?('application/xml').to_s })
self.headers.merge!({'X-AcceptJson' => accept?('text/json').to_s })
self.body = format
end
end
class CustomFromAccept
include Hanami::Action
configuration.format custom: 'application/custom'
accept :json, :custom
def call(params)
self.body = format
end
end
class Restricted
include Hanami::Action
configuration.format custom: 'application/custom'
accept :html, :json, :custom
def call(params)
self.body = format.to_s
end
end
class NoContent
include Hanami::Action
def call(params)
self.status = 204
end
end
class DefaultResponse
include Hanami::Action
configuration.default_request_format :html
configuration.default_response_format :json
def call(params)
self.body = configuration.default_request_format
end
end
class OverrideDefaultResponse
include Hanami::Action
configuration.default_response_format :json
def call(params)
self.format = :xml
end
end
end
describe 'Content type' do
before do
@app = Rack::MockRequest.new(MimeRoutes)
end
it 'fallbacks to the default "Content-Type" header when the request is lacking of this information' do
response = @app.get('/')
response.headers['Content-Type'].must_equal 'application/octet-stream; charset=utf-8'
response.body.must_equal 'all'
end
it 'fallbacks to the default format and charset, set in the configuration' do
response = @app.get('/configuration')
response.headers['Content-Type'].must_equal 'text/html; charset=ISO-8859-1'
response.body.must_equal 'html'
end
it 'returns the specified "Content-Type" header' do
response = @app.get('/custom')
response.headers['Content-Type'].must_equal 'application/xml; charset=utf-8'
response.body.must_equal 'xml'
end
it 'returns the custom charser header' do
response = @app.get('/latin')
response.headers['Content-Type'].must_equal 'text/html; charset=latin1'
response.body.must_equal 'html'
end
it 'uses default_response_format if set in the configuration regardless of request format' do
response = @app.get('/response')
response.headers['Content-Type'].must_equal 'application/json; charset=utf-8'
response.body.must_equal 'html'
end
it 'allows to override default_response_format' do
response = @app.get('/overwritten_format')
response.headers['Content-Type'].must_equal 'application/xml; charset=utf-8'
end
# FIXME Review if this test must be in place
it 'does not produce a "Content-Type" header when the request has a 204 No Content status'
# it 'does not produce a "Content-Type" header when the request has a 204 No Content status' do
# response = @app.get('/nocontent')
# response.headers['Content-Type'].must_be_nil
# response.body.must_equal ''
# end
describe 'when Accept is sent' do
it 'sets "Content-Type" header according to wildcard value' do
response = @app.get('/', 'HTTP_ACCEPT' => '*/*')
content_type = 'application/octet-stream; charset=utf-8'
response.headers['Content-Type'].must_equal content_type
response.body.must_equal 'all'
end
it 'sets "Content-Type" header according to exact value' do
headers = {'HTTP_ACCEPT' => 'application/custom'}
response = @app.get('/custom_from_accept', headers)
content_type = 'application/custom; charset=utf-8'
response.headers['Content-Type'].must_equal content_type
response.body.must_equal 'custom'
end
it 'sets "Content-Type" header according to weighted value' do
accept = 'application/custom;q=0.9,application/json;q=0.5'
headers = {'HTTP_ACCEPT' => accept}
response = @app.get('/custom_from_accept', headers)
content_type = 'application/custom; charset=utf-8'
response.headers['Content-Type'].must_equal content_type
response.body.must_equal 'custom'
end
it 'sets "Content-Type" header according to weighted, unordered value' do
accept = 'application/custom;q=0.1, application/json;q=0.5'
headers = {'HTTP_ACCEPT' => accept}
response = @app.get('/custom_from_accept', headers)
content_type = 'application/json; charset=utf-8'
response.headers['Content-Type'].must_equal content_type
response.body.must_equal 'json'
end
it 'sets "Content-Type" header according to exact and weighted value' do
accept = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
response = @app.get('/', 'HTTP_ACCEPT' => accept)
response.headers['Content-Type'].must_equal 'text/html; charset=utf-8'
response.body.must_equal 'html'
end
it 'sets "Content-Type" header according to quality scale value' do
accept = 'application/json;q=0.6,application/xml;q=0.9,*/*;q=0.8'
headers = {'HTTP_ACCEPT' => accept}
response = @app.get('/', headers)
content_type = 'application/xml; charset=utf-8'
response.headers['Content-Type'].must_equal content_type
response.body.must_equal 'xml'
end
end
end
describe 'Accept' do
before do
@app = Rack::MockRequest.new(MimeRoutes)
@response = @app.get('/accept', 'HTTP_ACCEPT' => accept)
end
describe 'when Accept is missing' do
let(:accept) { nil }
it 'accepts all' do
@response.headers['X-AcceptDefault'].must_equal 'true'
@response.headers['X-AcceptHtml'].must_equal 'true'
@response.headers['X-AcceptXml'].must_equal 'true'
@response.headers['X-AcceptJson'].must_equal 'true'
@response.body.must_equal 'all'
end
end
describe 'when Accept is sent' do
describe 'when "*/*"' do
let(:accept) { '*/*' }
it 'accepts all' do
@response.headers['X-AcceptDefault'].must_equal 'true'
@response.headers['X-AcceptHtml'].must_equal 'true'
@response.headers['X-AcceptXml'].must_equal 'true'
@response.headers['X-AcceptJson'].must_equal 'true'
@response.body.must_equal 'all'
end
end
describe 'when "text/html"' do
let(:accept) { 'text/html' }
it 'accepts selected mime types' do
@response.headers['X-AcceptDefault'].must_equal 'false'
@response.headers['X-AcceptHtml'].must_equal 'true'
@response.headers['X-AcceptXml'].must_equal 'false'
@response.headers['X-AcceptJson'].must_equal 'false'
@response.body.must_equal 'html'
end
end
describe 'when weighted' do
let(:accept) { 'text/html,application/xhtml+xml,application/xml;q=0.9' }
it 'accepts selected mime types' do
@response.headers['X-AcceptDefault'].must_equal 'false'
@response.headers['X-AcceptHtml'].must_equal 'true'
@response.headers['X-AcceptXml'].must_equal 'true'
@response.headers['X-AcceptJson'].must_equal 'false'
@response.body.must_equal 'html'
end
end
end
end
describe 'Restricted Accept' do
before do
@app = Rack::MockRequest.new(MimeRoutes)
@response = @app.get('/restricted', 'HTTP_ACCEPT' => accept)
end
describe 'when Accept is missing' do
let(:accept) { nil }
it 'returns the mime type according to the application defined policy' do
@response.status.must_equal 200
@response.body.must_equal 'all'
end
end
describe 'when Accept is sent' do
describe 'when "*/*"' do
let(:accept) { '*/*' }
it 'returns the mime type according to the application defined policy' do
@response.status.must_equal 200
@response.body.must_equal 'all'
end
end
describe 'when accepted' do
let(:accept) { 'text/html' }
it 'accepts selected mime types' do
@response.status.must_equal 200
@response.body.must_equal 'html'
end
end
describe 'when custom mime type' do
let(:accept) { 'application/custom' }
it 'accepts selected mime types' do
@response.status.must_equal 200
@response.body.must_equal 'custom'
end
end
describe 'when not accepted' do
let(:accept) { 'application/xml' }
it 'accepts selected mime types' do
@response.status.must_equal 406
end
end
describe 'when weighted' do
describe 'with an accepted format as first choice' do
let(:accept) { 'text/html,application/xhtml+xml,application/xml;q=0.9' }
it 'accepts selected mime types' do
@response.status.must_equal 200
@response.body.must_equal 'html'
end
end
describe 'with an accepted format as last choice' do
let(:accept) { 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,*/*;q=0.5' }
it 'accepts selected mime types' do
@response.status.must_equal 200
@response.body.must_equal 'html'
end
end
end
end
end

View File

@ -1,26 +0,0 @@
require 'test_helper'
describe "Exception notifiers integration" do
let(:env) { Hash[] }
it 'reference error in rack.exception' do
action = RackExceptionAction.new
action.call(env)
env['rack.exception'].must_be_kind_of RackExceptionAction::TestException
end
it "doesnt' reference error in rack.exception if it's handled" do
action = HandledRackExceptionAction.new
action.call(env)
env['rack.exception'].must_be_nil
end
it "doesn't reference of an error in rack.exception if it's handled" do
action = HandledRackExceptionSubclassAction.new
action.call(env)
env['rack.exception'].must_be_nil
end
end

View File

@ -1,151 +0,0 @@
require 'test_helper'
require 'hanami/router'
Routes = Hanami::Router.new do
get '/', to: 'root'
get '/team', to: 'about#team'
get '/contacts', to: 'about#contacts'
resource :identity
resources :flowers
resources :painters, only: [:update]
end
describe 'Hanami::Router integration' do
before do
@app = Rack::MockRequest.new(Routes)
end
it 'calls simple action' do
response = @app.get('/')
response.status.must_equal 200
response.body.must_equal '{}'
response.headers['X-Test'].must_equal 'test'
end
it "calls a controller's class action" do
response = @app.get('/team')
response.status.must_equal 200
response.body.must_equal '{}'
response.headers['X-Test'].must_equal 'test'
end
it "calls a controller's action (with DSL)" do
response = @app.get('/contacts')
response.status.must_equal 200
response.body.must_equal '{}'
end
it 'returns a 404 for unknown path' do
response = @app.get('/unknown')
response.status.must_equal 404
end
describe 'resource' do
it 'calls GET show' do
response = @app.get('/identity')
response.status.must_equal 200
response.body.must_equal "{}"
end
it 'calls GET new' do
response = @app.get('/identity/new')
response.status.must_equal 200
response.body.must_equal '{}'
end
it 'calls POST create' do
response = @app.post('/identity', params: { identity: { avatar: { image: 'jodosha.png' } }})
response.status.must_equal 200
response.body.must_equal %({:identity=>{:avatar=>{:image=>\"jodosha.png\"}}})
end
it 'calls GET edit' do
response = @app.get('/identity/edit')
response.status.must_equal 200
response.body.must_equal "{}"
end
it 'calls PATCH update' do
response = @app.request('PATCH', '/identity', params: { identity: { avatar: { image: 'jodosha-2x.png' } }})
response.status.must_equal 200
response.body.must_equal %({:identity=>{:avatar=>{:image=>\"jodosha-2x.png\"}}})
end
it 'calls DELETE destroy' do
response = @app.delete('/identity')
response.status.must_equal 200
response.body.must_equal "{}"
end
end
describe 'resources' do
it 'calls GET index' do
response = @app.get('/flowers')
response.status.must_equal 200
response.body.must_equal '{}'
end
it 'calls GET show' do
response = @app.get('/flowers/23')
response.status.must_equal 200
response.body.must_equal %({:id=>"23"})
end
it 'calls GET new' do
response = @app.get('/flowers/new')
response.status.must_equal 200
response.body.must_equal '{}'
end
it 'calls POST create' do
response = @app.post('/flowers', params: { flower: { name: 'Hanami' } })
response.status.must_equal 200
response.body.must_equal %({:flower=>{:name=>"Hanami"}})
end
it 'calls GET edit' do
response = @app.get('/flowers/23/edit')
response.status.must_equal 200
response.body.must_equal %({:id=>"23"})
end
it 'calls PATCH update' do
response = @app.request('PATCH', '/flowers/23', params: { flower: { name: 'Hanami!' } })
response.status.must_equal 200
response.body.must_equal %({:flower=>{:name=>"Hanami!"}, :id=>"23"})
end
it 'calls DELETE destroy' do
response = @app.delete('/flowers/23')
response.status.must_equal 200
response.body.must_equal %({:id=>"23"})
end
describe 'with validations' do
it 'automatically whitelists params from router' do
response = @app.request('PATCH', '/painters/23', params: { painter: { first_name: 'Gustav', last_name: 'Klimt' } })
response.status.must_equal 200
response.body.must_equal %({:painter=>{:first_name=>"Gustav", :last_name=>"Klimt"}, :id=>"23"})
end
end
end
end

View File

@ -1,202 +0,0 @@
require 'test_helper'
require 'rack/test'
SendFileRoutes = Hanami::Router.new(namespace: SendFileTest) do
get '/files/flow', to: 'files#flow'
get '/files/unsafe_local', to: 'files#unsafe_local'
get '/files/unsafe_public', to: 'files#unsafe_public'
get '/files/unsafe_absolute', to: 'files#unsafe_absolute'
get '/files/unsafe_missing_local', to: 'files#unsafe_missing_local'
get '/files/unsafe_missing_absolute', to: 'files#unsafe_missing_absolute'
get '/files/:id(.:format)', to: 'files#show'
get '/files/(*glob)', to: 'files#glob'
end
SendFileApplication = Rack::Builder.new do
use Rack::Lint
run SendFileRoutes
end.to_app
describe 'Full stack application' do
include Rack::Test::Methods
def app
SendFileApplication
end
describe 'send files from anywhere in the system' do
it 'responds 200 when a local file exists' do
get '/files/unsafe_local', {}
file = Pathname.new('Gemfile')
last_response.status.must_equal 200
last_response.headers['Content-Length'].to_i.must_equal file.size
last_response.headers['Content-Type'].must_equal 'text/plain'
last_response.body.size.must_equal(file.size)
end
it 'responds 200 when a relative path file exists' do
get '/files/unsafe_public', {}
file = Pathname.new('test/assets/test.txt')
last_response.status.must_equal 200
last_response.headers['Content-Length'].to_i.must_equal file.size
last_response.headers['Content-Type'].must_equal 'text/plain'
last_response.body.size.must_equal(file.size)
end
it 'responds 200 when an absoute path file exists' do
get '/files/unsafe_absolute', {}
file = Pathname.new('Gemfile')
last_response.status.must_equal 200
last_response.headers['Content-Length'].to_i.must_equal file.size
last_response.headers['Content-Type'].must_equal 'text/plain'
last_response.body.size.must_equal(file.size)
end
it 'responds 404 when a relative path does not exists' do
get '/files/unsafe_missing_local', {}
body = "Not Found"
last_response.status.must_equal 404
last_response.headers['Content-Length'].to_i.must_equal body.bytesize
last_response.headers['Content-Type'].must_equal 'text/plain'
last_response.body.must_equal(body)
end
it 'responds 404 when an absolute path does not exists' do
get '/files/unsafe_missing_absolute', {}
body = "Not Found"
last_response.status.must_equal 404
last_response.headers['Content-Length'].to_i.must_equal body.bytesize
last_response.headers['Content-Type'].must_equal 'text/plain'
last_response.body.must_equal(body)
end
end
describe 'when file exists, app responds 200' do
it 'sets Content-Type according to file type' do
get '/files/1', {}
file = Pathname.new('test/assets/test.txt')
last_response.status.must_equal 200
last_response.headers['Content-Length'].to_i.must_equal file.size
last_response.headers['Content-Type'].must_equal 'text/plain'
last_response.body.size.must_equal(file.size)
end
it 'sets Content-Type according to file type (ignoring HTTP_ACCEPT)' do
get '/files/2', {}, 'HTTP_ACCEPT' => 'text/html'
file = Pathname.new('test/assets/hanami.png')
last_response.status.must_equal 200
last_response.headers['Content-Length'].to_i.must_equal file.size
last_response.headers['Content-Type'].must_equal 'image/png'
last_response.body.size.must_equal(file.size)
end
it "doesn't send file in case of HEAD request" do
head '/files/1', {}
last_response.status.must_equal 200
last_response.headers.key?('Content-Length').must_equal false
last_response.headers.key?('Content-Type').must_equal false
last_response.body.must_be :empty?
end
it "doesn't send file outside of public directory" do
get '/files/3', {}
last_response.status.must_equal 404
end
end
describe "if file doesn't exist" do
it "responds 404" do
get '/files/100', {}
last_response.status.must_equal 404
last_response.body.must_equal "Not Found"
end
end
describe 'using conditional glob routes and :format' do
it "serves up json" do
get '/files/500.json', {}
file = Pathname.new('test/assets/resource-500.json')
last_response.status.must_equal 200
last_response.headers['Content-Type'].must_equal 'application/json'
last_response.body.size.must_equal(file.size)
end
it "fails on an unknown format" do
get '/files/500.xml', {}
last_response.status.must_equal 406
end
it "serves up html" do
get '/files/500.html', {}
file = Pathname.new('test/assets/resource-500.html')
last_response.status.must_equal 200
last_response.headers['Content-Type'].must_equal 'text/html; charset=utf-8'
last_response.body.size.must_equal(file.size)
end
it "works without a :format" do
get '/files/500', {}
file = Pathname.new('test/assets/resource-500.json')
last_response.status.must_equal 200
last_response.headers['Content-Type'].must_equal 'application/json'
last_response.body.size.must_equal(file.size)
end
it "returns 400 when I give a bogus id" do
get '/files/not-an-id.json', {}
last_response.status.must_equal 400
end
it "blows up when :format is sent as an :id" do
get '/files/501.json', {}
last_response.status.must_equal 404
end
end
describe 'conditional get request' do
it "shouldn't send file" do
if_modified_since = File.mtime('test/assets/test.txt').httpdate
get '/files/1', {}, 'HTTP_ACCEPT' => 'text/html', 'HTTP_IF_MODIFIED_SINCE' => if_modified_since
last_response.status.must_equal 304
last_response.headers.key?('Content-Length').must_equal false
last_response.headers.key?('Content-Type').must_equal false
last_response.body.must_be :empty?
end
end
describe 'bytes range' do
it "sends ranged contents" do
get '/files/1', {}, 'HTTP_RANGE' => 'bytes=5-13'
last_response.status.must_equal 206
last_response.headers['Content-Length'].must_equal '9'
last_response.headers['Content-Range'].must_equal 'bytes 5-13/69'
last_response.body.must_equal "Text File"
end
end
it "interrupts the control flow" do
get '/files/flow', {}
last_response.status.must_equal 200
end
end

View File

@ -1,68 +0,0 @@
require 'test_helper'
require 'rack/test'
describe 'Rack middleware integration' do
include Rack::Test::Methods
def response
last_response
end
describe '.use' do
let(:app) { UseActionApplication }
it 'uses the specified Rack middleware' do
router = Hanami::Router.new do
get '/', to: 'use_action#index'
get '/show', to: 'use_action#show'
get '/edit', to: 'use_action#edit'
end
UseActionApplication = Rack::Builder.new do
run router
end.to_app
get '/'
response.status.must_equal 200
response.headers.fetch('X-Middleware').must_equal 'OK'
response.headers['Y-Middleware'].must_be_nil
response.body.must_equal 'Hello from UseAction::Index'
get '/show'
response.status.must_equal 200
response.headers.fetch('Y-Middleware').must_equal 'OK'
response.headers['X-Middleware'].must_be_nil
response.body.must_equal 'Hello from UseAction::Show'
get '/edit'
response.status.must_equal 200
response.headers.fetch('Z-Middleware').must_equal 'OK'
response.headers['X-Middleware'].must_be_nil
response.headers['Y-Middleware'].must_be_nil
response.body.must_equal 'Hello from UseAction::Edit'
end
end
describe 'not using .use' do
let(:app) { NoUseActionApplication }
it "action doens't use a middleware" do
router = Hanami::Router.new do
get '/', to: 'no_use_action#index'
end
NoUseActionApplication = Rack::Builder.new do
run router
end.to_app
get '/'
response.status.must_equal 200
response.headers['X-Middleware'].must_be_nil
response.body.must_equal 'Hello from NoUseAction::Index'
end
end
end

View File

@ -1,22 +0,0 @@
require 'test_helper'
describe 'Method visibility' do
before do
@action = VisibilityAction.new
end
it 'x' do
status, headers, body = @action.call({})
status.must_equal 201
headers.fetch('X-Custom').must_equal 'OK'
headers.fetch('Y-Custom').must_equal 'YO'
body.must_equal ['x']
end
it 'has a public errors method' do
@action.public_methods.include?(:errors).must_equal true
end
end

View File

@ -1,9 +0,0 @@
require 'test_helper'
describe Hanami::Action::Mime do
it 'exposes content_type' do
action = CallAction.new
action.call({})
action.content_type.must_equal 'application/octet-stream'
end
end

View File

@ -1,16 +0,0 @@
require 'test_helper'
describe Hanami::Action::Rack do
before do
@action = MethodInspectionAction.new
end
['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'TRACE', 'OPTIONS'].each do |verb|
it "returns current request method (#{ verb })" do
env = Rack::MockRequest.env_for('/', method: verb)
_, _, body = @action.call(env)
body.must_equal [verb]
end
end
end

View File

@ -1,27 +0,0 @@
require 'test_helper'
describe Hanami::Action do
describe 'redirect' do
it 'redirects to the given path' do
action = RedirectAction.new
response = action.call({})
response[0].must_equal(302)
response[1].must_equal({ 'Location' => '/destination', 'Content-Type'=>'application/octet-stream; charset=utf-8' })
end
it 'redirects with custom status code' do
action = StatusRedirectAction.new
response = action.call({})
response[0].must_equal(301)
end
# Bug
# See: https://github.com/hanami/hanami/issues/196
it 'corces location to a ::String' do
response = SafeStringRedirectAction.new.call({})
response[1]['Location'].class.must_equal(::String)
end
end
end

View File

@ -1,45 +0,0 @@
require 'test_helper'
describe Hanami::Action do
describe 'session' do
it 'captures session from Rack env' do
action = SessionAction.new
action.call({'rack.session' => session = { 'user_id' => '23' }})
action.session.must_equal(session)
end
it 'returns empty hash when it is missing' do
action = SessionAction.new
action.call({})
action.session.must_equal({})
end
it 'exposes session' do
action = SessionAction.new
action.call({'rack.session' => session = { 'foo' => 'bar' }})
action.exposures[:session].must_equal(session)
end
it 'allows value access via symbols' do
action = SessionAction.new
action.call({'rack.session' => { 'foo' => 'bar' }})
action.session[:foo].must_equal('bar')
end
end
describe 'flash' do
it 'exposes flash' do
action = FlashAction.new
action.call({})
flash = action.exposures[:flash]
flash.must_be_kind_of(Hanami::Action::Flash)
flash[:error].must_equal "ouch"
end
end
end

View File

@ -1,92 +0,0 @@
require 'test_helper'
describe Hanami::Action do
before do
Hanami::Controller.unload!
end
describe '.handle_exception' do
it 'handle an exception with the given status' do
response = HandledExceptionAction.new.call({})
response[0].must_equal 404
end
it "returns a 500 if an action isn't handled" do
response = UnhandledExceptionAction.new.call({})
response[0].must_equal 500
end
describe 'with global handled exceptions' do
it 'handles raised exception' do
response = GlobalHandledExceptionAction.new.call({})
response[0].must_equal 400
end
end
end
describe '#throw' do
HTTP_TEST_STATUSES.each do |code, body|
next if HTTP_TEST_STATUSES_WITHOUT_BODY.include?(code)
it "throws an HTTP status code: #{ code }" do
response = ThrowCodeAction.new.call({ status: code })
response[0].must_equal code
response[2].must_equal [body]
end
end
it "throws an HTTP status code with given message" do
response = ThrowCodeAction.new.call({ status: 401, message: 'Secret Sauce' })
response[0].must_equal 401
response[2].must_equal ['Secret Sauce']
end
it 'throws the code as it is, when not recognized' do
response = ThrowCodeAction.new.call({ status: 2131231 })
response[0].must_equal 500
response[2].must_equal ['Internal Server Error']
end
it 'stops execution of before filters (method)' do
response = ThrowBeforeMethodAction.new.call({})
response[0].must_equal 401
response[2].must_equal ['Unauthorized']
end
it 'stops execution of before filters (block)' do
response = ThrowBeforeBlockAction.new.call({})
response[0].must_equal 401
response[2].must_equal ['Unauthorized']
end
it 'stops execution of after filters (method)' do
response = ThrowAfterMethodAction.new.call({})
response[0].must_equal 408
response[2].must_equal ['Request Timeout']
end
it 'stops execution of after filters (block)' do
response = ThrowAfterBlockAction.new.call({})
response[0].must_equal 408
response[2].must_equal ['Request Timeout']
end
end
describe 'using Kernel#throw in an action' do
it 'should work' do
response = CatchAndThrowSymbolAction.new.call({})
response[0].must_equal 200
end
end
end

View File

@ -1,125 +0,0 @@
require 'test_helper'
describe 'Directives' do
describe '#directives' do
describe 'non value directives' do
it 'accepts public symbol' do
subject = Hanami::Action::Cache::Directives.new(:public)
subject.values.size.must_equal(1)
end
it 'accepts private symbol' do
subject = Hanami::Action::Cache::Directives.new(:private)
subject.values.size.must_equal(1)
end
it 'accepts no_cache symbol' do
subject = Hanami::Action::Cache::Directives.new(:no_cache)
subject.values.size.must_equal(1)
end
it 'accepts no_store symbol' do
subject = Hanami::Action::Cache::Directives.new(:no_store)
subject.values.size.must_equal(1)
end
it 'accepts no_transform symbol' do
subject = Hanami::Action::Cache::Directives.new(:no_transform)
subject.values.size.must_equal(1)
end
it 'accepts must_revalidate symbol' do
subject = Hanami::Action::Cache::Directives.new(:must_revalidate)
subject.values.size.must_equal(1)
end
it 'accepts proxy_revalidate symbol' do
subject = Hanami::Action::Cache::Directives.new(:proxy_revalidate)
subject.values.size.must_equal(1)
end
it 'does not accept weird symbol' do
subject = Hanami::Action::Cache::Directives.new(:weird)
subject.values.size.must_equal(0)
end
describe 'multiple symbols' do
it 'creates one directive for each valid symbol' do
subject = Hanami::Action::Cache::Directives.new(:private, :proxy_revalidate)
subject.values.size.must_equal(2)
end
end
describe 'private and public at the same time' do
it 'ignores public directive' do
subject = Hanami::Action::Cache::Directives.new(:private, :public)
subject.values.size.must_equal(1)
end
it 'creates one private directive' do
subject = Hanami::Action::Cache::Directives.new(:private, :public)
subject.values.first.name.must_equal(:private)
end
end
end
describe 'value directives' do
it 'accepts max_age symbol' do
subject = Hanami::Action::Cache::Directives.new(max_age: 600)
subject.values.size.must_equal(1)
end
it 'accepts s_maxage symbol' do
subject = Hanami::Action::Cache::Directives.new(s_maxage: 600)
subject.values.size.must_equal(1)
end
it 'accepts min_fresh symbol' do
subject = Hanami::Action::Cache::Directives.new(min_fresh: 600)
subject.values.size.must_equal(1)
end
it 'accepts max_stale symbol' do
subject = Hanami::Action::Cache::Directives.new(max_stale: 600)
subject.values.size.must_equal(1)
end
it 'does not accept weird symbol' do
subject = Hanami::Action::Cache::Directives.new(weird: 600)
subject.values.size.must_equal(0)
end
describe 'multiple symbols' do
it 'creates one directive for each valid symbol' do
subject = Hanami::Action::Cache::Directives.new(max_age: 600, max_stale: 600)
subject.values.size.must_equal(2)
end
end
end
describe 'value and non value directives' do
it 'creates one directive for each valid symbol' do
subject = Hanami::Action::Cache::Directives.new(:public, max_age: 600, max_stale: 600)
subject.values.size.must_equal(3)
end
end
end
end
describe 'ValueDirective' do
describe '#to_str' do
it 'returns as http cache format' do
subject = Hanami::Action::Cache::ValueDirective.new(:max_age, 600)
subject.to_str.must_equal('max-age=600')
end
end
end
describe 'NonValueDirective' do
describe '#to_str' do
it 'returns as http cache format' do
subject = Hanami::Action::Cache::NonValueDirective.new(:no_cache)
subject.to_str.must_equal('no-cache')
end
end
end

View File

@ -1,7 +0,0 @@
require 'test_helper'
describe Hanami::Controller::VERSION do
it 'returns the current version' do
Hanami::Controller::VERSION.must_equal '1.0.0'
end
end