hanami-controller/spec/integration/hanami/controller/cache_spec.rb

515 lines
15 KiB
Ruby

# frozen_string_literal: true
require "hanami/router"
module CacheControl
class Default < Hanami::Action
include Hanami::Action::Cache
cache_control :public, max_age: 600
def handle(*)
end
end
class Overriding < Hanami::Action
include Hanami::Action::Cache
cache_control :public, max_age: 600
def handle(_, res)
res.cache_control :private
end
end
class Symbol < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.cache_control :private
end
end
class Symbols < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.cache_control :private, :no_cache, :no_store
end
end
class Hash < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.cache_control :public, :no_store, max_age: 900, s_maxage: 86_400, min_fresh: 500, max_stale: 700
end
end
class PrivatePublic < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.cache_control :private, :public
end
end
class Application
def initialize
routes = Hanami::Router.new do
get "/default", to: CacheControl::Default.new
get "/overriding", to: CacheControl::Overriding.new
get "/symbol", to: CacheControl::Symbol.new
get "/symbols", to: CacheControl::Symbols.new
get "/hash", to: CacheControl::Hash.new
get "/private-and-public", to: CacheControl::PrivatePublic.new
end
@app = Rack::Builder.new do
use Rack::Lint
run routes
end.to_app
end
def call(env)
@app.call(env)
end
end
end
module Web
module Controllers
module Home
class Index < Hanami::Action
def handle(*)
end
end
end
end
end
module Admin
module Controllers
module Home
class Index < Hanami::Action
def handle(*)
end
end
end
end
end
module Expires
class Default < Hanami::Action
include Hanami::Action::Cache
expires 900, :public, :no_cache
def handle(*)
end
end
class Overriding < Hanami::Action
include Hanami::Action::Cache
expires 900, :public, :no_cache
def handle(_, res)
res.expires 600, :private
end
end
class Symbol < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.expires 900, :private
end
end
class Symbols < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.expires 900, :private, :no_cache, :no_store
end
end
class Hash < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.expires 900, :public, :no_store, s_maxage: 86_400, min_fresh: 500, max_stale: 700
end
end
class Application
def initialize
routes = Hanami::Router.new do
get "/default", to: Expires::Default.new
get "/overriding", to: Expires::Overriding.new
get "/symbol", to: Expires::Symbol.new
get "/symbols", to: Expires::Symbols.new
get "/hash", to: Expires::Hash.new
end
@app = Rack::Builder.new do
use Rack::Lint
run routes
end.to_app
end
def call(env)
@app.call(env)
end
end
end
module ConditionalGet
class Etag < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.fresh etag: "updated"
end
end
class LastModified < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.fresh last_modified: Time.now
end
end
class EtagLastModified < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.fresh etag: "updated", last_modified: Time.now
end
end
class Application
def initialize
routes = Hanami::Router.new do
get "/etag", to: ConditionalGet::Etag.new
get "/last-modified", to: ConditionalGet::LastModified.new
get "/etag-last-modified", to: ConditionalGet::EtagLastModified.new
get "/last-modified-nil-value", to: ConditionalGet::LastModifiedNilValue.new
get "/etag-nil-value", to: ConditionalGet::EtagNilValue.new
end
@app = Rack::Builder.new do
# FIXME: enable again Rack::Lint. It looks like there was some problems
# with the headers that we never discovered, because this is the first
# time we add Lint to these tests.
#
# use Rack::Lint
run routes
end
end
def call(env)
@app.call(env)
end
end
class LastModifiedNilValue < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.fresh last_modified: nil
end
end
class EtagNilValue < Hanami::Action
include Hanami::Action::Cache
def handle(_, res)
res.fresh etag: nil
end
end
end
RSpec.describe "HTTP Cache" do
describe "Cache control" do
let(:app) { Rack::MockRequest.new(CacheControl::Application.new) }
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(Expires::Application.new) }
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
# FIXME: remove `at_least`
expect(Time).to receive(:now).at_least(:once).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
# FIXME: remove `at_least`
expect(Time).to receive(:now).at_least(:once).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
# FIXME: remove `at_least`
expect(Time).to receive(:now).at_least(:once).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(ConditionalGet::Application.new) }
describe "#etag" do
context "when HTTP_IF_NONE_MATCH header is not defined" do
it "completes request" do
response = app.get("/etag")
expect(response.status).to be(200)
end
it "returns etag header" do
response = app.get("/etag")
expect(response.headers.fetch("ETag")).to eq("updated")
end
end
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
context "when If-Modified-Since is set" do
it "completes request" do
response = app.get("/etag", "HTTP_IF_MODIFIED_SINCE" => Time.now.httpdate)
expect(response.status).to be(200)
end
it "returns etag header" do
response = app.get("/etag", "HTTP_IF_MODIFIED_SINCE" => Time.now.httpdate)
expect(response.headers.fetch("ETag")).to eq("updated")
end
end
context "when etag has nil value" do
it "completes request" do
response = app.get("/etag-nil-value", "HTTP_IF_NONE_MATCH" => "outdated")
expect(response.status).to be(200)
end
it "does not return ETag header" do
response = app.get("/etag-nil-value", "HTTP_IF_NONE_MATCH" => "outdated")
expect(response.headers).not_to have_key("ETag")
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 HTTP_IF_MODIFIED_SINCE header is not defined" do
before do
expect(Time).to receive(:now).at_least(:once).and_return(modified_since)
end
it "completes request" do
response = app.get("/last-modified")
expect(response.status).to be(200)
end
it "returns Last-Modified header" do
response = app.get("/last-modified")
expect(response.headers.fetch("Last-Modified")).to eq(modified_since.httpdate)
end
end
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 "returns Last-Modified header" do
expect(Time).to receive(:now).and_return(modified_since)
response = app.get("/last-modified", "HTTP_IF_NONE_MATCH" => "")
expect(response.headers).to 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 "returns Last-Modified header" do
expect(Time).to receive(:now).and_return(modified_since)
response = app.get("/last-modified", "HTTP_IF_NONE_MATCH" => " ")
expect(response.headers).to have_key("Last-Modified")
end
end
end
context "when last_modified has nil value" do
it "completes request" do
response = app.get("/last-modified-nil-value", "HTTP_IF_NONE_MATCH" => "outdated")
expect(response.status).to be(200)
end
it "does not return Last-Modified header" do
response = app.get("/last-modified-nil-value", "HTTP_IF_NONE_MATCH" => "outdated")
expect(response.headers).not_to have_key("Last-Modified")
end
end
end
end
end