Add support for modern image formats (WebP and AVIF) and implement image conversion commands

This commit is contained in:
Joakim Hellsén 2026-02-12 21:29:17 +01:00
commit 477bb753ae
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
15 changed files with 629 additions and 93 deletions

View file

@ -0,0 +1,227 @@
"""Tests for custom image template tags."""
from __future__ import annotations
from django.template import Context
from django.template import Template
from django.utils.safestring import SafeString
from twitch.templatetags.image_tags import get_format_url
from twitch.templatetags.image_tags import picture
class TestGetFormatUrl:
"""Tests for the get_format_url helper function."""
def test_empty_url(self) -> None:
"""Test that empty URL returns empty string."""
assert not get_format_url("", "webp")
def test_jpg_to_webp(self) -> None:
"""Test converting JPG to WebP."""
assert get_format_url("/static/img/banner.jpg", "webp") == "/static/img/banner.webp"
def test_jpeg_to_avif(self) -> None:
"""Test converting JPEG to AVIF."""
assert get_format_url("/static/img/photo.jpeg", "avif") == "/static/img/photo.avif"
def test_png_to_webp(self) -> None:
"""Test converting PNG to WebP."""
assert get_format_url("/static/img/logo.png", "webp") == "/static/img/logo.webp"
def test_uppercase_extension(self) -> None:
"""Test converting uppercase extensions."""
assert get_format_url("/static/img/photo.JPG", "webp") == "/static/img/photo.webp"
def test_non_convertible_format(self) -> None:
"""Test that non-convertible formats return unchanged."""
svg_url = "/static/img/icon.svg"
assert get_format_url(svg_url, "webp") == svg_url
gif_url = "/static/img/animated.gif"
assert get_format_url(gif_url, "avif") == gif_url
def test_url_with_query_params(self) -> None:
"""Test URL with query parameters preserves them."""
result = get_format_url("/static/img/photo.jpg?v=123", "webp")
assert result == "/static/img/photo.webp?v=123"
def test_full_url(self) -> None:
"""Test full URL with domain."""
result: str = get_format_url("https://example.com/img/photo.jpg", "avif")
assert result == "https://example.com/img/photo.avif"
class TestPictureTag:
"""Tests for the picture template tag."""
def test_empty_src(self) -> None:
"""Test that empty src returns empty string."""
result: SafeString = picture("")
assert not result
def test_basic_picture(self) -> None:
"""Test basic picture tag with minimal parameters."""
result: SafeString = picture("/static/img/photo.jpg")
# Should contain picture element
assert "<picture>" in result
assert "</picture>" in result
# Should have AVIF and WebP sources
assert '<source srcset="/static/img/photo.avif" type="image/avif" />' in result
assert '<source srcset="/static/img/photo.webp" type="image/webp" />' in result
# Should have fallback img tag
assert '<img src="/static/img/photo.jpg"' in result
assert 'loading="lazy"' in result
def test_all_attributes(self) -> None:
"""Test picture tag with all optional attributes."""
result: SafeString = picture(
src="/static/img/photo.jpg",
alt="Test photo",
width=800,
height=600,
loading="eager",
css_class="rounded shadow",
style="max-width: 100%",
)
assert 'alt="Test photo"' in result
assert 'width="800"' in result
assert 'height="600"' in result
assert 'loading="eager"' in result
assert 'class="rounded shadow"' in result
assert 'style="max-width: 100%"' in result
def test_non_convertible_format(self) -> None:
"""Test that non-convertible formats don't generate source tags."""
result: SafeString = picture("/static/img/icon.svg")
# Should not have source tags since SVG can't be converted
assert "<source" not in result
# Should still have picture and img tags
assert "<picture>" in result
assert '<img src="/static/img/icon.svg"' in result
assert "</picture>" in result
def test_xss_prevention_in_src(self) -> None:
"""Test that XSS attempts in src are escaped."""
malicious_src = '"><script>alert("xss")</script><img src="'
result = picture(malicious_src)
# Should escape the malicious code
assert "<script>" not in result
assert "</script>" not in result
assert "&quot;" in result
def test_xss_prevention_in_alt(self) -> None:
"""Test that XSS attempts in alt text are escaped."""
result: SafeString = picture(
"/static/img/photo.jpg",
alt='"><script>alert("xss")</script>',
)
# Should escape the malicious code
assert "<script>" not in result
assert "</script>" not in result
assert "&quot;" in result
def test_xss_prevention_in_css_class(self) -> None:
"""Test that XSS attempts in CSS class are escaped."""
result: SafeString = picture(
"/static/img/photo.jpg",
css_class='"><script>alert("xss")</script>',
)
# Should escape the malicious code
assert "<script>" not in result
assert "</script>" not in result
def test_xss_prevention_in_style(self) -> None:
"""Test that XSS attempts in style are escaped."""
result: SafeString = picture(
"/static/img/photo.jpg",
style='"><script>alert("xss")</script>',
)
# Should escape the malicious code
assert "<script>" not in result
assert "</script>" not in result
def test_returns_safestring(self) -> None:
"""Test that the result is a SafeString."""
result: SafeString = picture("/static/img/photo.jpg")
assert isinstance(result, SafeString)
def test_alt_empty_string(self) -> None:
"""Test that alt="" includes empty alt attribute."""
result: SafeString = picture("/static/img/photo.jpg", alt="")
assert 'alt=""' in result
def test_no_width_or_height(self) -> None:
"""Test that missing width/height are not included."""
result: SafeString = picture("/static/img/photo.jpg")
# Should not have width or height attributes
assert 'width="' not in result
assert 'height="' not in result
def test_url_with_query_params(self) -> None:
"""Test that query parameters are preserved in formats."""
result: SafeString = picture("/static/img/photo.jpg?v=123")
assert "/static/img/photo.avif?v=123" in result
assert "/static/img/photo.webp?v=123" in result
assert "/static/img/photo.jpg?v=123" in result
def test_full_url(self) -> None:
"""Test with full URL including domain."""
result: SafeString = picture("https://cdn.example.com/images/photo.jpg")
assert "https://cdn.example.com/images/photo.avif" in result
assert "https://cdn.example.com/images/photo.webp" in result
assert "https://cdn.example.com/images/photo.jpg" in result
class TestPictureTagTemplate:
"""Tests for the picture tag used in templates."""
def test_picture_tag_in_template(self) -> None:
"""Test that the picture tag works when called from a template."""
template = Template('{% load image_tags %}{% picture src="/img/photo.jpg" alt="Test" %}')
context = Context({})
result: SafeString = template.render(context)
assert "<picture>" in result
assert "</picture>" in result
assert '<source srcset="/img/photo.avif"' in result
assert '<source srcset="/img/photo.webp"' in result
assert '<img src="/img/photo.jpg"' in result
assert 'alt="Test"' in result
def test_picture_tag_with_context_variables(self) -> None:
"""Test using context variables in the picture tag."""
template = Template("{% load image_tags %}{% picture src=image_url alt=image_alt width=image_width %}")
context = Context({
"image_url": "/img/banner.png",
"image_alt": "Banner image",
"image_width": 1200,
})
result: SafeString = template.render(context)
assert '<img src="/img/banner.png"' in result
assert 'alt="Banner image"' in result
assert 'width="1200"' in result
assert "/img/banner.avif" in result
def test_picture_tag_with_no_parameters(self) -> None:
"""Test that the tag requires at least src parameter."""
# This should work with empty src but return empty string
template = Template('{% load image_tags %}{% picture src="" %}')
context = Context({})
result: SafeString = template.render(context)
assert not result.strip()