Decouple specs from global state to isolated tests

This commit is contained in:
Johannes Müller 2017-07-18 00:42:05 +02:00 committed by sdogruyol
parent 29b18c927c
commit 2e42b3f48c
19 changed files with 336 additions and 304 deletions

View file

@ -1,102 +1,72 @@
require "./spec_helper" require "./spec_helper"
describe "Context" do describe "Context" do
context "headers" do it "sets content type" do
it "sets content type" do app = Kemal::Base.new
get "/" do |env| app.get "/" do |env|
env.response.content_type = "application/json" env.response.content_type = "application/json"
"Hello" "Hello"
end
request = HTTP::Request.new("GET", "/")
client_response = call_request_on_app(request)
client_response.headers["Content-Type"].should eq("application/json")
end
it "parses headers" do
get "/" do |env|
name = env.request.headers["name"]
"Hello #{name}"
end
headers = HTTP::Headers.new
headers["name"] = "kemal"
request = HTTP::Request.new("GET", "/", headers)
client_response = call_request_on_app(request)
client_response.body.should eq "Hello kemal"
end
it "sets response headers" do
get "/" do |env|
env.response.headers.add "Accept-Language", "tr"
end
request = HTTP::Request.new("GET", "/")
client_response = call_request_on_app(request)
client_response.headers["Accept-Language"].should eq "tr"
end end
request = HTTP::Request.new("GET", "/")
client_response = call_request_on_app(app, request)
client_response.headers["Content-Type"].should eq("application/json")
end end
context "storage" do it "parses headers" do
it "can store primitive types" do app = Kemal::Base.new
before_get "/" do |env| app.get "/" do |env|
env.set "before_get", "Kemal" name = env.request.headers["name"]
env.set "before_get_int", 123 "Hello #{name}"
env.set "before_get_float", 3.5 end
end headers = HTTP::Headers.new
headers["name"] = "kemal"
request = HTTP::Request.new("GET", "/", headers)
client_response = call_request_on_app(app, request)
client_response.body.should eq "Hello kemal"
end
get "/" do |env| it "sets response headers" do
{ app = Kemal::Base.new
before_get: env.get("before_get"), app.get "/" do |env|
before_get_int: env.get("before_get_int"), env.response.headers.add "Accept-Language", "tr"
before_get_float: env.get("before_get_float"), end
} request = HTTP::Request.new("GET", "/")
end client_response = call_request_on_app(app, request)
client_response.headers["Accept-Language"].should eq "tr"
end
request = HTTP::Request.new("GET", "/") it "can store variables" do
io = IO::Memory.new app = Kemal::Base.new
response = HTTP::Server::Response.new(io) app.before_get "/" do |env|
context = HTTP::Server::Context.new(request, response) t = TestContextStorageType.new
Kemal::FilterHandler::INSTANCE.call(context) t.id = 32
Kemal::RouteHandler::INSTANCE.call(context) a = AnotherContextStorageType.new
env.set "key", "value"
context.get("before_get").should eq "Kemal" env.set "before_get", "Kemal"
context.get("before_get_int").should eq 123 env.set "before_get_int", 123
context.get("before_get_float").should eq 3.5 env.set "before_get_context_test", t
env.set "another_context_test", a
env.set "before_get_float", 3.5
end end
it "can store custom types" do app.get "/" do |env|
before_get "/" do |env| env.set "key", "value"
t = TestContextStorageType.new {
t.id = 32 key: env.get("key"),
a = AnotherContextStorageType.new before_get: env.get("before_get"),
before_get_int: env.get("before_get_int"),
env.set "before_get_context_test", t before_get_float: env.get("before_get_float"),
env.set "another_context_test", a before_get_context_test: env.get("before_get_context_test"),
end }
get "/" do |env|
{
before_get_context_test: env.get("before_get_context_test"),
another_context_test: env.get("another_context_test"),
}
end
request = HTTP::Request.new("GET", "/")
io = IO::Memory.new
response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response)
Kemal::FilterHandler::INSTANCE.call(context)
Kemal::RouteHandler::INSTANCE.call(context)
context.get("before_get_context_test").as(TestContextStorageType).id.should eq 32
context.get("another_context_test").as(AnotherContextStorageType).name.should eq "kemal-context"
end end
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
io = IO::Memory.new io = IO::Memory.new
response = HTTP::Server::Response.new(io) response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response) context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application context.app = app
Kemal.application.filter_handler.call(context) app.filter_handler.call(context)
Kemal.application.route_handler.call(context) app.route_handler.call(context)
context.store["key"].should eq "value" context.store["key"].should eq "value"
context.store["before_get"].should eq "Kemal" context.store["before_get"].should eq "Kemal"
context.store["before_get_int"].should eq 123 context.store["before_get_int"].should eq 123

58
spec/dsl_helper.cr Normal file
View file

@ -0,0 +1,58 @@
require "./spec_helper"
require "../src/kemal/dsl"
include Kemal
class CustomLogHandler < Kemal::BaseLogHandler
def call(env)
call_next env
end
def write(message)
end
end
class TestContextStorageType
property id
@id = 1
def to_s
@id
end
end
class AnotherContextStorageType
property name
@name = "kemal-context"
end
add_context_storage_type(TestContextStorageType)
add_context_storage_type(AnotherContextStorageType)
def create_request_and_return_io(handler, request)
io = IO::Memory.new
response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application
handler.call(context)
response.close
io.rewind
io
end
def call_request_on_app(request)
call_request_on_app(Kemal.application, request)
end
def build_main_handler
build_main_handler(Kemal.application)
end
Spec.before_each do
config = Kemal.config
config.env = "development"
end
Spec.after_each do
Kemal.application.clear
end

View file

@ -1,18 +1,13 @@
require "./spec_helper" require "./dsl_helper"
private INSTANCE = Kemal::ExceptionHandler.new
describe "Kemal::ExceptionHandler" do describe "Kemal::ExceptionHandler" do
it "renders 404 on route not found" do it "renders 404 on route not found" do
get "/" do
"Hello"
end
request = HTTP::Request.new("GET", "/asd") request = HTTP::Request.new("GET", "/asd")
io = IO::Memory.new io = IO::Memory.new
response = HTTP::Server::Response.new(io) response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response) context = HTTP::Server::Context.new(request, response)
INSTANCE.call(context) subject = Kemal::ExceptionHandler.new(Kemal::Base.new)
subject.call(context)
response.close response.close
io.rewind io.rewind
response = HTTP::Client::Response.from_io(io, decompress: false) response = HTTP::Client::Response.from_io(io, decompress: false)
@ -20,19 +15,21 @@ describe "Kemal::ExceptionHandler" do
end end
it "renders custom error" do it "renders custom error" do
error 403 do
"403 error"
end
get "/" do |env|
env.response.status_code = 403
end
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
io = IO::Memory.new io = IO::Memory.new
response = HTTP::Server::Response.new(io) response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response) context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application app = Kemal::Base.new
INSTANCE.next = Kemal::RouteHandler.new app.error 403 do
INSTANCE.call(context) "403 error"
end
app.get "/" do |env|
env.response.status_code = 403
end
context.app = app
subject = Kemal::ExceptionHandler.new(app)
subject.next = Kemal::RouteHandler.new
subject.call(context)
response.close response.close
io.rewind io.rewind
response = HTTP::Client::Response.from_io(io, decompress: false) response = HTTP::Client::Response.from_io(io, decompress: false)
@ -42,19 +39,21 @@ describe "Kemal::ExceptionHandler" do
end end
it "renders custom 500 error" do it "renders custom 500 error" do
error 500 do
"Something happened"
end
get "/" do |env|
env.response.status_code = 500
end
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
io = IO::Memory.new io = IO::Memory.new
response = HTTP::Server::Response.new(io) response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response) context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application app = Kemal::Base.new
INSTANCE.next = Kemal::RouteHandler.new app.error 500 do |env|
INSTANCE.call(context) "Something happened"
end
app.get "/" do |env|
env.response.status_code = 500
end
context.app = app
subject = Kemal::ExceptionHandler.new(app)
subject.next = Kemal::RouteHandler.new
subject.call(context)
response.close response.close
io.rewind io.rewind
response = HTTP::Client::Response.from_io(io, decompress: false) response = HTTP::Client::Response.from_io(io, decompress: false)
@ -64,20 +63,22 @@ describe "Kemal::ExceptionHandler" do
end end
it "keeps the specified error Content-Type" do it "keeps the specified error Content-Type" do
error 500 do
"Something happened"
end
get "/" do |env|
env.response.content_type = "application/json"
env.response.status_code = 500
end
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
io = IO::Memory.new io = IO::Memory.new
response = HTTP::Server::Response.new(io) response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response) context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application app = Kemal::Base.new
INSTANCE.next = Kemal::RouteHandler.new app.error 500 do |env|
INSTANCE.call(context) "Something happened"
end
app.get "/" do |env|
env.response.content_type = "application/json"
env.response.status_code = 500
end
context.app = app
subject = Kemal::ExceptionHandler.new(app)
subject.next = Kemal::RouteHandler.new
subject.call(context)
response.close response.close
io.rewind io.rewind
response = HTTP::Client::Response.from_io(io, decompress: false) response = HTTP::Client::Response.from_io(io, decompress: false)
@ -87,20 +88,22 @@ describe "Kemal::ExceptionHandler" do
end end
it "renders custom error with env and error" do it "renders custom error with env and error" do
error 500 do |_, err|
err.message
end
get "/" do |env|
env.response.content_type = "application/json"
env.response.status_code = 500
end
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
io = IO::Memory.new io = IO::Memory.new
response = HTTP::Server::Response.new(io) response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response) context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application app = Kemal::Base.new
INSTANCE.next = Kemal::RouteHandler.new app.error 500 do |env, err|
INSTANCE.call(context) err.message
end
app.get "/" do |env|
env.response.content_type = "application/json"
env.response.status_code = 500
end
context.app = app
subject = Kemal::ExceptionHandler.new(Kemal::Base.new)
subject.next = Kemal::RouteHandler.new
subject.call(context)
response.close response.close
io.rewind io.rewind
response = HTTP::Client::Response.from_io(io, decompress: false) response = HTTP::Client::Response.from_io(io, decompress: false)

View file

@ -78,81 +78,88 @@ describe "Handler" do
filter_middleware._add_route_filter("GET", "/", :before) do |env| filter_middleware._add_route_filter("GET", "/", :before) do |env|
env.response << " so" env.response << " so"
end end
Kemal.application.add_filter_handler filter_middleware app = Kemal::Base.new
app.add_filter_handler filter_middleware
add_handler CustomTestHandler.new app.add_handler CustomTestHandler.new
get "/" do app.get "/" do |env|
" Great" " Great"
end end
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.status_code.should eq(200) client_response.status_code.should eq(200)
client_response.body.should eq("Kemal is so Great") client_response.body.should eq("Kemal is so Great")
end end
it "runs specified only_routes in middleware" do it "runs specified only_routes in middleware" do
get "/only" do app = Kemal::Base.new
app.get "/only" do |env|
"Get" "Get"
end end
add_handler OnlyHandler.new app.add_handler OnlyHandler.new
request = HTTP::Request.new("GET", "/only") request = HTTP::Request.new("GET", "/only")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.body.should eq "OnlyGet" client_response.body.should eq "OnlyGet"
end end
it "doesn't run specified exclude_routes in middleware" do it "doesn't run specified exclude_routes in middleware" do
get "/" do app = Kemal::Base.new
app.get "/" do |env|
"Get" "Get"
end end
get "/exclude" do app.get "/exclude" do
"Exclude" "Exclude"
end end
add_handler ExcludeHandler.new app.add_handler ExcludeHandler.new
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.body.should eq "ExcludeGet" client_response.body.should eq "ExcludeGet"
end end
it "runs specified only_routes with method in middleware" do it "runs specified only_routes with method in middleware" do
post "/only" do app = Kemal::Base.new
app.post "/only" do
"Post" "Post"
end end
get "/only" do app.get "/only" do
"Get" "Get"
end end
add_handler PostOnlyHandler.new app.add_handler PostOnlyHandler.new
request = HTTP::Request.new("POST", "/only") request = HTTP::Request.new("POST", "/only")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.body.should eq "OnlyPost" client_response.body.should eq "OnlyPost"
end end
it "doesn't run specified exclude_routes with method in middleware" do it "doesn't run specified exclude_routes with method in middleware" do
post "/exclude" do app = Kemal::Base.new
app.post "/exclude" do
"Post" "Post"
end end
post "/only" do app.post "/only" do
"Post" "Post"
end end
add_handler PostOnlyHandler.new app.add_handler PostOnlyHandler.new
add_handler PostExcludeHandler.new app.add_handler PostExcludeHandler.new
request = HTTP::Request.new("POST", "/only") request = HTTP::Request.new("POST", "/only")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.body.should eq "OnlyExcludePost" client_response.body.should eq "OnlyExcludePost"
end end
it "adds a handler at given position" do it "adds a handler at given position" do
post_handler = PostOnlyHandler.new post_handler = PostOnlyHandler.new
add_handler post_handler, 1 app = Kemal::Base.new
Kemal.application.setup app.add_handler post_handler, 1
Kemal.application.handlers[1].should eq post_handler app.setup
app.handlers[1].should eq post_handler
end end
it "assigns custom handlers" do it "assigns custom handlers" do
post_only_handler = PostOnlyHandler.new post_only_handler = PostOnlyHandler.new
post_exclude_handler = PostExcludeHandler.new post_exclude_handler = PostExcludeHandler.new
Kemal.application.handlers = [post_only_handler, post_exclude_handler] app = Kemal::Base.new
Kemal.application.handlers.should eq [post_only_handler, post_exclude_handler] app.handlers = [post_only_handler, post_exclude_handler]
app.handlers.should eq [post_only_handler, post_exclude_handler]
end end
it "is able to use %w in macros" do it "is able to use %w in macros" do

View file

@ -10,9 +10,10 @@ describe "Macros" do
describe "#add_handler" do describe "#add_handler" do
it "adds a custom handler" do it "adds a custom handler" do
add_handler CustomTestHandler.new app = Kemal::Application.new
Kemal.application.setup app.add_handler CustomTestHandler.new
Kemal.application.handlers.size.should eq 8 app.setup
app.handlers.size.should eq 8
end end
end end
@ -23,7 +24,6 @@ describe "Macros" do
end end
it "sets a custom logger" do it "sets a custom logger" do
config = Kemal.config
logger CustomLogHandler.new logger CustomLogHandler.new
Kemal.application.logger.should be_a(CustomLogHandler) Kemal.application.logger.should be_a(CustomLogHandler)
end end
@ -31,32 +31,34 @@ describe "Macros" do
describe "#halt" do describe "#halt" do
it "can break block with halt macro" do it "can break block with halt macro" do
get "/non-breaking" do app = Kemal::Base.new
app.get "/non-breaking" do |env|
"hello" "hello"
"world" "world"
end end
request = HTTP::Request.new("GET", "/non-breaking") request = HTTP::Request.new("GET", "/non-breaking")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.status_code.should eq(200) client_response.status_code.should eq(200)
client_response.body.should eq("world") client_response.body.should eq("world")
get "/breaking" do |env| app.get "/breaking" do |env|
halt env, 404, "hello" halt env, 404, "hello"
"world" "world"
end end
request = HTTP::Request.new("GET", "/breaking") request = HTTP::Request.new("GET", "/breaking")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.status_code.should eq(404) client_response.status_code.should eq(404)
client_response.body.should eq("hello") client_response.body.should eq("hello")
end end
it "can break block with halt macro using default values" do it "can break block with halt macro using default values" do
get "/" do |env| app = Kemal::Base.new
app.get "/" do |env|
halt env halt env
"world" "world"
end end
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.status_code.should eq(200) client_response.status_code.should eq(200)
client_response.body.should eq("") client_response.body.should eq("")
end end
@ -64,7 +66,8 @@ describe "Macros" do
describe "#headers" do describe "#headers" do
it "can add headers" do it "can add headers" do
get "/headers" do |env| app = Kemal::Base.new
app.get "/headers" do |env|
env.response.headers.add "Content-Type", "image/png" env.response.headers.add "Content-Type", "image/png"
headers env, { headers env, {
"Access-Control-Allow-Origin" => "*", "Access-Control-Allow-Origin" => "*",
@ -72,7 +75,7 @@ describe "Macros" do
} }
end end
request = HTTP::Request.new("GET", "/headers") request = HTTP::Request.new("GET", "/headers")
response = call_request_on_app(request) response = call_request_on_app(app, request)
response.headers["Access-Control-Allow-Origin"].should eq("*") response.headers["Access-Control-Allow-Origin"].should eq("*")
response.headers["Content-Type"].should eq("text/plain") response.headers["Content-Type"].should eq("text/plain")
end end
@ -80,36 +83,39 @@ describe "Macros" do
describe "#send_file" do describe "#send_file" do
it "sends file with given path and default mime-type" do it "sends file with given path and default mime-type" do
get "/" do |env| app = Kemal::Base.new
app.get "/" do |env|
send_file env, "./spec/asset/hello.ecr" send_file env, "./spec/asset/hello.ecr"
end end
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
response = call_request_on_app(request) response = call_request_on_app(app, request)
response.status_code.should eq(200) response.status_code.should eq(200)
response.headers["Content-Type"].should eq("application/octet-stream") response.headers["Content-Type"].should eq("application/octet-stream")
response.headers["Content-Length"].should eq("18") response.headers["Content-Length"].should eq("18")
end end
it "sends file with given path and given mime-type" do it "sends file with given path and given mime-type" do
get "/" do |env| app = Kemal::Base.new
app.get "/" do |env|
send_file env, "./spec/asset/hello.ecr", "image/jpeg" send_file env, "./spec/asset/hello.ecr", "image/jpeg"
end end
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
response = call_request_on_app(request) response = call_request_on_app(app, request)
response.status_code.should eq(200) response.status_code.should eq(200)
response.headers["Content-Type"].should eq("image/jpeg") response.headers["Content-Type"].should eq("image/jpeg")
response.headers["Content-Length"].should eq("18") response.headers["Content-Length"].should eq("18")
end end
it "sends file with binary stream" do it "sends file with binary stream" do
get "/" do |env| app = Kemal::Base.new
app.get "/" do |env|
send_file env, "Serdar".to_slice send_file env, "Serdar".to_slice
end end
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
response = call_request_on_app(request) response = call_request_on_app(app, request)
response.status_code.should eq(200) response.status_code.should eq(200)
response.headers["Content-Type"].should eq("application/octet-stream") response.headers["Content-Type"].should eq("application/octet-stream")
response.headers["Content-Length"].should eq("6") response.headers["Content-Length"].should eq("6")

View file

@ -1,4 +1,4 @@
require "./spec_helper" require "./dsl_helper"
describe "ParamParser" do describe "ParamParser" do
it "parses query params" do it "parses query params" do

View file

@ -1,4 +1,4 @@
require "./spec_helper" require "./dsl_helper"
describe "Kemal::RouteHandler" do describe "Kemal::RouteHandler" do
it "routes" do it "routes" do

View file

@ -1,4 +1,4 @@
require "./spec_helper" require "./dsl_helper"
describe "Route" do describe "Route" do
describe "match?" do describe "match?" do

View file

@ -1,4 +1,4 @@
require "./spec_helper" require "./dsl_helper"
private def run(code) private def run(code)
code = <<-CR code = <<-CR

View file

@ -1,89 +1,24 @@
require "spec" require "spec"
require "../src/**" require "../src/kemal"
require "../src/kemal/base"
require "../src/kemal/dsl"
include Kemal def call_request_on_app(app, request)
class CustomLogHandler < Kemal::BaseLogHandler
def call(env)
call_next env
end
def write(message)
end
end
class TestContextStorageType
property id
@id = 1
def to_s
@id
end
end
class AnotherContextStorageType
property name
@name = "kemal-context"
end
add_context_storage_type(TestContextStorageType)
add_context_storage_type(AnotherContextStorageType)
def create_request_and_return_io_and_context(handler, request)
io = IO::Memory.new io = IO::Memory.new
response = HTTP::Server::Response.new(io) response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response) context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application main_handler = build_main_handler(app)
handler.call(context)
response.close
io.rewind
{io, context}
end
def create_ws_request_and_return_io_and_context(handler, request)
io = IO::Memory.new
response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application
begin
handler.call context
rescue IO::Error
# Raises because the IO::Memory is empty
end
io.rewind
{io, context}
end
def call_request_on_app(request)
io = IO::Memory.new
response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response)
main_handler = build_main_handler
main_handler.call context main_handler.call context
response.close response.close
io.rewind io.rewind
HTTP::Client::Response.from_io(io, decompress: false) HTTP::Client::Response.from_io(io, decompress: false)
end end
def build_main_handler def build_main_handler(app)
Kemal.application.setup app.setup
main_handler = Kemal.application.handlers.first main_handler = app.handlers.first
current_handler = main_handler current_handler = main_handler
Kemal.application.handlers.each_with_index do |handler, index| app.handlers.each_with_index do |handler, index|
current_handler.next = handler current_handler.next = handler
current_handler = handler current_handler = handler
end end
main_handler main_handler
end end
Spec.before_each do
config = Kemal.config
config.env = "development"
config.logging = false
end
Spec.after_each do
Kemal.application.clear
end

View file

@ -1,17 +1,22 @@
require "./spec_helper" require "./spec_helper"
private def handle(request, fallthrough = true) private def handle(request, config = default_config, fallthrough = true)
io = IO::Memory.new io = IO::Memory.new
response = HTTP::Server::Response.new(io) response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response) context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application handler = Kemal::StaticFileHandler.new config, fallthrough
handler = Kemal::StaticFileHandler.new "#{__DIR__}/static", fallthrough
handler.call context handler.call context
response.close response.close
io.rewind io.rewind
HTTP::Client::Response.from_io(io) HTTP::Client::Response.from_io(io)
end end
private def default_config
Kemal::Config.new.tap do |config|
config.public_folder = "#{__DIR__}/static"
end
end
describe Kemal::StaticFileHandler do describe Kemal::StaticFileHandler do
file = File.open "#{__DIR__}/static/dir/test.txt" file = File.open "#{__DIR__}/static/dir/test.txt"
file_size = file.size file_size = file.size
@ -37,38 +42,43 @@ describe Kemal::StaticFileHandler do
end end
it "should not list directory's entries" do it "should not list directory's entries" do
serve_static({"gzip" => true, "dir_listing" => false}) config = default_config
response = handle HTTP::Request.new("GET", "/dir/") config.serve_static = {"gzip" => true, "dir_listing" => false}
response = handle HTTP::Request.new("GET", "/dir/"), config
response.status_code.should eq(404) response.status_code.should eq(404)
end end
it "should list directory's entries when config is set" do it "should list directory's entries when config is set" do
serve_static({"gzip" => true, "dir_listing" => true}) config = default_config
response = handle HTTP::Request.new("GET", "/dir/") config.serve_static = {"gzip" => true, "dir_listing" => true}
response = handle HTTP::Request.new("GET", "/dir/"), config
response.status_code.should eq(200) response.status_code.should eq(200)
response.body.should match(/test.txt/) response.body.should match(/test.txt/)
end end
it "should gzip a file if config is true, headers accept gzip and file is > 880 bytes" do it "should gzip a file if config is true, headers accept gzip and file is > 880 bytes" do
serve_static({"gzip" => true, "dir_listing" => true}) config = default_config
config.serve_static = {"gzip" => true, "dir_listing" => true}
headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"} headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"}
response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers) response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers), config
response.status_code.should eq(200) response.status_code.should eq(200)
response.headers["Content-Encoding"].should eq "gzip" response.headers["Content-Encoding"].should eq "gzip"
end end
it "should not gzip a file if config is true, headers accept gzip and file is < 880 bytes" do it "should not gzip a file if config is true, headers accept gzip and file is < 880 bytes" do
serve_static({"gzip" => true, "dir_listing" => true}) config = default_config
config.serve_static = {"gzip" => true, "dir_listing" => true}
headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"} headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"}
response = handle HTTP::Request.new("GET", "/dir/test.txt", headers) response = handle HTTP::Request.new("GET", "/dir/test.txt", headers), config
response.status_code.should eq(200) response.status_code.should eq(200)
response.headers["Content-Encoding"]?.should be_nil response.headers["Content-Encoding"]?.should be_nil
end end
it "should not gzip a file if config is false, headers accept gzip and file is > 880 bytes" do it "should not gzip a file if config is false, headers accept gzip and file is > 880 bytes" do
serve_static({"gzip" => false, "dir_listing" => true}) config = default_config
config.serve_static = {"gzip" => false, "dir_listing" => true}
headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"} headers = HTTP::Headers{"Accept-Encoding" => "gzip, deflate, sdch, br"}
response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers) response = handle HTTP::Request.new("GET", "/dir/bigger.txt", headers), config
response.status_code.should eq(200) response.status_code.should eq(200)
response.headers["Content-Encoding"]?.should be_nil response.headers["Content-Encoding"]?.should be_nil
end end
@ -97,7 +107,7 @@ describe Kemal::StaticFileHandler do
%w(POST PUT DELETE).each do |method| %w(POST PUT DELETE).each do |method|
response = handle HTTP::Request.new(method, "/dir/test.txt") response = handle HTTP::Request.new(method, "/dir/test.txt")
response.status_code.should eq(404) response.status_code.should eq(404)
response = handle HTTP::Request.new(method, "/dir/test.txt"), false response = handle HTTP::Request.new(method, "/dir/test.txt"), fallthrough: false
response.status_code.should eq(405) response.status_code.should eq(405)
response.headers["Allow"].should eq("GET, HEAD") response.headers["Allow"].should eq("GET, HEAD")
end end
@ -133,22 +143,21 @@ describe Kemal::StaticFileHandler do
end end
it "should handle setting custom headers" do it "should handle setting custom headers" do
headers = Proc(HTTP::Server::Response, String, File::Info, Void).new do |response, path, stat| config = default_config
config.static_headers = Proc(HTTP::Server::Response, String, File::Info, Void).new do |response, path, stat|
if path =~ /\.html$/ if path =~ /\.html$/
response.headers.add("Access-Control-Allow-Origin", "*") response.headers.add("Access-Control-Allow-Origin", "*")
end end
response.headers.add("Content-Size", stat.size.to_s) response.headers.add("Content-Size", stat.size.to_s)
end end
static_headers(&headers) response = handle HTTP::Request.new("GET", "/dir/test.txt"), config
response = handle HTTP::Request.new("GET", "/dir/test.txt")
response.headers.has_key?("Access-Control-Allow-Origin").should be_false response.headers.has_key?("Access-Control-Allow-Origin").should be_false
response.headers["Content-Size"].should eq( response.headers["Content-Size"].should eq(
File.info("#{__DIR__}/static/dir/test.txt").size.to_s File.info("#{__DIR__}/static/dir/test.txt").size.to_s
) )
response = handle HTTP::Request.new("GET", "/dir/index.html") response = handle HTTP::Request.new("GET", "/dir/index.html"), config
response.headers["Access-Control-Allow-Origin"].should eq("*") response.headers["Access-Control-Allow-Origin"].should eq("*")
end end
end end

View file

@ -1,4 +1,4 @@
require "./spec_helper" require "./dsl_helper"
macro render_with_base_and_layout(filename) macro render_with_base_and_layout(filename)
render "spec/asset/#{{{filename}}}", "spec/asset/layout.ecr" render "spec/asset/#{{{filename}}}", "spec/asset/layout.ecr"
@ -6,56 +6,61 @@ end
describe "Views" do describe "Views" do
it "renders file" do it "renders file" do
get "/view/:name" do |env| app = Kemal::Base.new
app.get "/view/:name" do |env|
name = env.params.url["name"] name = env.params.url["name"]
render "spec/asset/hello.ecr" render "spec/asset/hello.ecr"
end end
request = HTTP::Request.new("GET", "/view/world") request = HTTP::Request.new("GET", "/view/world")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.body.should contain("Hello world") client_response.body.should contain("Hello world")
end end
it "renders file with dynamic variables" do it "renders file with dynamic variables" do
get "/view/:name" do |env| app = Kemal::Base.new
app.get "/view/:name" do |env|
name = env.params.url["name"] name = env.params.url["name"]
render_with_base_and_layout "hello.ecr" render_with_base_and_layout "hello.ecr"
end end
request = HTTP::Request.new("GET", "/view/world") request = HTTP::Request.new("GET", "/view/world")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.body.should contain("Hello world") client_response.body.should contain("Hello world")
end end
it "renders layout" do it "renders layout" do
get "/view/:name" do |env| app = Kemal::Base.new
app.get "/view/:name" do |env|
name = env.params.url["name"] name = env.params.url["name"]
render "spec/asset/hello.ecr", "spec/asset/layout.ecr" render "spec/asset/hello.ecr", "spec/asset/layout.ecr"
end end
request = HTTP::Request.new("GET", "/view/world") request = HTTP::Request.new("GET", "/view/world")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.body.should contain("<html>Hello world") client_response.body.should contain("<html>Hello world")
end end
it "renders layout with variables" do it "renders layout with variables" do
get "/view/:name" do |env| app = Kemal::Base.new
app.get "/view/:name" do |env|
name = env.params.url["name"] name = env.params.url["name"]
var1 = "serdar" var1 = "serdar"
var2 = "kemal" var2 = "kemal"
render "spec/asset/hello_with_content_for.ecr", "spec/asset/layout_with_yield_and_vars.ecr" render "spec/asset/hello_with_content_for.ecr", "spec/asset/layout_with_yield_and_vars.ecr"
end end
request = HTTP::Request.new("GET", "/view/world") request = HTTP::Request.new("GET", "/view/world")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.body.should contain("Hello world") client_response.body.should contain("Hello world")
client_response.body.should contain("serdar") client_response.body.should contain("serdar")
client_response.body.should contain("kemal") client_response.body.should contain("kemal")
end end
it "renders layout with content_for" do it "renders layout with content_for" do
get "/view/:name" do |env| app = Kemal::Base.new
app.get "/view/:name" do |env|
name = env.params.url["name"] name = env.params.url["name"]
render "spec/asset/hello_with_content_for.ecr", "spec/asset/layout_with_yield.ecr" render "spec/asset/hello_with_content_for.ecr", "spec/asset/layout_with_yield.ecr"
end end
request = HTTP::Request.new("GET", "/view/world") request = HTTP::Request.new("GET", "/view/world")
client_response = call_request_on_app(request) client_response = call_request_on_app(app, request)
client_response.body.should contain("Hello world") client_response.body.should contain("Hello world")
client_response.body.should contain("<h1>Hello from otherside</h1>") client_response.body.should contain("<h1>Hello from otherside</h1>")
end end

View file

@ -1,10 +1,24 @@
require "./spec_helper" require "./spec_helper"
private def create_ws_request_and_return_io(handler, request, app)
io = IO::Memory.new
response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response)
context.app = app
begin
handler.call context
rescue IO::Error
# Raises because the IO::Memory is empty
end
io
end
describe "Kemal::WebSocketHandler" do describe "Kemal::WebSocketHandler" do
it "doesn't match on wrong route" do it "doesn't match on wrong route" do
app = Kemal::Base.new
handler = Kemal::WebSocketHandler.new handler = Kemal::WebSocketHandler.new
handler.next = Kemal::RouteHandler.new handler.next = Kemal::RouteHandler.new
ws "/" { } app.ws "/" { }
headers = HTTP::Headers{ headers = HTTP::Headers{
"Upgrade" => "websocket", "Upgrade" => "websocket",
"Connection" => "Upgrade", "Connection" => "Upgrade",
@ -14,7 +28,7 @@ describe "Kemal::WebSocketHandler" do
io = IO::Memory.new io = IO::Memory.new
response = HTTP::Server::Response.new(io) response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response) context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application context.app = app
expect_raises(Kemal::Exceptions::RouteNotFound) do expect_raises(Kemal::Exceptions::RouteNotFound) do
handler.call context handler.call context
@ -22,9 +36,10 @@ describe "Kemal::WebSocketHandler" do
end end
it "matches on given route" do it "matches on given route" do
app = Kemal::Base.new
handler = Kemal::WebSocketHandler.new handler = Kemal::WebSocketHandler.new
ws "/" { |socket, context| socket.send("Match") } app.ws "/" { |socket, context| socket.send("Match") }
ws "/no_match" { |socket, context| socket.send "No Match" } app.ws "/no_match" { |socket, context| socket.send "No Match" }
headers = HTTP::Headers{ headers = HTTP::Headers{
"Upgrade" => "websocket", "Upgrade" => "websocket",
"Connection" => "Upgrade", "Connection" => "Upgrade",
@ -33,13 +48,14 @@ describe "Kemal::WebSocketHandler" do
} }
request = HTTP::Request.new("GET", "/", headers) request = HTTP::Request.new("GET", "/", headers)
io_with_context = create_ws_request_and_return_io_and_context(handler, request)[0] io_with_context = create_ws_request_and_return_io(handler, request, app)
io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n\x81\u0005Match") io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n\x81\u0005Match")
end end
it "fetches named url parameters" do it "fetches named url parameters" do
app = Kemal::Base.new
handler = Kemal::WebSocketHandler.new handler = Kemal::WebSocketHandler.new
ws "/:id" { |s, c| c.params.url["id"] } app.ws "/:id" { |s, c| c.params.url["id"] }
headers = HTTP::Headers{ headers = HTTP::Headers{
"Upgrade" => "websocket", "Upgrade" => "websocket",
"Connection" => "Upgrade", "Connection" => "Upgrade",
@ -47,20 +63,21 @@ describe "Kemal::WebSocketHandler" do
"Sec-WebSocket-Version" => "13", "Sec-WebSocket-Version" => "13",
} }
request = HTTP::Request.new("GET", "/1234", headers) request = HTTP::Request.new("GET", "/1234", headers)
io_with_context = create_ws_request_and_return_io_and_context(handler, request)[0] io_with_context = create_ws_request_and_return_io(handler, request, app)
io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n") io_with_context.to_s.should eq("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n")
end end
it "matches correct verb" do it "matches correct verb" do
app = Kemal::Base.new
handler = Kemal::WebSocketHandler.new handler = Kemal::WebSocketHandler.new
handler.next = Kemal::RouteHandler.new handler.next = Kemal::RouteHandler.new
ws "/" { } app.ws "/" { }
get "/" { "get" } app.get "/" { "get" }
request = HTTP::Request.new("GET", "/") request = HTTP::Request.new("GET", "/")
io = IO::Memory.new io = IO::Memory.new
response = HTTP::Server::Response.new(io) response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response) context = HTTP::Server::Context.new(request, response)
context.app = Kemal.application context.app = app
handler.call(context) handler.call(context)
response.close response.close
io.rewind io.rewind

View file

@ -99,4 +99,8 @@ class Kemal::Base
raise "Kemal is already stopped." raise "Kemal is already stopped."
end end
end end
def log(message)
logger.write "#{message}\n"
end
end end

View file

@ -71,7 +71,7 @@ class Kemal::Base
private def setup_error_handler private def setup_error_handler
if @config.always_rescue? if @config.always_rescue?
error_handler = @error_handler ||= Kemal::ExceptionHandler.new error_handler = @error_handler ||= Kemal::ExceptionHandler.new(self)
@handlers.insert(@handler_position, error_handler) @handlers.insert(@handler_position, error_handler)
@handler_position += 1 @handler_position += 1
end end
@ -79,7 +79,7 @@ class Kemal::Base
private def setup_static_file_handler private def setup_static_file_handler
if @config.serve_static.is_a?(Hash) if @config.serve_static.is_a?(Hash)
@handlers.insert(@handler_position, Kemal::StaticFileHandler.new(@config.public_folder)) @handlers.insert(@handler_position, Kemal::StaticFileHandler.new(@config))
@handler_position += 1 @handler_position += 1
end end
end end

View file

@ -57,6 +57,10 @@ module Kemal
def extra_options(&@extra_options : OptionParser ->) def extra_options(&@extra_options : OptionParser ->)
end end
def serve_static?(key)
(h = @serve_static).is_a?(Hash) && h[key]? == true
end
# Create a config with default values # Create a config with default values
def self.default def self.default
new new

View file

@ -3,6 +3,13 @@ module Kemal
class ExceptionHandler class ExceptionHandler
include HTTP::Handler include HTTP::Handler
getter app : Kemal::Base
def initialize(@app)
end
delegate log, to: app
def call(context : HTTP::Server::Context) def call(context : HTTP::Server::Context)
begin begin
call_next(context) call_next(context)
@ -14,7 +21,7 @@ module Kemal
log("Exception: #{ex.inspect_with_backtrace}") log("Exception: #{ex.inspect_with_backtrace}")
return call_exception_with_status_code(context, ex, 500) if context.app.error_handlers.has_key?(500) return call_exception_with_status_code(context, ex, 500) if context.app.error_handlers.has_key?(500)
verbosity = context.app.config.env == "production" ? false : true verbosity = context.app.config.env == "production" ? false : true
return render_500(context, ex.inspect_with_backtrace, verbosity) return app.render_500(context, ex.inspect_with_backtrace, verbosity)
end end
end end

View file

@ -1,8 +1,5 @@
module Kemal::FileHelpers module Kemal::FileHelpers
def log(message) extend self
logger.write "#{message}\n"
end
# Send a file with given path and base the mime-type on the file extension # Send a file with given path and base the mime-type on the file extension
# or default `application/octet-stream` mime_type. # or default `application/octet-stream` mime_type.
# #
@ -15,8 +12,7 @@ module Kemal::FileHelpers
# ``` # ```
# send_file env, "./path/to/file", "image/jpeg" # send_file env, "./path/to/file", "image/jpeg"
# ``` # ```
def send_file(env : HTTP::Server::Context, path : String, mime_type : String? = nil) def send_file(env : HTTP::Server::Context, path : String, config : Kemal::Config, mime_type : String? = nil)
config = env.app.config
file_path = File.expand_path(path, Dir.current) file_path = File.expand_path(path, Dir.current)
mime_type ||= Kemal::Utils.mime_type(file_path) mime_type ||= Kemal::Utils.mime_type(file_path)
env.response.content_type = mime_type env.response.content_type = mime_type
@ -28,17 +24,18 @@ module Kemal::FileHelpers
filestat = File.stat(file_path) filestat = File.stat(file_path)
config.static_headers.try(&.call(env.response, file_path, filestat)) config.static_headers.try(&.call(env.response, file_path, filestat))
gzip = config.serve_static?("gzip")
File.open(file_path) do |file| File.open(file_path) do |file|
if env.request.method == "GET" && env.request.headers.has_key?("Range") if env.request.method == "GET" && env.request.headers.has_key?("Range")
next multipart(file, env) next multipart(file, env)
end end
if request_headers.includes_word?("Accept-Encoding", "gzip") && config.serve_static?("gzip") && filesize > minsize && Kemal::Utils.zip_types(file_path) if request_headers.includes_word?("Accept-Encoding", "gzip") && gzip && filesize > minsize && Kemal::Utils.zip_types(file_path)
env.response.headers["Content-Encoding"] = "gzip" env.response.headers["Content-Encoding"] = "gzip"
Gzip::Writer.open(env.response) do |deflate| Gzip::Writer.open(env.response) do |deflate|
IO.copy(file, deflate) IO.copy(file, deflate)
end end
elsif request_headers.includes_word?("Accept-Encoding", "deflate") && config.serve_static?("gzip") && filesize > minsize && Kemal::Utils.zip_types(file_path) elsif request_headers.includes_word?("Accept-Encoding", "deflate") && gzip && filesize > minsize && Kemal::Utils.zip_types(file_path)
env.response.headers["Content-Encoding"] = "deflate" env.response.headers["Content-Encoding"] = "deflate"
Flate::Writer.open(env.response) do |deflate| Flate::Writer.open(env.response) do |deflate|
IO.copy(file, deflate) IO.copy(file, deflate)
@ -51,6 +48,10 @@ module Kemal::FileHelpers
return return
end end
def send_file(env, path : String, mime_type : String? = nil)
send_file(env, path, env.app.config, mime_type)
end
private def multipart(file, env : HTTP::Server::Context) private def multipart(file, env : HTTP::Server::Context)
# See http://httpwg.org/specs/rfc7233.html # See http://httpwg.org/specs/rfc7233.html
fileb = file.size fileb = file.size

View file

@ -4,6 +4,12 @@
module Kemal module Kemal
class StaticFileHandler < HTTP::StaticFileHandler class StaticFileHandler < HTTP::StaticFileHandler
getter config : Kemal::Config
def initialize(@config, fallthrough = true)
super(@config.public_folder, fallthrough)
end
def call(context : HTTP::Server::Context) def call(context : HTTP::Server::Context)
return call_next(context) if context.request.path.not_nil! == "/" return call_next(context) if context.request.path.not_nil! == "/"
@ -19,7 +25,6 @@ module Kemal
return return
end end
config = Kemal.config.serve_static
original_path = context.request.path.not_nil! original_path = context.request.path.not_nil!
request_path = URI.unescape(original_path) request_path = URI.unescape(original_path)
@ -48,7 +53,7 @@ module Kemal
end end
if Dir.exists?(file_path) if Dir.exists?(file_path)
if config.is_a?(Hash) && config["dir_listing"] == true if @config.serve_static?("dir_listing")
context.response.content_type = "text/html" context.response.content_type = "text/html"
directory_listing(context.response, request_path, file_path) directory_listing(context.response, request_path, file_path)
else else
@ -62,7 +67,8 @@ module Kemal
context.response.status_code = 304 context.response.status_code = 304
return return
end end
send_file(context, file_path)
FileHelpers.send_file(context, file_path, config)
else else
call_next(context) call_next(context)
end end