|
13 | 13 | from core.middleware import apply_private_no_store |
14 | 14 | from core.preview import SENSITIVE_PREVIEW_QUERY_KEYS, staff_preview_required |
15 | 15 | from core.seo import validated_canonical_url |
16 | | -from core.views import DEVELOPMENT_ROBOTS_BODY |
| 16 | +from core.views import DEVELOPMENT_ROBOTS_BODY, PRODUCTION_ROBOTS_BODY |
17 | 17 | from courses.models import Course |
18 | 18 |
|
19 | 19 | FIXTURE_URLCONF = "core.tests.seo_fixture_urls" |
@@ -122,6 +122,11 @@ def test_credential_bearing_early_response_fails_closed(self) -> None: |
122 | 122 | for headers in ( |
123 | 123 | {"authorization": "Bearer opaque-input"}, |
124 | 124 | {"cookie": "sessionid=opaque-session"}, |
| 125 | + {"cookie": "csrftoken=opaque-csrf"}, |
| 126 | + {"cookie": "opaque_credential=opaque-value"}, |
| 127 | + {"x-csrftoken": "opaque-csrf-header"}, |
| 128 | + {"x-preview-token": "opaque-preview"}, |
| 129 | + {"x-management-token": "opaque-management"}, |
125 | 130 | ): |
126 | 131 | with self.subTest(headers=headers): |
127 | 132 | response = self.client.get("/missing", headers=headers) |
@@ -164,10 +169,90 @@ def test_sitemap_get_and_head_expose_the_checked_section_index(self) -> None: |
164 | 169 | self.assertEqual(head.content, b"") |
165 | 170 |
|
166 | 171 | @override_settings(NOINDEX=False) |
167 | | - def test_production_exposes_only_the_public_sitemap(self) -> None: |
| 172 | + def test_production_robots_contract_and_public_sitemap(self) -> None: |
168 | 173 | robots = self.client.get("/robots.txt") |
169 | | - self.assertEqual(robots.status_code, 404) |
| 174 | + self.assertEqual(robots.status_code, 200) |
| 175 | + self.assertEqual(robots.content, PRODUCTION_ROBOTS_BODY.encode()) |
| 176 | + self.assertEqual(robots.headers["Content-Type"], "text/plain; charset=utf-8") |
| 177 | + self.assertEqual(robots.headers["Cache-Control"], "max-age=0, must-revalidate") |
170 | 178 | self.assertNotIn("X-Robots-Tag", robots.headers) |
| 179 | + self.assertNotIn("/podwiki/", robots.content.decode()) |
| 180 | + self.assertNotIn("web.dtcdev.click", robots.content.decode()) |
| 181 | + |
| 182 | + head = self.client.head("/robots.txt") |
| 183 | + self.assertEqual(head.status_code, 200) |
| 184 | + self.assertEqual(head.content, b"") |
| 185 | + self.assertEqual(head.headers["Content-Type"], robots.headers["Content-Type"]) |
| 186 | + self.assertEqual(head.headers["Cache-Control"], robots.headers["Cache-Control"]) |
| 187 | + self.assertNotIn("X-Robots-Tag", head.headers) |
| 188 | + |
| 189 | + for method in ("POST", "PUT", "PATCH", "DELETE", "OPTIONS"): |
| 190 | + with self.subTest(method=method): |
| 191 | + response = self.client.generic(method, "/robots.txt", data=b"opaque-input") |
| 192 | + self.assertEqual(response.status_code, 405) |
| 193 | + self.assertEqual(response.headers["Allow"], "GET, HEAD") |
| 194 | + self.assertEqual(response.headers["Cache-Control"], "no-store, max-age=0") |
| 195 | + self.assertNotIn("public", cache_directives(response)) |
| 196 | + self.assertNotIn("s-maxage=3600", cache_directives(response)) |
| 197 | + |
| 198 | + credential_responses = ( |
| 199 | + ( |
| 200 | + "authorization", |
| 201 | + self.client.get("/robots.txt", HTTP_AUTHORIZATION="Bearer opaque-input"), |
| 202 | + self.client.head("/robots.txt", HTTP_AUTHORIZATION="Bearer opaque-input"), |
| 203 | + ), |
| 204 | + ( |
| 205 | + "session-cookie", |
| 206 | + self.client.get("/robots.txt", HTTP_COOKIE="sessionid=opaque-session"), |
| 207 | + self.client.head("/robots.txt", HTTP_COOKIE="sessionid=opaque-session"), |
| 208 | + ), |
| 209 | + ( |
| 210 | + "csrf-cookie", |
| 211 | + self.client.get("/robots.txt", HTTP_COOKIE="csrftoken=opaque-csrf"), |
| 212 | + self.client.head("/robots.txt", HTTP_COOKIE="csrftoken=opaque-csrf"), |
| 213 | + ), |
| 214 | + ( |
| 215 | + "csrf-token-header", |
| 216 | + self.client.get("/robots.txt", HTTP_X_CSRFTOKEN="opaque-csrf-header"), |
| 217 | + self.client.head("/robots.txt", HTTP_X_CSRFTOKEN="opaque-csrf-header"), |
| 218 | + ), |
| 219 | + ( |
| 220 | + "unknown-cookie", |
| 221 | + self.client.get("/robots.txt", HTTP_COOKIE="opaque_credential=opaque-value"), |
| 222 | + self.client.head("/robots.txt", HTTP_COOKIE="opaque_credential=opaque-value"), |
| 223 | + ), |
| 224 | + ( |
| 225 | + "preview-token-header", |
| 226 | + self.client.get("/robots.txt", HTTP_X_PREVIEW_TOKEN="opaque-preview"), |
| 227 | + self.client.head("/robots.txt", HTTP_X_PREVIEW_TOKEN="opaque-preview"), |
| 228 | + ), |
| 229 | + ( |
| 230 | + "management-token-header", |
| 231 | + self.client.get("/robots.txt", HTTP_X_MANAGEMENT_TOKEN="opaque-management"), |
| 232 | + self.client.head("/robots.txt", HTTP_X_MANAGEMENT_TOKEN="opaque-management"), |
| 233 | + ), |
| 234 | + ) |
| 235 | + for credential_kind, get_response, head_response in credential_responses: |
| 236 | + for method, response in (("GET", get_response), ("HEAD", head_response)): |
| 237 | + with self.subTest(credential_kind=credential_kind, method=method): |
| 238 | + directives = cache_directives(response) |
| 239 | + self.assertTrue({"private", "no-store", "max-age=0"}.issubset(directives)) |
| 240 | + self.assertNotIn("public", directives) |
| 241 | + self.assertFalse( |
| 242 | + any( |
| 243 | + item.startswith("s-maxage=") and item != "s-maxage=0" |
| 244 | + for item in directives |
| 245 | + ) |
| 246 | + ) |
| 247 | + |
| 248 | + for cookie in ( |
| 249 | + "dtc_analytics_consent=v1.allow", |
| 250 | + "browser_timezone=Europe%2FBerlin", |
| 251 | + ): |
| 252 | + with self.subTest(cookie=cookie): |
| 253 | + preference = self.client.get("/robots.txt", HTTP_COOKIE=cookie) |
| 254 | + self.assertEqual(cache_directives(preference), {"max-age=0", "must-revalidate"}) |
| 255 | + self.assertNotIn("private", preference.headers["Cache-Control"]) |
171 | 256 |
|
172 | 257 | sitemap = self.client.get("/sitemap.xml") |
173 | 258 | self.assertEqual(sitemap.status_code, 200) |
|
0 commit comments