diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..83e16f8 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/Gemfile b/Gemfile index 7db37a7..dbdeea5 100644 --- a/Gemfile +++ b/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' diff --git a/Rakefile b/Rakefile index 415a74f..e1ddb5f 100644 --- a/Rakefile +++ b/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' diff --git a/hanami-controller.gemspec b/hanami-controller.gemspec index a606951..3d2b69e 100644 --- a/hanami-controller.gemspec +++ b/hanami-controller.gemspec @@ -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 diff --git a/script/ci b/script/ci index ff81abe..1fc3a8a 100755 --- a/script/ci +++ b/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 } diff --git a/spec/integration/hanami/controller/cache_spec.rb b/spec/integration/hanami/controller/cache_spec.rb new file mode 100644 index 0000000..a3b7ca0 --- /dev/null +++ b/spec/integration/hanami/controller/cache_spec.rb @@ -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 diff --git a/spec/integration/hanami/controller/configuration_spec.rb b/spec/integration/hanami/controller/configuration_spec.rb new file mode 100644 index 0000000..7c63823 --- /dev/null +++ b/spec/integration/hanami/controller/configuration_spec.rb @@ -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 diff --git a/spec/integration/hanami/controller/framework_freeze_spec.rb b/spec/integration/hanami/controller/framework_freeze_spec.rb new file mode 100644 index 0000000..da6ced4 --- /dev/null +++ b/spec/integration/hanami/controller/framework_freeze_spec.rb @@ -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 diff --git a/spec/integration/hanami/controller/full_stack_spec.rb b/spec/integration/hanami/controller/full_stack_spec.rb new file mode 100644 index 0000000..c483648 --- /dev/null +++ b/spec/integration/hanami/controller/full_stack_spec.rb @@ -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 diff --git a/spec/integration/hanami/controller/head_spec.rb b/spec/integration/hanami/controller/head_spec.rb new file mode 100644 index 0000000..da79b99 --- /dev/null +++ b/spec/integration/hanami/controller/head_spec.rb @@ -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 diff --git a/spec/integration/hanami/controller/mime_type_spec.rb b/spec/integration/hanami/controller/mime_type_spec.rb new file mode 100644 index 0000000..1706ff7 --- /dev/null +++ b/spec/integration/hanami/controller/mime_type_spec.rb @@ -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 diff --git a/test/integration/rack_errors_test.rb b/spec/integration/hanami/controller/rack_errors_spec.rb similarity index 60% rename from test/integration/rack_errors_test.rb rename to spec/integration/hanami/controller/rack_errors_spec.rb index f06ebc8..a1f4476 100644 --- a/test/integration/rack_errors_test.rb +++ b/spec/integration/hanami/controller/rack_errors_spec.rb @@ -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 diff --git a/spec/integration/hanami/controller/rack_exception_spec.rb b/spec/integration/hanami/controller/rack_exception_spec.rb new file mode 100644 index 0000000..c7bdce5 --- /dev/null +++ b/spec/integration/hanami/controller/rack_exception_spec.rb @@ -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 diff --git a/spec/integration/hanami/controller/routing_spec.rb b/spec/integration/hanami/controller/routing_spec.rb new file mode 100644 index 0000000..7b5dade --- /dev/null +++ b/spec/integration/hanami/controller/routing_spec.rb @@ -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 diff --git a/spec/integration/hanami/controller/send_file_spec.rb b/spec/integration/hanami/controller/send_file_spec.rb new file mode 100644 index 0000000..2167e59 --- /dev/null +++ b/spec/integration/hanami/controller/send_file_spec.rb @@ -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 diff --git a/test/integration/sessions_test.rb b/spec/integration/hanami/controller/sessions_spec.rb similarity index 53% rename from test/integration/sessions_test.rb rename to spec/integration/hanami/controller/sessions_spec.rb index 4243707..0ced066 100644 --- a/test/integration/sessions_test.rb +++ b/spec/integration/hanami/controller/sessions_spec.rb @@ -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 diff --git a/test/integration/sessions_with_cookies_test.rb b/spec/integration/hanami/controller/sessions_with_cookies_spec.rb similarity index 50% rename from test/integration/sessions_with_cookies_test.rb rename to spec/integration/hanami/controller/sessions_with_cookies_spec.rb index c8959e6..6075529 100644 --- a/test/integration/sessions_with_cookies_test.rb +++ b/spec/integration/hanami/controller/sessions_with_cookies_spec.rb @@ -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 diff --git a/test/integration/sessions_without_cookies_test.rb b/spec/integration/hanami/controller/sessions_without_cookies_spec.rb similarity index 51% rename from test/integration/sessions_without_cookies_test.rb rename to spec/integration/hanami/controller/sessions_without_cookies_spec.rb index 8d52371..6683df0 100644 --- a/test/integration/sessions_without_cookies_test.rb +++ b/spec/integration/hanami/controller/sessions_without_cookies_spec.rb @@ -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 diff --git a/spec/integration/hanami/controller/use_spec.rb b/spec/integration/hanami/controller/use_spec.rb new file mode 100644 index 0000000..0f2fd5a --- /dev/null +++ b/spec/integration/hanami/controller/use_spec.rb @@ -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 diff --git a/spec/isolation/.rspec b/spec/isolation/.rspec new file mode 100644 index 0000000..4e1e0d2 --- /dev/null +++ b/spec/isolation/.rspec @@ -0,0 +1 @@ +--color diff --git a/test/isolation/without_validations_test.rb b/spec/isolation/without_hanami_validations_spec.rb similarity index 55% rename from test/isolation/without_validations_test.rb rename to spec/isolation/without_hanami_validations_spec.rb index 83f9ebe..506c261 100644 --- a/test/isolation/without_validations_test.rb +++ b/spec/isolation/without_hanami_validations_spec.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..c35dbae --- /dev/null +++ b/spec/spec_helper.rb @@ -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") diff --git a/test/test_helper.rb b/spec/support/controller.rb similarity index 62% rename from test/test_helper.rb rename to spec/support/controller.rb index a943c66..5a2bbdb 100644 --- a/test/test_helper.rb +++ b/spec/support/controller.rb @@ -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 diff --git a/test/fixtures.rb b/spec/support/fixtures.rb similarity index 98% rename from test/fixtures.rb rename to spec/support/fixtures.rb index 4cbb03c..c478bbd 100644 --- a/test/fixtures.rb +++ b/spec/support/fixtures.rb @@ -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 diff --git a/test/assets/hanami.png b/spec/support/fixtures/hanami.png similarity index 100% rename from test/assets/hanami.png rename to spec/support/fixtures/hanami.png diff --git a/test/assets/multipart-upload.png b/spec/support/fixtures/multipart-upload.png similarity index 100% rename from test/assets/multipart-upload.png rename to spec/support/fixtures/multipart-upload.png diff --git a/test/assets/resource-500.html b/spec/support/fixtures/resource-500.html similarity index 100% rename from test/assets/resource-500.html rename to spec/support/fixtures/resource-500.html diff --git a/test/assets/resource-500.json b/spec/support/fixtures/resource-500.json similarity index 100% rename from test/assets/resource-500.json rename to spec/support/fixtures/resource-500.json diff --git a/test/assets/test.txt b/spec/support/fixtures/test.txt similarity index 100% rename from test/assets/test.txt rename to spec/support/fixtures/test.txt diff --git a/spec/support/isolation_spec_helper.rb b/spec/support/isolation_spec_helper.rb new file mode 100644 index 0000000..405d20e --- /dev/null +++ b/spec/support/isolation_spec_helper.rb @@ -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 diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb new file mode 100644 index 0000000..dc993bd --- /dev/null +++ b/spec/support/rspec.rb @@ -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 diff --git a/spec/unit/hanami/action/after_spec.rb b/spec/unit/hanami/action/after_spec.rb new file mode 100644 index 0000000..a8fd5fe --- /dev/null +++ b/spec/unit/hanami/action/after_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/base_param_spec.rb b/spec/unit/hanami/action/base_param_spec.rb new file mode 100644 index 0000000..fd9819e --- /dev/null +++ b/spec/unit/hanami/action/base_param_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/before_spec.rb b/spec/unit/hanami/action/before_spec.rb new file mode 100644 index 0000000..bed0603 --- /dev/null +++ b/spec/unit/hanami/action/before_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/cache/directives_spec.rb b/spec/unit/hanami/action/cache/directives_spec.rb new file mode 100644 index 0000000..acf71a1 --- /dev/null +++ b/spec/unit/hanami/action/cache/directives_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/cache/non_value_directive_spec.rb b/spec/unit/hanami/action/cache/non_value_directive_spec.rb new file mode 100644 index 0000000..78651e8 --- /dev/null +++ b/spec/unit/hanami/action/cache/non_value_directive_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/cache/value_directive_spec.rb b/spec/unit/hanami/action/cache/value_directive_spec.rb new file mode 100644 index 0000000..e69575d --- /dev/null +++ b/spec/unit/hanami/action/cache/value_directive_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/cookies_spec.rb b/spec/unit/hanami/action/cookies_spec.rb new file mode 100644 index 0000000..38704a6 --- /dev/null +++ b/spec/unit/hanami/action/cookies_spec.rb @@ -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 diff --git a/test/action/exposable_test.rb b/spec/unit/hanami/action/expose_spec.rb similarity index 70% rename from test/action/exposable_test.rb rename to spec/unit/hanami/action/expose_spec.rb index 1dc2108..16a0186 100644 --- a/test/action/exposable_test.rb +++ b/spec/unit/hanami/action/expose_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/format_spec.rb b/spec/unit/hanami/action/format_spec.rb new file mode 100644 index 0000000..59ce367 --- /dev/null +++ b/spec/unit/hanami/action/format_spec.rb @@ -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 diff --git a/test/action/glue_test.rb b/spec/unit/hanami/action/glue_spec.rb similarity index 71% rename from test/action/glue_test.rb rename to spec/unit/hanami/action/glue_spec.rb index 92c883f..2f4fc29 100644 --- a/test/action/glue_test.rb +++ b/spec/unit/hanami/action/glue_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/mime_spec.rb b/spec/unit/hanami/action/mime_spec.rb new file mode 100644 index 0000000..c2a56cd --- /dev/null +++ b/spec/unit/hanami/action/mime_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/params_spec.rb b/spec/unit/hanami/action/params_spec.rb new file mode 100644 index 0000000..74cb342 --- /dev/null +++ b/spec/unit/hanami/action/params_spec.rb @@ -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 diff --git a/test/action/rack/file_test.rb b/spec/unit/hanami/action/rack/file_spec.rb similarity index 55% rename from test/action/rack/file_test.rb rename to spec/unit/hanami/action/rack/file_spec.rb index 932adf9..01a8bae 100644 --- a/test/action/rack/file_test.rb +++ b/spec/unit/hanami/action/rack/file_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/rack_spec.rb b/spec/unit/hanami/action/rack_spec.rb new file mode 100644 index 0000000..14cb3a2 --- /dev/null +++ b/spec/unit/hanami/action/rack_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/redirect_spec.rb b/spec/unit/hanami/action/redirect_spec.rb new file mode 100644 index 0000000..5c782bb --- /dev/null +++ b/spec/unit/hanami/action/redirect_spec.rb @@ -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 diff --git a/test/action/request_test.rb b/spec/unit/hanami/action/request_spec.rb similarity index 65% rename from test/action/request_test.rb rename to spec/unit/hanami/action/request_spec.rb index b75bfcf..83f6c3d 100644 --- a/test/action/request_test.rb +++ b/spec/unit/hanami/action/request_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/session_spec.rb b/spec/unit/hanami/action/session_spec.rb new file mode 100644 index 0000000..71dde0f --- /dev/null +++ b/spec/unit/hanami/action/session_spec.rb @@ -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 diff --git a/spec/unit/hanami/action/throw_spec.rb b/spec/unit/hanami/action/throw_spec.rb new file mode 100644 index 0000000..364a075 --- /dev/null +++ b/spec/unit/hanami/action/throw_spec.rb @@ -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 diff --git a/spec/unit/hanami/action_spec.rb b/spec/unit/hanami/action_spec.rb new file mode 100644 index 0000000..556e455 --- /dev/null +++ b/spec/unit/hanami/action_spec.rb @@ -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 diff --git a/spec/unit/hanami/controller/configuration_spec.rb b/spec/unit/hanami/controller/configuration_spec.rb new file mode 100644 index 0000000..964c075 --- /dev/null +++ b/spec/unit/hanami/controller/configuration_spec.rb @@ -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 diff --git a/spec/unit/hanami/controller/error_spec.rb b/spec/unit/hanami/controller/error_spec.rb new file mode 100644 index 0000000..70b2080 --- /dev/null +++ b/spec/unit/hanami/controller/error_spec.rb @@ -0,0 +1,5 @@ +RSpec.describe Hanami::Controller::Error do + it 'inherits from ::StandardError' do + expect(described_class.superclass).to eq(StandardError) + end +end diff --git a/spec/unit/hanami/controller/unknown_format_error_spec.rb b/spec/unit/hanami/controller/unknown_format_error_spec.rb new file mode 100644 index 0000000..6a65329 --- /dev/null +++ b/spec/unit/hanami/controller/unknown_format_error_spec.rb @@ -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 diff --git a/spec/unit/hanami/controller/version_spec.rb b/spec/unit/hanami/controller/version_spec.rb new file mode 100644 index 0000000..7577678 --- /dev/null +++ b/spec/unit/hanami/controller/version_spec.rb @@ -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 diff --git a/test/controller_test.rb b/spec/unit/hanami/controller_spec.rb similarity index 51% rename from test/controller_test.rb rename to spec/unit/hanami/controller_spec.rb index f4ac892..79c68ed 100644 --- a/test/controller_test.rb +++ b/spec/unit/hanami/controller_spec.rb @@ -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 diff --git a/test/action/base_params_test.rb b/test/action/base_params_test.rb deleted file mode 100644 index b91354b..0000000 --- a/test/action/base_params_test.rb +++ /dev/null @@ -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 diff --git a/test/action/callbacks_test.rb b/test/action/callbacks_test.rb deleted file mode 100644 index 8b3de37..0000000 --- a/test/action/callbacks_test.rb +++ /dev/null @@ -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 diff --git a/test/action/format_test.rb b/test/action/format_test.rb deleted file mode 100644 index b2fe521..0000000 --- a/test/action/format_test.rb +++ /dev/null @@ -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 diff --git a/test/action/params_test.rb b/test/action/params_test.rb deleted file mode 100644 index 5b36afb..0000000 --- a/test/action/params_test.rb +++ /dev/null @@ -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 diff --git a/test/action_test.rb b/test/action_test.rb deleted file mode 100644 index aa49352..0000000 --- a/test/action_test.rb +++ /dev/null @@ -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 diff --git a/test/configuration_test.rb b/test/configuration_test.rb deleted file mode 100644 index 9ff68db..0000000 --- a/test/configuration_test.rb +++ /dev/null @@ -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 diff --git a/test/cookies_test.rb b/test/cookies_test.rb deleted file mode 100644 index 2130dd0..0000000 --- a/test/cookies_test.rb +++ /dev/null @@ -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 diff --git a/test/error_test.rb b/test/error_test.rb deleted file mode 100644 index 7a8bc75..0000000 --- a/test/error_test.rb +++ /dev/null @@ -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 diff --git a/test/integration/cache_test.rb b/test/integration/cache_test.rb deleted file mode 100644 index b87f5be..0000000 --- a/test/integration/cache_test.rb +++ /dev/null @@ -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 diff --git a/test/integration/configuration_test.rb b/test/integration/configuration_test.rb deleted file mode 100644 index 50f45eb..0000000 --- a/test/integration/configuration_test.rb +++ /dev/null @@ -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 diff --git a/test/integration/framework_freeze_test.rb b/test/integration/framework_freeze_test.rb deleted file mode 100644 index 2659032..0000000 --- a/test/integration/framework_freeze_test.rb +++ /dev/null @@ -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 diff --git a/test/integration/full_stack_test.rb b/test/integration/full_stack_test.rb deleted file mode 100644 index fefce49..0000000 --- a/test/integration/full_stack_test.rb +++ /dev/null @@ -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 diff --git a/test/integration/head_test.rb b/test/integration/head_test.rb deleted file mode 100644 index 9c9a252..0000000 --- a/test/integration/head_test.rb +++ /dev/null @@ -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 diff --git a/test/integration/mime_type_test.rb b/test/integration/mime_type_test.rb deleted file mode 100644 index 148c244..0000000 --- a/test/integration/mime_type_test.rb +++ /dev/null @@ -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 diff --git a/test/integration/rack_exception_test.rb b/test/integration/rack_exception_test.rb deleted file mode 100644 index 6d05c1c..0000000 --- a/test/integration/rack_exception_test.rb +++ /dev/null @@ -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 diff --git a/test/integration/routing_test.rb b/test/integration/routing_test.rb deleted file mode 100644 index 7ccdeb3..0000000 --- a/test/integration/routing_test.rb +++ /dev/null @@ -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 diff --git a/test/integration/send_file_test.rb b/test/integration/send_file_test.rb deleted file mode 100644 index e3ed41b..0000000 --- a/test/integration/send_file_test.rb +++ /dev/null @@ -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 diff --git a/test/integration/use_test.rb b/test/integration/use_test.rb deleted file mode 100644 index 47a7ee7..0000000 --- a/test/integration/use_test.rb +++ /dev/null @@ -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 diff --git a/test/method_visibility_test.rb b/test/method_visibility_test.rb deleted file mode 100644 index 4ce0ef5..0000000 --- a/test/method_visibility_test.rb +++ /dev/null @@ -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 diff --git a/test/mime_type_test.rb b/test/mime_type_test.rb deleted file mode 100644 index a2a37ec..0000000 --- a/test/mime_type_test.rb +++ /dev/null @@ -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 diff --git a/test/rack_test.rb b/test/rack_test.rb deleted file mode 100644 index 4dfc03a..0000000 --- a/test/rack_test.rb +++ /dev/null @@ -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 diff --git a/test/redirect_test.rb b/test/redirect_test.rb deleted file mode 100644 index 0553f3c..0000000 --- a/test/redirect_test.rb +++ /dev/null @@ -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 diff --git a/test/session_test.rb b/test/session_test.rb deleted file mode 100644 index 44a0d90..0000000 --- a/test/session_test.rb +++ /dev/null @@ -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 diff --git a/test/throw_test.rb b/test/throw_test.rb deleted file mode 100644 index 144c3e6..0000000 --- a/test/throw_test.rb +++ /dev/null @@ -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 diff --git a/test/unit/cache/directives_test.rb b/test/unit/cache/directives_test.rb deleted file mode 100644 index db29ba4..0000000 --- a/test/unit/cache/directives_test.rb +++ /dev/null @@ -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 diff --git a/test/version_test.rb b/test/version_test.rb deleted file mode 100644 index 56e7b52..0000000 --- a/test/version_test.rb +++ /dev/null @@ -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