Skip to content

Commit f4e8122

Browse files
committed
eli-389 adding simple unit tests to test security header middleware
1 parent 5580fc3 commit f4e8122

File tree

2 files changed

+147
-0
lines changed

2 files changed

+147
-0
lines changed

tests/unit/middleware/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for middleware components."""
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""Tests for security headers middleware."""
2+
3+
from http import HTTPStatus
4+
5+
import pytest
6+
from flask import Flask
7+
from flask.testing import FlaskClient
8+
9+
from eligibility_signposting_api.middleware import SecurityHeadersMiddleware
10+
11+
12+
class TestError(Exception):
13+
"""Test exception for error handling tests."""
14+
15+
16+
@pytest.fixture
17+
def test_app() -> Flask:
18+
"""Create a test Flask app with security headers middleware."""
19+
app = Flask(__name__)
20+
SecurityHeadersMiddleware(app)
21+
22+
@app.route("/test")
23+
def test_route():
24+
return {"status": "ok"}, HTTPStatus.OK
25+
26+
@app.route("/error")
27+
def error_route():
28+
msg = "Test error"
29+
raise TestError(msg)
30+
31+
@app.errorhandler(TestError)
32+
def handle_value_error(e):
33+
return {"error": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR
34+
35+
return app
36+
37+
38+
@pytest.fixture
39+
def client(test_app: Flask) -> FlaskClient:
40+
"""Create a test client."""
41+
return test_app.test_client()
42+
43+
44+
class TestSecurityHeadersMiddleware:
45+
"""Test suite for SecurityHeadersMiddleware."""
46+
47+
def test_security_headers_present_on_successful_response(self, client: FlaskClient) -> None:
48+
"""Test that security headers are added to successful responses."""
49+
response = client.get("/test")
50+
51+
assert response.status_code == HTTPStatus.OK
52+
assert response.headers.get("Cache-Control") == "no-store, private"
53+
assert response.headers.get("Strict-Transport-Security") == "max-age=31536000; includeSubDomains"
54+
assert response.headers.get("X-Content-Type-Options") == "nosniff"
55+
56+
def test_security_headers_present_on_error_response(self, client: FlaskClient) -> None:
57+
"""Test that security headers are added to error responses."""
58+
response = client.get("/error")
59+
60+
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
61+
assert response.headers.get("Cache-Control") == "no-store, private"
62+
assert response.headers.get("Strict-Transport-Security") == "max-age=31536000; includeSubDomains"
63+
assert response.headers.get("X-Content-Type-Options") == "nosniff"
64+
65+
def test_security_headers_present_on_404(self, client: FlaskClient) -> None:
66+
"""Test that security headers are added to 404 responses."""
67+
response = client.get("/nonexistent")
68+
69+
assert response.status_code == HTTPStatus.NOT_FOUND
70+
assert response.headers.get("Cache-Control") == "no-store, private"
71+
assert response.headers.get("Strict-Transport-Security") == "max-age=31536000; includeSubDomains"
72+
assert response.headers.get("X-Content-Type-Options") == "nosniff"
73+
74+
def test_all_expected_headers_are_present(self, client: FlaskClient) -> None:
75+
"""Test that all expected security headers are present."""
76+
response = client.get("/test")
77+
78+
expected_headers = {
79+
"Cache-Control",
80+
"Strict-Transport-Security",
81+
"X-Content-Type-Options",
82+
}
83+
84+
response_headers = set(response.headers.keys())
85+
assert expected_headers.issubset(response_headers), (
86+
f"Missing security headers: {expected_headers - response_headers}"
87+
)
88+
89+
def test_cache_control_prevents_caching(self, client: FlaskClient) -> None:
90+
"""Test that Cache-Control header prevents caching of sensitive data."""
91+
response = client.get("/test")
92+
93+
cache_control = response.headers.get("Cache-Control")
94+
assert cache_control is not None
95+
assert "no-store" in cache_control, "Response should not be stored in any cache"
96+
assert "private" in cache_control, "Response should be marked as private"
97+
98+
def test_hsts_header_enforces_https(self, client: FlaskClient) -> None:
99+
"""Test that HSTS header is properly configured."""
100+
response = client.get("/test")
101+
102+
hsts = response.headers.get("Strict-Transport-Security")
103+
assert hsts is not None
104+
assert "max-age=31536000" in hsts, "HSTS should be valid for 1 year"
105+
assert "includeSubDomains" in hsts, "HSTS should apply to all subdomains"
106+
107+
def test_content_type_options_prevents_sniffing(self, client: FlaskClient) -> None:
108+
"""Test that X-Content-Type-Options prevents MIME sniffing."""
109+
response = client.get("/test")
110+
111+
content_type_options = response.headers.get("X-Content-Type-Options")
112+
assert content_type_options == "nosniff", "Should prevent MIME type sniffing"
113+
114+
def test_middleware_init_app_method(self) -> None:
115+
"""Test that middleware can be initialized separately using init_app."""
116+
app = Flask(__name__)
117+
middleware = SecurityHeadersMiddleware()
118+
middleware.init_app(app)
119+
120+
@app.route("/test")
121+
def test_route():
122+
return {"status": "ok"}, 200
123+
124+
with app.test_client() as client:
125+
response = client.get("/test")
126+
assert response.headers.get("Cache-Control") == "no-store, private"
127+
128+
def test_existing_headers_are_not_overridden(self) -> None:
129+
"""Test that existing headers are not overridden by middleware."""
130+
app = Flask(__name__)
131+
SecurityHeadersMiddleware(app)
132+
133+
@app.route("/test")
134+
def test_route():
135+
from flask import make_response
136+
137+
resp = make_response({"status": "ok"}, 200)
138+
resp.headers["Cache-Control"] = "public, max-age=3600"
139+
return resp
140+
141+
with app.test_client() as client:
142+
response = client.get("/test")
143+
# Should keep the custom Cache-Control value
144+
assert response.headers.get("Cache-Control") == "public, max-age=3600"
145+
# But other headers should still be added
146+
assert response.headers.get("X-Content-Type-Options") == "nosniff"

0 commit comments

Comments
 (0)