Module: Otto::Testing
- Defined in:
- lib/otto/testing.rb
Overview
Test support for applications built on Otto, independent of the test
framework. require 'otto' does not load this file; require
'otto/testing' from a test helper. Loading it is the opt-in for the
resets below, which production code must never call.
Class Method Summary collapse
- .env_for(uri = '/', client_ip:, security_config:, **rack_options) ⇒ Hash
-
.reset! ⇒ nil
Clear the process-global state Otto accumulates across Otto.new calls, so one test's applications cannot decide whether the next test's raise.
-
.resolve_client_ip!(env, security_config) ⇒ Hash
Run IPPrivacyMiddleware over an existing env, in place, so it carries the keys the middleware writes, all derived from one resolution.
Class Method Details
.env_for(uri = '/', client_ip:, security_config:, **rack_options) ⇒ Hash
Build a Rack env as IPPrivacyMiddleware leaves it for a request arriving directly from client_ip: env (masked under the privacy profile in force), env over the unmasked address, the rewritten REMOTE_ADDR, and the proxy-trust verdict when trust is configured.
Use it instead of writing env by hand. A hand-written value looks to the middleware like a prior pass, so the precise otto.ip_match is never built and every CIDR check denies.
security_config is required because the app's own IPPrivacyMiddleware keeps whatever this resolution produced: pass the application's (otto.security_config) so the privacy profile and proxy trust match the app under test. nil means an unconfigured middleware: public addresses masked, no proxy trust.
The request is direct, so the headers in Otto::Utils::CLIENT_ADDRESS_HEADERS (X-Forwarded-For, X-Real-IP, X-Client-IP, Forwarded) raise ArgumentError: under a config that trusts the peer they would move the resolved address away from client_ip. For a relayed request, build the env with REMOTE_ADDR and the forwarded headers and call resolve_client_ip!.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/otto/testing.rb', line 96 def env_for(uri = '/', client_ip:, security_config:, **) forwarded = Otto::Utils::CLIENT_ADDRESS_HEADERS & .keys unless forwarded.empty? raise ArgumentError, "env_for builds a direct request from client_ip; #{forwarded.join(', ')} " \ 'would change which address resolves. Build the env yourself and call ' \ 'Otto::Testing.resolve_client_ip! for a relayed request.' end env = Rack::MockRequest.env_for(uri, ) if client_ip.nil? env.delete('REMOTE_ADDR') else env['REMOTE_ADDR'] = client_ip end resolve_client_ip!(env, security_config) end |
.reset! ⇒ nil
Clear the process-global state Otto accumulates across Otto.new calls, so one test's applications cannot decide whether the next test's raise.
Today that is the forwarding-family registry and Rack::Request.forwarded_priority, which Otto pins from trusted_proxy_header. Rack's setting is one per process, so a second application choosing a different family raises ArgumentError; in a suite that builds applications with different families, the outcome would otherwise depend on test order.
Call it before every test (or after every test), not once per suite.
52 53 54 55 |
# File 'lib/otto/testing.rb', line 52 def reset! Otto::Security::Config.send(:reset_rack_forwarding_family!) nil end |
.resolve_client_ip!(env, security_config) ⇒ Hash
Run IPPrivacyMiddleware over an existing env, in place, so it carries the keys the middleware writes, all derived from one resolution. Use it for relayed requests: REMOTE_ADDR is the proxy and the forwarded headers carry the client.
Pass the application's security config. Its proxy trust decides whether the forwarded headers are read at all; resolved without it, the proxy becomes the client, and the app's middleware keeps that result.
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/otto/testing.rb', line 130 def resolve_client_ip!(env, security_config) if env.key?('otto.client_ip') || env.key?('otto.ip_match') raise ArgumentError, 'env already carries otto.client_ip or otto.ip_match, so IPPrivacyMiddleware ' \ 'would keep that result instead of resolving under this security_config' end Otto::Security::Middleware::IPPrivacyMiddleware.new(RESOLVED_APP, security_config).call(env) # The middleware's response is discarded, so check its effect instead: a # path that answered before resolving would otherwise hand back an env # that looks built and is not. unless env.key?('otto.ip_match') raise 'IPPrivacyMiddleware returned without installing otto.ip_match; Otto::Testing ' \ 'no longer matches the middleware and needs updating' end env end |