"""The app factory activates the hardening kit on the real app (incremental rollout). ``create_app`` wires the hardening pieces lowest-risk-first: the app-owned security headers and gzip/brotli compression (response-side), the request body-size measurement plus its 413 backstop, and ProxyFix (request-side). These boot the full app via ``fixture_client``, so they run under CI (a DB vendor is available) and skip locally when none is reachable. """ from werkzeug.exceptions import TooManyRequests from werkzeug.middleware.proxy_fix import ProxyFix from core.hardening.breaker import CircuitBreakerError def test_security_headers_are_active(fixture_client): """A real response from the booted app carries the app-owned security headers.""" resp = fixture_client.get('/hello/') assert resp.headers['X-Content-Type-Options'] == 'nosniff' assert resp.headers['X-Frame-Options'] == 'DENY' assert resp.headers['Referrer-Policy'] == 'strict-origin-when-cross-origin' assert resp.headers['Cross-Origin-Opener-Policy'] == 'same-origin' assert ( resp.headers['Content-Security-Policy'] == "default-src 'none'; frame-ancestors 'none'" ) assert resp.headers['Cache-Control'] == 'no-cache' def test_compression_is_active(fixture_client): """flask-compress is installed (streamed exports excluded) and runs on the response path.""" app = fixture_client.application assert app.config.get('COMPRESS_MIMETYPES') assert app.config.get('COMPRESS_STREAMS') is False # flask-compress tags every response it has processed with Vary: Accept-Encoding, so this # proves the compression after_request is actually wired (not just configured). resp = fixture_client.get('/hello/') assert 'accept-encoding' in resp.headers.get('Vary', '').lower() def test_body_limit_backstop_is_active(fixture_client): """The booted app carries the data-informed 1MB body-size ceiling.""" app = fixture_client.application assert app.config['MAX_CONTENT_LENGTH'] == 1 * 1024 * 1024 def test_proxyfix_is_active(fixture_client): """The booted app wraps wsgi_app in ProxyFix trusting the configured number of hops.""" app = fixture_client.application assert isinstance(app.wsgi_app, ProxyFix) assert app.wsgi_app.x_for == app.config['PROXYFIX_X_FOR'] def test_rate_limiting_is_active_in_shadow(fixture_client): """The booted app installs the rate limiter, and the dark-launch default keeps it in shadow.""" from core.hardening.rate_limit import RateLimitMode app = fixture_client.application state = app.extensions['hardening_rate_limiter'] assert state.mode is RateLimitMode.SHADOW def test_leak_free_error_handlers_are_registered(fixture_client): """The booted app renders breaker-open and rate-limit errors via the generic handlers. Registered via ``init_hardening``, not the generic HTTPException/InternalServerError handlers in ``register_error_handlers`` -- those would either leak the limit string (TooManyRequests) or 500 an otherwise-uncaught exception (CircuitBreakerError isn't an HTTPException). """ app = fixture_client.application # Flask buckets registered handlers by (blueprint, code): a plain Exception (CircuitBreakerError) # lands under code None, but an HTTPException subclass (TooManyRequests) lands under its own # .code (429) -- so each must be looked up under its own bucket, not both under [None][None]. assert CircuitBreakerError in app.error_handler_spec[None].get(None, {}) assert TooManyRequests in app.error_handler_spec[None].get(TooManyRequests.code, {}) def test_health_and_ready_paths_are_excluded_from_access_logging(fixture_client): """Both LB-polled paths are in the excluded-paths list handed to the request/access loggers.""" app = fixture_client.application config = app.config assert config['HEALTH_CHECK'] in config['HEALTH_CHECK_PATHS'] assert config['READY_CHECK'] in config['HEALTH_CHECK_PATHS'] def test_ready_endpoint_is_registered(fixture_client): """/ready/ answers with the DB-probe-driven status, distinct from the static /hello/.""" resp = fixture_client.get('/ready/') assert resp.status_code in (200, 503)