Skip to content

Commit 7bc64a9

Browse files
Tests - Added tests for Image and SocialLink. (#1138)
* Tests - Added backend tests for Image and SocialLink. * Turn back on type check and strict mode * Add in vite-plugin-checker to fix frontend typecheck * Remove always() after typecheck * Try to echo error from typecheck to fail workflow * Remove all if always() * Add back always() and remove strict from typecheck in build * Remove if always() to check * Fixes for all typecheck errors * Remove unneeded vite-plugin-checker * Update coverage threshold to 70 and misc formatting --------- Co-authored-by: Andrew Tavis McAllister <andrew.t.mcallister@gmail.com>
1 parent d10cc7d commit 7bc64a9

File tree

15 files changed

+584
-83
lines changed

15 files changed

+584
-83
lines changed

.github/workflows/pr_ci_backend.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,4 @@ jobs:
8080
- name: Run pytest - Unit Tests
8181
if: always()
8282
run: |
83-
pytest ./backend --cov=backend --cov-report=term-missing --cov-fail-under=60 --cov-config=./backend/pyproject.toml
83+
pytest ./backend --cov=backend --cov-report=term-missing --cov-fail-under=70 --cov-config=./backend/pyproject.toml

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ __pycache__
1717
venv/
1818
.mypy_cache
1919

20+
# Don't include migrations for now.
21+
*migrations/*
22+
!*migrations/__init__.py
23+
2024
# Testing
2125
.coverage
2226
cov_html/

backend/content/factories.py

+25-10
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
11
# SPDX-License-Identifier: AGPL-3.0-or-later
22
import datetime
33
import random
4+
from uuid import uuid4
45

56
import factory
67

7-
from content.models import Faq, Location, Resource, Task, Topic
8+
from content.models import Faq, Image, Location, Resource, Task, Topic
89

9-
# MARK: Main Tables
10-
11-
12-
class FaqFactory(factory.django.DjangoModelFactory):
13-
class Meta:
14-
model = Faq
15-
16-
name = factory.Faker("name")
17-
question = factory.Faker("text")
10+
# MARK: Main Table
1811

1912

2013
class EntityLocationFactory(factory.django.DjangoModelFactory):
@@ -111,6 +104,28 @@ def location(self, create, extracted, **kwargs):
111104
self.display_name = random_locations[self.location_idx][3]
112105

113106

107+
class FaqFactory(factory.django.DjangoModelFactory):
108+
class Meta:
109+
model = Faq
110+
111+
name = factory.Faker("name")
112+
question = factory.Faker("text")
113+
114+
115+
class ImageFactory(factory.django.DjangoModelFactory):
116+
class Meta:
117+
model = Image
118+
119+
# Generate a UUID automatically for each image instance.
120+
id = factory.LazyFunction(uuid4)
121+
122+
# Create a dummy image file for the file_object field.
123+
file_object = factory.django.ImageField(upload_to="images/")
124+
125+
# Use Faker to generate a random creation date within the last 10 years.
126+
creation_date = factory.Faker("date_time_this_decade")
127+
128+
114129
class ResourceFactory(factory.django.DjangoModelFactory):
115130
class Meta:
116131
model = Resource

backend/content/models.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ def __str__(self) -> str:
4343
class Image(models.Model):
4444
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
4545
file_object = models.ImageField(
46-
upload_to="images/",
47-
validators=[validate_image_file_extension]
46+
upload_to="images/", validators=[validate_image_file_extension]
4847
)
4948
creation_date = models.DateTimeField(auto_now_add=True)
5049

backend/content/tests/test_image.py

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# SPDX-License-Identifier: AGPL-3.0-or-later
2+
"""
3+
Testing for Image upload-related functionality.
4+
"""
5+
6+
import os
7+
from datetime import datetime
8+
from typing import Generator
9+
10+
import pytest
11+
from django.conf import settings
12+
from rest_framework.test import APIClient
13+
14+
from communities.organizations.factories import OrganizationFactory
15+
from content.factories import ImageFactory
16+
from content.models import Image
17+
from content.serializers import ImageSerializer
18+
19+
# Adjust MEDIA_ROOT for testing (temporary directory)
20+
MEDIA_ROOT = settings.MEDIA_ROOT # Ensure this points to the imagefolder
21+
22+
23+
@pytest.fixture
24+
def image_with_file() -> Generator[Image, None, None]:
25+
"""
26+
Fixture for creating an image with a file.
27+
Deletes the file after the test.
28+
"""
29+
# Clean up any leftover files.
30+
image = ImageFactory() # create image using the factory
31+
yield image
32+
33+
# Cleanup after the test.
34+
file_path = os.path.join(settings.MEDIA_ROOT, image.file_object.name)
35+
if os.path.exists(file_path):
36+
os.remove(file_path)
37+
38+
39+
@pytest.mark.django_db
40+
def test_image_creation(image_with_file: Image) -> None:
41+
"""
42+
Test the creation of an image with a file.
43+
This is like a Model test.
44+
"""
45+
image = image_with_file
46+
47+
# Check if the file exists in MEDIA_ROOT.
48+
file_path = os.path.join(settings.MEDIA_ROOT, image.file_object.name)
49+
assert os.path.exists(file_path)
50+
51+
assert image.id is not None
52+
assert image.file_object.name.endswith(".jpg")
53+
assert isinstance(image.creation_date, datetime)
54+
55+
56+
@pytest.mark.django_db
57+
def test_image_serializer(image_with_file: Image) -> None:
58+
"""
59+
Test the serializer with a file.
60+
"""
61+
image = image_with_file
62+
serializer = ImageSerializer(image)
63+
64+
assert serializer.data["id"] == str(image.id)
65+
assert "file_object" in serializer.data
66+
assert "creation_date" in serializer.data
67+
68+
# Check if the file exists in MEDIA_ROOT.
69+
file_path = os.path.join(settings.MEDIA_ROOT, image.file_object.name)
70+
assert os.path.exists(file_path)
71+
72+
73+
@pytest.mark.django_db
74+
def test_image_serializer_missing_file() -> None:
75+
"""
76+
Test the serializer with a missing file.
77+
"""
78+
serializer = ImageSerializer(data={}) # no file_object
79+
assert not serializer.is_valid()
80+
assert "file_object" in serializer.errors
81+
82+
83+
@pytest.mark.django_db
84+
def test_image_list_view(client: APIClient) -> None:
85+
"""
86+
Test the list view for images.
87+
This is like a GET request.
88+
"""
89+
images = ImageFactory.create_batch(3)
90+
filenames = [image.file_object.name for image in images]
91+
92+
response = client.get("/v1/content/images/")
93+
94+
assert response.status_code == 200
95+
assert response.json()["count"] == 3
96+
97+
# Test cleanup. Delete files from the filesystem.
98+
for filename in filenames:
99+
file_path = os.path.join(settings.MEDIA_ROOT, filename)
100+
if os.path.exists(file_path):
101+
os.remove(file_path)
102+
103+
104+
@pytest.mark.django_db
105+
def test_image_create_view(client: APIClient, image_with_file: Image) -> None:
106+
"""
107+
Test the create view for images.
108+
This is like a POST request.
109+
"""
110+
# Use the 'image_with_file' fixture to create an image.
111+
# This is "the file on the user's computer". The rest of the test is "uploading it to the server".
112+
# That is the default, correct behavior. The fixture will delete the file after the test.
113+
image = image_with_file
114+
115+
# Image.objects (there should be only one) in the database are a side effect of the fixture. Delete them here.
116+
# The client.post call will create a file "on the server" and a database entry.
117+
Image.objects.all().delete()
118+
119+
org = OrganizationFactory()
120+
assert org is not None, "Organization was not created"
121+
122+
data = {"organization_id": str(org.id), "file_object": image.file_object}
123+
124+
# Test the upload behavior. Make the POST request to the image create endpoint.
125+
# This will create a second copy of the image file in the media root.
126+
# This is correct, default, upload behavior. POST is supposed to put a file on the server and add an Image entry to the database.
127+
response = client.post("/v1/content/images/", data, format="multipart")
128+
129+
assert (
130+
response.status_code == 201
131+
), f"Expected status code 201, but got {response.status_code}."
132+
assert (
133+
Image.objects.count() == 1
134+
), "Expected one image in the database, but found more than one."
135+
136+
# Check if the file was "uploaded".
137+
uploaded_file = os.path.join(settings.MEDIA_ROOT, image.file_object.name)
138+
assert os.path.exists(uploaded_file)
139+
140+
# Get the actual filename of the "uploaded" file, so we can delete it from the file system.
141+
# This is for test cleanup. In production, the file is not deleted.
142+
file_object = response.json()["fileObject"].split("/")[-1]
143+
file_to_delete = os.path.join(settings.MEDIA_ROOT, "images", file_object)
144+
if os.path.exists(file_to_delete):
145+
os.remove(file_to_delete)
146+
147+
148+
@pytest.mark.django_db
149+
def test_image_create_missing_file(client: APIClient) -> None:
150+
"""
151+
Test the create view for images with a missing file.
152+
"""
153+
154+
org = OrganizationFactory()
155+
data = {"organization_id": str(org.id)} # no file_object provided
156+
response = client.post("/v1/content/images/", data, format="multipart")
157+
158+
assert response.status_code == 400
159+
assert "fileObject" in response.json()
160+
assert "No file was submitted." in response.json()["fileObject"]

0 commit comments

Comments
 (0)