Skip to content

Commit a0a667c

Browse files
Use new pytest-jinja-check plugin
1 parent 7465fdf commit a0a667c

4 files changed

Lines changed: 91 additions & 176 deletions

File tree

main.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ async def rate_limit_error_handler(request: Request, exc: RateLimitError):
100100
response = templates.TemplateResponse(
101101
request,
102102
"errors/error.html",
103-
{"status_code": 429, "detail": exc.detail, "user": user},
103+
{"status_code": 429, "detail": exc.detail, "errors": None, "user": user},
104104
status_code=429,
105105
)
106106
response.headers["Retry-After"] = str(exc.retry_after)
@@ -119,7 +119,7 @@ async def credentials_exception_handler(request: Request, exc: CredentialsError)
119119
return templates.TemplateResponse(
120120
request,
121121
"errors/error.html",
122-
{"status_code": exc.status_code, "detail": exc.detail, "user": user},
122+
{"status_code": exc.status_code, "detail": exc.detail, "errors": None, "user": user},
123123
status_code=exc.status_code,
124124
)
125125

@@ -174,6 +174,7 @@ async def password_validation_exception_handler(
174174
"errors/error.html",
175175
{
176176
"status_code": 422,
177+
"detail": None,
177178
"errors": {field.replace("_", " ").title(): message},
178179
"user": user
179180
},
@@ -238,6 +239,7 @@ async def validation_exception_handler(
238239
"errors/error.html",
239240
{
240241
"status_code": 422,
242+
"detail": None,
241243
"errors": errors,
242244
"user": user
243245
},
@@ -258,7 +260,7 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException):
258260
return templates.TemplateResponse(
259261
request,
260262
"errors/error.html",
261-
{"status_code": exc.status_code, "detail": exc.detail, "user": user},
263+
{"status_code": exc.status_code, "detail": exc.detail, "errors": None, "user": user},
262264
status_code=exc.status_code,
263265
)
264266

@@ -283,6 +285,7 @@ async def general_exception_handler(request: Request, exc: Exception):
283285
{
284286
"status_code": 500,
285287
"detail": "Internal Server Error",
288+
"errors": None,
286289
"user": user
287290
},
288291
status_code=500,

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dev = [
3333
"sqlalchemy-schemadisplay<3.0,>=2.0",
3434
"ty>=0.0.21",
3535
"ruff>=0.15.5",
36+
"pytest-jinja-check[fastapi]>=1.0.2",
3637
]
3738

3839
[tool.ty.rules]

tests/test_templates.py

Lines changed: 20 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,27 @@
1-
import re
21
from pathlib import Path
3-
from typing import Set
4-
import jinja2
5-
from jinja2 import meta, Environment
6-
from jinja2 import nodes
72
import pytest
83

94

10-
def get_all_template_files():
11-
"""Recursively find all template files in the templates directory"""
12-
template_dir = Path("templates")
13-
return list(template_dir.glob("**/*.html"))
14-
15-
16-
def test_no_hardcoded_routes():
17-
"""Test that templates don't contain hardcoded routes"""
18-
template_files = get_all_template_files()
19-
20-
# Make sure we found some templates
21-
assert len(template_files) > 0, "No template files found"
22-
23-
# Patterns to look for hardcoded routes
24-
# We're looking for attributes that might contain routes but don't use url_for
25-
patterns = [
26-
r'action\s*=\s*["\'](?!{{.*?url_for.*?}})[^"\']*?/', # action attribute with relative path
27-
r'hx-get\s*=\s*["\'](?!{{.*?url_for.*?}})[^"\']*?/', # hx-get with relative path
28-
r'hx-post\s*=\s*["\'](?!{{.*?url_for.*?}})[^"\']*?/', # hx-post with relative path
29-
r'hx-put\s*=\s*["\'](?!{{.*?url_for.*?}})[^"\']*?/', # hx-put with relative path
30-
r'hx-patch\s*=\s*["\'](?!{{.*?url_for.*?}})[^"\']*?/', # hx-patch with relative path
31-
r'hx-delete\s*=\s*["\'](?!{{.*?url_for.*?}})[^"\']*?/', # hx-delete with relative path
32-
r'href\s*=\s*["\'](?!{{.*?url_for.*?}}|#|https?://|mailto:|tel:)[^"\']*?/', # href with relative path
33-
]
34-
35-
# Compile the patterns for better performance
36-
compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in patterns]
37-
38-
# Check each template file
39-
for template_file in template_files:
40-
with open(template_file, 'r') as f:
41-
content = f.read()
42-
43-
for i, pattern in enumerate(compiled_patterns):
44-
matches = pattern.findall(content)
45-
if matches:
46-
attribute = patterns[i].split(r'\s*=')[0].replace(r'\\', '').replace(r'\s*', '')
47-
assert False, f"Hardcoded route found in {template_file}: {attribute}={matches[0]}"
48-
49-
50-
def extract_template_variables(template_path: Path) -> Set[str]:
51-
"""
52-
Extract all undeclared variables from a Jinja2 template.
53-
54-
Args:
55-
template_path: Path to the template file
56-
57-
Returns:
58-
Set of variable names used in the template
59-
"""
60-
with open(template_path, 'r') as f:
61-
template_source = f.read()
62-
63-
env = Environment()
64-
try:
65-
ast = env.parse(template_source)
66-
variables = meta.find_undeclared_variables(ast)
67-
return variables
68-
except jinja2.exceptions.TemplateSyntaxError as e:
69-
pytest.fail(f"Syntax error in template {template_path}: {str(e)}")
70-
71-
72-
@pytest.mark.parametrize("template_file", get_all_template_files())
73-
def test_template_syntax(template_file: Path):
74-
"""Test that templates have valid Jinja2 syntax"""
75-
with open(template_file, 'r') as f:
76-
template_source = f.read()
77-
78-
env = Environment()
79-
try:
80-
# Just parse the template to check for syntax errors
81-
env.parse(template_source)
82-
# If we get here, the template has valid syntax
83-
assert True
84-
except jinja2.exceptions.TemplateSyntaxError as e:
85-
pytest.fail(f"Syntax error in template {template_file}: {str(e)}")
86-
87-
88-
@pytest.mark.parametrize("template_file", get_all_template_files())
89-
def test_extends_paths_are_valid(template_file: Path):
90-
"""Test that {% extends ... %} paths point to valid files."""
91-
with open(template_file, 'r') as f:
92-
template_source = f.read()
93-
94-
# Use a loader so Jinja2 knows the base directory for relative paths
95-
env = Environment(loader=jinja2.FileSystemLoader("templates"))
96-
try:
97-
ast = env.parse(template_source)
98-
# Find the extends node, if it exists
99-
extends_node = ast.find(nodes.Extends)
100-
101-
if extends_node:
102-
# Get the path specified in {% extends "..." %}
103-
# The template can be different types of expressions
104-
if isinstance(extends_node.template, nodes.Const):
105-
parent_template_path = extends_node.template.value
106-
else:
107-
# For other expression types, skip this test
108-
return
109-
110-
# Check if the resolved path exists relative to the templates dir
111-
full_path = Path("templates") / parent_template_path
112-
assert full_path.is_file(), (
113-
f"In {template_file}: extends path '{parent_template_path}' "
114-
f"does not point to a valid file ({full_path})"
115-
)
116-
# If no extends node, this test passes for this file
117-
except jinja2.exceptions.TemplateSyntaxError as e:
118-
# If syntax is invalid, this test fails, but test_template_syntax should catch it more specifically.
119-
# We fail here too to be explicit.
120-
pytest.fail(f"Syntax error in template {template_file}: {str(e)}")
121-
122-
123-
@pytest.mark.parametrize("template_file", get_all_template_files())
124-
def test_template_required_variables(template_file: Path):
125-
"""Test that we can identify required variables for each template"""
126-
# Extract variables from the template
127-
variables = extract_template_variables(template_file)
128-
129-
# Print the variables for debugging
130-
print(f"Template: {template_file}")
131-
print(f"Required variables: {variables}")
132-
133-
# TODO: Add tests to ensure that each route passes the required variables to the template
5+
def test_no_syntax_errors(template_syntax_errors):
6+
"""Test that all templates have valid Jinja2 syntax."""
7+
assert not template_syntax_errors, template_syntax_errors
8+
9+
10+
def test_no_hardcoded_routes(hardcoded_routes):
11+
"""Test that templates don't contain hardcoded routes."""
12+
assert not hardcoded_routes, hardcoded_routes
13+
14+
15+
def test_no_missing_context_variables(missing_context_variables):
16+
"""Test that routes pass all required variables to their templates."""
17+
assert not missing_context_variables, missing_context_variables
18+
19+
20+
def test_valid_endpoints(validate_endpoints):
21+
"""Test that url_for() calls in templates reference valid FastAPI endpoints."""
22+
from main import app
23+
errors = validate_endpoints(app)
24+
assert not errors, errors
13425

13526

13627

0 commit comments

Comments
 (0)