55import pytest
66from flask import Flask
77from flask .testing import FlaskClient
8+ from hamcrest import assert_that , contains_string , equal_to , has_entries , is_
89
910from eligibility_signposting_api .middleware import SecurityHeadersMiddleware
1011
1112
12- class TestError (Exception ):
13- """Test exception for error handling tests."""
13+ class MiddlewareTestError (Exception ):
14+ """Custom exception for middleware error handling tests."""
1415
1516
1617@pytest .fixture
@@ -26,9 +27,9 @@ def test_route():
2627 @app .route ("/error" )
2728 def error_route ():
2829 msg = "Test error"
29- raise TestError (msg )
30+ raise MiddlewareTestError (msg )
3031
31- @app .errorhandler (TestError )
32+ @app .errorhandler (MiddlewareTestError )
3233 def handle_value_error (e ):
3334 return {"error" : str (e )}, HTTPStatus .INTERNAL_SERVER_ERROR
3435
@@ -48,28 +49,49 @@ def test_security_headers_present_on_successful_response(self, client: FlaskClie
4849 """Test that security headers are added to successful responses."""
4950 response = client .get ("/test" )
5051
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"
52+ assert_that (response .status_code , is_ (equal_to (HTTPStatus .OK )))
53+ assert_that (
54+ dict (response .headers ),
55+ has_entries (
56+ {
57+ "Cache-Control" : "no-store, private" ,
58+ "Strict-Transport-Security" : "max-age=31536000; includeSubDomains" ,
59+ "X-Content-Type-Options" : "nosniff" ,
60+ }
61+ ),
62+ )
5563
5664 def test_security_headers_present_on_error_response (self , client : FlaskClient ) -> None :
5765 """Test that security headers are added to error responses."""
5866 response = client .get ("/error" )
5967
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"
68+ assert_that (response .status_code , is_ (equal_to (HTTPStatus .INTERNAL_SERVER_ERROR )))
69+ assert_that (
70+ dict (response .headers ),
71+ has_entries (
72+ {
73+ "Cache-Control" : "no-store, private" ,
74+ "Strict-Transport-Security" : "max-age=31536000; includeSubDomains" ,
75+ "X-Content-Type-Options" : "nosniff" ,
76+ }
77+ ),
78+ )
6479
6580 def test_security_headers_present_on_404 (self , client : FlaskClient ) -> None :
6681 """Test that security headers are added to 404 responses."""
6782 response = client .get ("/nonexistent" )
6883
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"
84+ assert_that (response .status_code , is_ (equal_to (HTTPStatus .NOT_FOUND )))
85+ assert_that (
86+ dict (response .headers ),
87+ has_entries (
88+ {
89+ "Cache-Control" : "no-store, private" ,
90+ "Strict-Transport-Security" : "max-age=31536000; includeSubDomains" ,
91+ "X-Content-Type-Options" : "nosniff" ,
92+ }
93+ ),
94+ )
7395
7496 def test_all_expected_headers_are_present (self , client : FlaskClient ) -> None :
7597 """Test that all expected security headers are present."""
@@ -91,25 +113,23 @@ def test_cache_control_prevents_caching(self, client: FlaskClient) -> None:
91113 response = client .get ("/test" )
92114
93115 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"
116+ assert_that (cache_control , contains_string ("no-store" ))
117+ assert_that (cache_control , contains_string ("private" ))
97118
98119 def test_hsts_header_enforces_https (self , client : FlaskClient ) -> None :
99120 """Test that HSTS header is properly configured."""
100121 response = client .get ("/test" )
101122
102123 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"
124+ assert_that (hsts , contains_string ("max-age=31536000" ))
125+ assert_that (hsts , contains_string ("includeSubDomains" ))
106126
107127 def test_content_type_options_prevents_sniffing (self , client : FlaskClient ) -> None :
108128 """Test that X-Content-Type-Options prevents MIME sniffing."""
109129 response = client .get ("/test" )
110130
111131 content_type_options = response .headers .get ("X-Content-Type-Options" )
112- assert content_type_options == "nosniff" , "Should prevent MIME type sniffing"
132+ assert_that ( content_type_options , is_ ( equal_to ( "nosniff" )))
113133
114134 def test_middleware_init_app_method (self ) -> None :
115135 """Test that middleware can be initialized separately using init_app."""
@@ -119,11 +139,11 @@ def test_middleware_init_app_method(self) -> None:
119139
120140 @app .route ("/test" )
121141 def test_route ():
122- return {"status" : "ok" }, 200
142+ return {"status" : "ok" }, HTTPStatus . OK
123143
124144 with app .test_client () as client :
125145 response = client .get ("/test" )
126- assert response .headers .get ("Cache-Control" ) == "no-store, private"
146+ assert_that ( response .headers .get ("Cache-Control" ), is_ ( equal_to ( "no-store, private" )))
127147
128148 def test_existing_headers_are_not_overridden (self ) -> None :
129149 """Test that existing headers are not overridden by middleware."""
@@ -134,13 +154,13 @@ def test_existing_headers_are_not_overridden(self) -> None:
134154 def test_route ():
135155 from flask import make_response
136156
137- resp = make_response ({"status" : "ok" }, 200 )
157+ resp = make_response ({"status" : "ok" }, HTTPStatus . OK )
138158 resp .headers ["Cache-Control" ] = "public, max-age=3600"
139159 return resp
140160
141161 with app .test_client () as client :
142162 response = client .get ("/test" )
143163 # Should keep the custom Cache-Control value
144- assert response .headers .get ("Cache-Control" ) == "public, max-age=3600"
164+ assert_that ( response .headers .get ("Cache-Control" ), is_ ( equal_to ( "public, max-age=3600" )))
145165 # But other headers should still be added
146- assert response .headers .get ("X-Content-Type-Options" ) == "nosniff"
166+ assert_that ( response .headers .get ("X-Content-Type-Options" ), is_ ( equal_to ( "nosniff" )))
0 commit comments