Limit file types of user and news images

- Add file type validator
- Apply validator to user image
- Save validator class to variable
- Add image validator to news image model field
- Add file size validator
- Enable file size validator on news and user model image fields
- Fix test
This commit is contained in:
Lacey Williams Henschel
2023-11-02 11:45:52 -07:00
committed by Lacey Henschel
parent 5f4146cbdf
commit 7507fa50b3
9 changed files with 261 additions and 9 deletions

View File

@@ -0,0 +1,53 @@
import pytest
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from ..validators import FileTypeValidator, MaxFileSizeValidator
def test_file_type_validator():
"""
Eensure validator only allows specific file extensions.
"""
validator = FileTypeValidator(extensions=[".jpg", ".png"])
# Valid file types
valid_file = SimpleUploadedFile(
"test.jpg", b"file_content", content_type="image/jpeg"
)
validator(valid_file)
valid_file = SimpleUploadedFile(
"test.png", b"file_content", content_type="image/jpeg"
)
validator(valid_file)
# Invalid file type
invalid_file = SimpleUploadedFile(
"test.txt", b"file_content", content_type="text/plain"
)
with pytest.raises(ValidationError):
validator(invalid_file)
def test_max_file_size_validator():
"""
Ensure the validator enforces the file size limit.
"""
# 1MB max file size
validator = MaxFileSizeValidator(max_size=1 * 1024 * 1024)
# Valid file size
valid_file = SimpleUploadedFile(
"small.jpg", b"a" * (1 * 1024 * 1024 - 1), content_type="image/jpeg"
)
validator(valid_file) # Should not raise
# Invalid file size
invalid_file = SimpleUploadedFile(
"large.jpg", b"a" * (1 * 1024 * 1024 + 1), content_type="image/jpeg"
)
with pytest.raises(ValidationError) as exc_info:
validator(invalid_file)
assert "File too large" in str(exc_info.value)

44
core/validators.py Normal file
View File

@@ -0,0 +1,44 @@
import os
from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
@deconstructible
class FileTypeValidator:
"""
Validates that a file is of the permitted types.
"""
def __init__(self, extensions):
self.extensions = extensions
def __call__(self, value):
ext = os.path.splitext(value.name)[1].lower()
if ext not in self.extensions:
raise ValidationError(
f"Unsupported file extension. Allowed types are: {', '.join(self.extensions)}" # noqa
)
image_validator = FileTypeValidator(extensions=[".jpg", ".jpeg", ".png"])
@deconstructible
class MaxFileSizeValidator:
"""
Validates that a file is not larger than a certain size.
"""
def __init__(self, max_size):
self.max_size = max_size
def __call__(self, value):
if value.size > self.max_size:
raise ValidationError(
f"File too large. Size should not exceed {self.max_size / 1024 / 1024} MB." # noqa
)
# 1 MB max file size
max_file_size_validator = MaxFileSizeValidator(max_size=1 * 1024 * 1024)

View File

@@ -0,0 +1,28 @@
# Generated by Django 4.2.2 on 2023-11-02 19:32
import core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("news", "0007_alter_entry_external_url_alter_entry_slug"),
]
operations = [
migrations.AlterField(
model_name="entry",
name="image",
field=models.ImageField(
blank=True,
null=True,
upload_to="news/%Y/%m/",
validators=[
core.validators.FileTypeValidator(
extensions=[".jpg", ".jpeg", ".png"]
),
core.validators.MaxFileSizeValidator(max_size=1048576),
],
),
),
]

View File

@@ -7,6 +7,8 @@ from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from core.validators import image_validator, max_file_size_validator
from . import acl
User = get_user_model()
@@ -67,7 +69,12 @@ class Entry(models.Model):
related_name="moderated_entries_set",
)
external_url = models.URLField(_("URL"), blank=True, default="", max_length=500)
image = models.ImageField(upload_to="news/%Y/%m/", null=True, blank=True)
image = models.ImageField(
upload_to="news/%Y/%m/",
null=True,
blank=True,
validators=[image_validator, max_file_size_validator],
)
created_at = models.DateTimeField(default=now)
approved_at = models.DateTimeField(null=True, blank=True)
modified_at = models.DateTimeField(auto_now=True)

View File

@@ -2,6 +2,8 @@ import datetime
import pytest
from django.contrib.auth.models import Permission
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.exceptions import ValidationError
from django.utils.timezone import now
from model_bakery import baker
@@ -62,6 +64,54 @@ def test_entry_absolute_url(tp):
assert entry.get_absolute_url() == tp.reverse("news-detail", "the-slug")
def test_entry_model_image_validator(tp):
"""
Test that the `image` field only accepts certain file types.
"""
author = baker.make("users.User")
entry = Entry.objects.create(title="😀 Foo Bar Baz!@! +", author=author)
# Valid image file
valid_image = SimpleUploadedFile(
"test.jpg", b"file_content", content_type="image/jpeg"
)
entry.image = valid_image
# This should not raise any errors
entry.full_clean()
# Invalid image file
invalid_image = SimpleUploadedFile(
"test.pdf", b"file_content", content_type="application/pdf"
)
entry.image = invalid_image
# This should raise a ValidationError
with pytest.raises(ValidationError):
entry.full_clean()
def test_entry_model_image_file_size(tp):
"""
Test that the `image` field rejects files larger than a specific size.
"""
author = baker.make("users.User")
entry = Entry.objects.create(title="😀 Foo Bar Baz!@! +", author=author)
valid_image = SimpleUploadedFile(
"test.jpg", b"a" * (1 * 1024 * 1024 - 1), content_type="image/jpeg"
)
entry.image = valid_image
# This should not raise any errors
entry.full_clean()
# This should fail (just over 1MB)
invalid_image = SimpleUploadedFile(
"too_large.jpg", b"a" * (1 * 1024 * 1024 + 1), content_type="image/jpeg"
)
entry.image = invalid_image
# This should raise a ValidationError for file size
with pytest.raises(ValidationError):
entry.full_clean()
def test_approve_entry(make_entry):
future = now() + datetime.timedelta(hours=1)
entry = make_entry(approved=False, publish_at=future)

View File

@@ -418,7 +418,7 @@ def test_news_create_post(
b"GIF89a\x01\x00\x01\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00"
b"\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x01\x00\x00"
)
img.name = f"random-value-{uuid.uuid4()}.gif"
img.name = f"random-value-{uuid.uuid4()}.png"
data["image"] = img
data["publish_at"] = right_now = now()

View File

@@ -0,0 +1,28 @@
# Generated by Django 4.2.2 on 2023-11-02 19:32
import core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0010_preferences"),
]
operations = [
migrations.AlterField(
model_name="user",
name="image",
field=models.FileField(
blank=True,
null=True,
upload_to="profile-images",
validators=[
core.validators.FileTypeValidator(
extensions=[".jpg", ".jpeg", ".png"]
),
core.validators.MaxFileSizeValidator(max_size=1048576),
],
),
),
]

View File

@@ -16,6 +16,8 @@ from django.dispatch import receiver
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from core.validators import image_validator, max_file_size_validator
logger = logging.getLogger(__name__)
@@ -203,7 +205,12 @@ class User(BaseUser):
badges = models.ManyToManyField(Badge)
github_username = models.CharField(_("github username"), max_length=100, blank=True)
image = models.FileField(upload_to="profile-images", null=True, blank=True)
image = models.FileField(
upload_to="profile-images",
null=True,
blank=True,
validators=[image_validator, max_file_size_validator],
)
claimed = models.BooleanField(
_("claimed"),
default=True,

View File

@@ -1,6 +1,8 @@
import pytest
from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.exceptions import ValidationError
from pytest_django.asserts import assertQuerySetEqual
from ..models import Preferences
@@ -50,14 +52,47 @@ def test_get_display_name(user):
assert user.get_display_name == "Last"
@pytest.mark.skip("Add this test when I have the patience for mocks")
def test_user_save_image_from_github(user):
def test_user_model_image_validator(user):
"""
Test `User.save_image_from_github(avatar_url)`
See test_signals -- you will need to do something similar here, but
dealing with a File object might make it trickier.
Test that the `image` field on the User model only accepts certain file types.
"""
pass
# Valid image file
valid_image = SimpleUploadedFile(
"test.jpg", b"file_content", content_type="image/jpeg"
)
user.image = valid_image
# This should not raise any errors
user.full_clean()
# Invalid image file
invalid_image = SimpleUploadedFile(
"test.pdf", b"file_content", content_type="application/pdf"
)
user.image = invalid_image
# This should raise a ValidationError
with pytest.raises(ValidationError):
user.full_clean()
def test_user_model_image_file_size(user):
"""
Test that the `image` field rejects files larger than a specific size.
"""
valid_image = SimpleUploadedFile(
"test.jpg", b"a" * (1 * 1024 * 1024 - 1), content_type="image/jpeg"
)
user.image = valid_image
# This should not raise any errors
user.full_clean()
# This should fail (just over 1MB)
invalid_image = SimpleUploadedFile(
"too_large.jpg", b"a" * (1 * 1024 * 1024 + 1), content_type="image/jpeg"
)
user.image = invalid_image
# This should raise a ValidationError for file size
with pytest.raises(ValidationError):
user.full_clean()
def test_claim(user):