Migrate to RSpec (#228)
This commit is contained in:
parent
0f44b8525e
commit
1adb006c57
1
Gemfile
1
Gemfile
|
@ -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'
|
||||
|
||||
|
|
23
Rakefile
23
Rakefile
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
32
script/ci
32
script/ci
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
--color
|
|
@ -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
|
|
@ -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")
|
|
@ -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
|
|
@ -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
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 675 B After Width: | Height: | Size: 675 B |
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
RSpec.describe Hanami::Controller::Error do
|
||||
it 'inherits from ::StandardError' do
|
||||
expect(described_class.superclass).to eq(StandardError)
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue