1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/railties/test/application/middleware_test.rb
eileencodes 6f02a2aec9
Add automatic shard swapping middleware
This PR adds a middleware that can be used for automatic shard swapping.
The design is similar to the database selector middleware in that the
resolver is provided by the application to determine which shard to
switch to. The selector also takes options to change the default
behavior of the middleware.

The only supported option is `lock` at the moment which will allow shard
swapping in a request, otherwise it defaults to true and prevents shard
swapping. This will help protect mistakenly switching shards inside of
application code in a multi-tenant application.

The resolver can be designed however the application wants but the basic
idea is that the resolver accesses the `request` headers and uses that
to lookup the subdomain which then looks up the tenant shard name stored
in that table. The tenant table is the "router" for the entire
application and an example resolver looks like this:

```ruby
config.active_record.shard_resolver = ->(request) {
  subdomain = request.subdomain
  tenant = Tenant.find_by_subdomain!(subdomain)
  tenant.shard
}
```

The `Tenant` table in this example would have `subdomain` and `shard`
attributes that are inserted into the database. These are used to route
to the shard in `connected_to`. Ie if we had a `Tenant` with the
subdomain `github` and the shard name `github_shard` we'd lookup the
connection with `ActiveRecord::Base.connected_to(shard: :github_shard)
{}` and all queries within that block (request) would be scoped to
the github shard.

Co-authored-by: John Crepezzi <john.crepezzi@gmail.com>
2021-11-18 08:18:45 -05:00

365 lines
12 KiB
Ruby

# frozen_string_literal: true
require "isolation/abstract_unit"
module ApplicationTests
class MiddlewareTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
def setup
build_app
FileUtils.rm_rf "#{app_path}/config/environments"
end
def teardown
teardown_app
end
def app
@app ||= Rails.application
end
test "default middleware stack" do
add_to_config "config.active_record.migration_error = :page_load"
add_to_config "config.server_timing = true"
boot!
assert_equal [
"ActionDispatch::HostAuthorization",
"Rack::Sendfile",
"ActionDispatch::Static",
"ActionDispatch::Executor",
"ActionDispatch::ServerTiming",
"ActiveSupport::Cache::Strategy::LocalCache",
"Rack::Runtime",
"Rack::MethodOverride",
"ActionDispatch::RequestId",
"ActionDispatch::RemoteIp",
"Rails::Rack::Logger",
"ActionDispatch::ShowExceptions",
"ActionDispatch::DebugExceptions",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"ActiveRecord::Migration::CheckPending",
"ActionDispatch::Cookies",
"ActionDispatch::Session::CookieStore",
"ActionDispatch::Flash",
"ActionDispatch::ContentSecurityPolicy::Middleware",
"ActionDispatch::PermissionsPolicy::Middleware",
"Rack::Head",
"Rack::ConditionalGet",
"Rack::ETag",
"Rack::TempfileReaper"
], middleware
end
test "default middleware stack when requests are local" do
add_to_config "config.consider_all_requests_local = true"
add_to_config "config.active_record.migration_error = :page_load"
add_to_config "config.server_timing = true"
boot!
assert_equal [
"ActionDispatch::HostAuthorization",
"Rack::Sendfile",
"ActionDispatch::Static",
"ActionDispatch::Executor",
"ActionDispatch::ServerTiming",
"ActiveSupport::Cache::Strategy::LocalCache",
"Rack::Runtime",
"Rack::MethodOverride",
"ActionDispatch::RequestId",
"ActionDispatch::RemoteIp",
"Rails::Rack::Logger",
"ActionDispatch::ShowExceptions",
"ActionDispatch::DebugExceptions",
"ActionDispatch::ActionableExceptions",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"ActiveRecord::Migration::CheckPending",
"ActionDispatch::Cookies",
"ActionDispatch::Session::CookieStore",
"ActionDispatch::Flash",
"ActionDispatch::ContentSecurityPolicy::Middleware",
"ActionDispatch::PermissionsPolicy::Middleware",
"Rack::Head",
"Rack::ConditionalGet",
"Rack::ETag",
"Rack::TempfileReaper"
], middleware
end
test "api middleware stack" do
add_to_config "config.api_only = true"
boot!
assert_equal [
"ActionDispatch::HostAuthorization",
"Rack::Sendfile",
"ActionDispatch::Static",
"ActionDispatch::Executor",
"ActiveSupport::Cache::Strategy::LocalCache",
"Rack::Runtime",
"ActionDispatch::RequestId",
"ActionDispatch::RemoteIp",
"Rails::Rack::Logger",
"ActionDispatch::ShowExceptions",
"ActionDispatch::DebugExceptions",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"Rack::Head",
"Rack::ConditionalGet",
"Rack::ETag"
], middleware
end
test "middleware dependencies" do
boot!
# The following array-of-arrays describes dependencies between
# middlewares: the first item in each list depends on the
# remaining items (and therefore must occur later in the
# middleware stack).
dependencies = [
# Logger needs a fully "corrected" request environment
%w(Rails::Rack::Logger Rack::MethodOverride ActionDispatch::RequestId ActionDispatch::RemoteIp),
# Serving public/ doesn't invoke user code, so it should skip
# locks etc
%w(ActionDispatch::Executor ActionDispatch::Static),
# Errors during reload must be reported
%w(ActionDispatch::Reloader ActionDispatch::ShowExceptions ActionDispatch::DebugExceptions),
# Outright dependencies
%w(ActionDispatch::Static Rack::Sendfile),
%w(ActionDispatch::Flash ActionDispatch::Session::CookieStore),
%w(ActionDispatch::Session::CookieStore ActionDispatch::Cookies),
]
require "tsort"
sorted = TSort.tsort((middleware | dependencies.flatten).method(:each),
lambda { |n, &b| dependencies.each { |m, *ds| ds.each(&b) if m == n } })
assert_equal sorted, middleware
end
test "Rack::Cache is not included by default" do
boot!
assert_not_includes middleware, "Rack::Cache", "Rack::Cache is not included in the default stack unless you set config.action_dispatch.rack_cache"
end
test "Rack::Cache is present when action_dispatch.rack_cache is set" do
add_to_config "config.action_dispatch.rack_cache = true"
boot!
assert_includes middleware, "Rack::Cache"
end
test "ActiveRecord::Migration::CheckPending is present when active_record.migration_error is set to :page_load" do
add_to_config "config.active_record.migration_error = :page_load"
boot!
assert_includes middleware, "ActiveRecord::Migration::CheckPending"
end
test "ActionDispatch::SSL is present when force_ssl is set" do
add_to_config "config.force_ssl = true"
boot!
assert_includes middleware, "ActionDispatch::SSL"
end
test "ActionDispatch::SSL is configured with options when given" do
add_to_config "config.force_ssl = true"
add_to_config "config.ssl_options = { redirect: { host: 'example.com' } }"
boot!
assert_equal [{ redirect: { host: "example.com" }, ssl_default_redirect_status: 308 }], Rails.application.middleware[1].args
end
test "removing Active Record omits its middleware" do
use_frameworks []
boot!
assert_not_includes middleware, "ActiveRecord::Migration::CheckPending"
end
test "includes executor" do
boot!
assert_includes middleware, "ActionDispatch::Executor"
end
test "does not include lock if cache_classes is set and so is eager_load" do
add_to_config "config.cache_classes = true"
add_to_config "config.eager_load = true"
boot!
assert_not_includes middleware, "Rack::Lock"
end
test "does not include lock if allow_concurrency is set to :unsafe" do
add_to_config "config.allow_concurrency = :unsafe"
boot!
assert_not_includes middleware, "Rack::Lock"
end
test "includes lock if allow_concurrency is disabled" do
add_to_config "config.allow_concurrency = false"
boot!
assert_includes middleware, "Rack::Lock"
end
test "removes static asset server if public_file_server.enabled is disabled" do
add_to_config "config.public_file_server.enabled = false"
boot!
assert_not_includes middleware, "ActionDispatch::Static"
end
test "can delete a middleware from the stack" do
add_to_config "config.middleware.delete ActionDispatch::Static"
boot!
assert_not_includes middleware, "ActionDispatch::Static"
end
test "can delete a middleware from the stack even if insert_before is added after delete" do
add_to_config "config.middleware.delete Rack::Runtime"
add_to_config "config.middleware.insert_before(Rack::Runtime, Rack::Config)"
boot!
assert_includes middleware, "Rack::Config"
assert_not middleware.include?("Rack::Runtime")
end
test "can delete a middleware from the stack even if insert_after is added after delete" do
add_to_config "config.middleware.delete Rack::Runtime"
add_to_config "config.middleware.insert_after(Rack::Runtime, Rack::Config)"
boot!
assert_includes middleware, "Rack::Config"
assert_not middleware.include?("Rack::Runtime")
end
test "includes exceptions middlewares even if action_dispatch.show_exceptions is disabled" do
add_to_config "config.action_dispatch.show_exceptions = false"
boot!
assert_includes middleware, "ActionDispatch::ShowExceptions"
assert_includes middleware, "ActionDispatch::DebugExceptions"
end
test "removes ActionDispatch::Reloader if cache_classes is true" do
add_to_config "config.cache_classes = true"
boot!
assert_not_includes middleware, "ActionDispatch::Reloader"
end
test "use middleware" do
use_frameworks []
add_to_config "config.middleware.use Rack::Config"
boot!
assert_equal "Rack::Config", middleware.last
end
test "insert middleware after" do
add_to_config "config.middleware.insert_after Rack::Sendfile, Rack::Config"
boot!
assert_equal "Rack::Config", middleware.third
end
test "unshift middleware" do
add_to_config "config.middleware.unshift Rack::Config"
boot!
assert_equal "Rack::Config", middleware.first
end
test "Rails.cache does not respond to middleware" do
add_to_config "config.cache_store = :memory_store, { timeout: 10 }"
boot!
assert_equal "Rack::Runtime", middleware[4]
assert_instance_of ActiveSupport::Cache::MemoryStore, Rails.cache
end
test "Rails.cache does respond to middleware" do
boot!
assert_equal "ActiveSupport::Cache::Strategy::LocalCache", middleware[4]
assert_equal "Rack::Runtime", middleware[5]
end
test "insert middleware before" do
add_to_config "config.middleware.insert_before Rack::Sendfile, Rack::Config"
boot!
assert_equal "Rack::Config", middleware.second
end
test "can't change middleware after it's built" do
boot!
assert_raise FrozenError do
app.config.middleware.use Rack::Config
end
end
# ConditionalGet + Etag
test "conditional get + etag middlewares handle http caching based on body" do
make_basic_app
class ::OmgController < ActionController::Base
def index
if params[:nothing]
render plain: ""
else
render plain: "OMG"
end
end
end
etag = "W/" + "c00862d1c6c1cf7c1b49388306e7b3c1".inspect
get "/"
assert_equal 200, last_response.status
assert_equal "OMG", last_response.body
assert_equal "text/plain; charset=utf-8", last_response.headers["Content-Type"]
assert_equal "max-age=0, private, must-revalidate", last_response.headers["Cache-Control"]
assert_equal etag, last_response.headers["Etag"]
get "/", {}, { "HTTP_IF_NONE_MATCH" => etag }
assert_equal 304, last_response.status
assert_equal "", last_response.body
assert_nil last_response.headers["Content-Type"]
assert_equal "max-age=0, private, must-revalidate", last_response.headers["Cache-Control"]
assert_equal etag, last_response.headers["Etag"]
get "/?nothing=true"
assert_equal 200, last_response.status
assert_equal "", last_response.body
assert_equal "text/plain; charset=utf-8", last_response.headers["Content-Type"]
assert_equal "no-cache", last_response.headers["Cache-Control"]
assert_nil last_response.headers["Etag"]
end
test "ORIGINAL_FULLPATH is passed to env" do
boot!
env = ::Rack::MockRequest.env_for("/foo/?something")
Rails.application.call(env)
assert_equal "/foo/?something", env["ORIGINAL_FULLPATH"]
end
test "shard selector middleware is installed by config option" do
add_to_config "config.active_record.shard_resolver = ->(*) { }"
boot!
assert_includes middleware, "ActiveRecord::Middleware::ShardSelector"
end
private
def boot!
require "#{app_path}/config/environment"
end
def middleware
Rails.application.middleware.map(&:klass).map(&:name)
end
end
end