Files
AdventureLog/backend/server/adventures/tests/test_image_metadata.py

291 lines
11 KiB
Python

from unittest.mock import patch
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from rest_framework.test import APIRequestFactory
from adventures.models import ContentImage, Location
from adventures.serializers import ContentImageSerializer
from adventures.services.images.metadata import (
ImageSource,
create_content_image,
extract_gps_from_bytes,
infer_source_from_url,
resolve_image_metadata,
_dms_to_decimal,
)
from adventures.utils.geo import make_point, point_to_lat_lon
from users.models import CustomUser
class ImageSourceInferenceTests(TestCase):
def test_google_url(self):
self.assertEqual(
infer_source_from_url(
'https://places.googleapis.com/v1/photo/media?key=test'
),
ImageSource.GOOGLE,
)
def test_wikipedia_url(self):
self.assertEqual(
infer_source_from_url('https://upload.wikimedia.org/wikipedia/commons/a/a1/test.jpg'),
ImageSource.WIKIPEDIA,
)
def test_generic_url(self):
self.assertEqual(
infer_source_from_url('https://example.com/photo.jpg'),
ImageSource.URL,
)
def test_spoofed_hostnames_are_not_matched(self):
spoofed_urls = [
'https://evil.googleapis.com.attacker.com/photo.jpg',
'https://notgoogleusercontent.com/photo.jpg',
'https://evil.wikimedia.org.attacker.com/photo.jpg',
'https://notwikipedia.org/photo.jpg',
]
for url in spoofed_urls:
with self.subTest(url=url):
self.assertEqual(infer_source_from_url(url), ImageSource.URL)
class ImageMetadataResolutionTests(TestCase):
def test_explicit_source_overrides_url_inference(self):
metadata = resolve_image_metadata(
source_url='https://upload.wikimedia.org/wikipedia/commons/x.jpg',
explicit_source=ImageSource.GOOGLE,
)
self.assertEqual(metadata['source'], ImageSource.GOOGLE)
def test_immich_id_sets_immich_source(self):
metadata = resolve_image_metadata(immich_id='abc-123')
self.assertEqual(metadata['source'], ImageSource.IMMICH)
def test_extract_gps_returns_none_for_non_gps_image(self):
from PIL import Image
import io
buffer = io.BytesIO()
Image.new('RGB', (4, 4), color='blue').save(buffer, format='JPEG')
self.assertIsNone(extract_gps_from_bytes(buffer.getvalue()))
@patch('adventures.services.images.metadata.fetch_immich_coordinates')
def test_resolve_metadata_falls_back_to_immich_when_file_has_no_gps(
self, mock_fetch_immich_coordinates
):
from PIL import Image
import io
buffer = io.BytesIO()
Image.new('RGB', (4, 4), color='blue').save(buffer, format='JPEG')
file_bytes = buffer.getvalue()
immich_point = make_point(-70.6473, 44.9673)
mock_fetch_immich_coordinates.return_value = immich_point
mock_integration = object()
metadata = resolve_image_metadata(
file_bytes=file_bytes,
immich_id='abc-123',
immich_integration=mock_integration,
)
self.assertEqual(metadata['source'], ImageSource.IMMICH)
self.assertEqual(metadata['coordinates'], immich_point)
mock_fetch_immich_coordinates.assert_called_once_with(mock_integration, 'abc-123')
class DmsConversionTests(TestCase):
def test_ifdrational_components(self):
from PIL.TiffImagePlugin import IFDRational
values = (IFDRational(44, 1), IFDRational(58, 2), IFDRational(1234, 100))
result = _dms_to_decimal(values, 'N')
self.assertIsNotNone(result)
self.assertAlmostEqual(result, 44.486761111111115, places=4)
def test_tuple_rational_components(self):
values = ((44, 1), (58, 2), (1234, 100))
result = _dms_to_decimal(values, 'N')
self.assertIsNotNone(result)
self.assertAlmostEqual(result, 44.486761111111115, places=4)
def test_bytes_ref_applies_hemisphere(self):
values = ((44, 1), (58, 1), (0, 1))
north = _dms_to_decimal(values, b'N')
south = _dms_to_decimal(values, b'S')
self.assertAlmostEqual(north, 44.96666666666667, places=4)
self.assertAlmostEqual(south, -44.96666666666667, places=4)
def test_extract_gps_from_jpeg_with_exif(self):
from PIL import Image
import io
try:
import piexif
except ImportError:
self.skipTest('piexif is not installed')
lat, lon = 44.9673, -70.6473
def to_deg(value, refs):
abs_value = abs(value)
deg = int(abs_value)
t1 = (abs_value - deg) * 60
min_ = int(t1)
sec = round((t1 - min_) * 60 * 100)
return ((deg, 1), (min_, 1), (sec, 100)), refs[0 if value < 0 else 1]
lat_deg = to_deg(lat, ['S', 'N'])
lon_deg = to_deg(lon, ['W', 'E'])
exif_dict = {
'GPS': {
piexif.GPSIFD.GPSLatitudeRef: lat_deg[1],
piexif.GPSIFD.GPSLatitude: lat_deg[0],
piexif.GPSIFD.GPSLongitudeRef: lon_deg[1],
piexif.GPSIFD.GPSLongitude: lon_deg[0],
}
}
exif_bytes = piexif.dump(exif_dict)
buffer = io.BytesIO()
Image.new('RGB', (4, 4), color='blue').save(buffer, format='JPEG', exif=exif_bytes)
point = extract_gps_from_bytes(buffer.getvalue())
self.assertIsNotNone(point)
extracted_lat, extracted_lon = point_to_lat_lon(point)
self.assertAlmostEqual(extracted_lat, lat, places=3)
self.assertAlmostEqual(extracted_lon, lon, places=3)
class CreateContentImageTests(TestCase):
def setUp(self):
self.user = CustomUser.objects.create_user(
username='image-user',
email='image-user@example.com',
password='testpassword123',
)
self.location = Location.objects.create(user=self.user, name='Test Location')
self.content_type = ContentType.objects.get_for_model(Location)
def test_create_with_explicit_google_source(self):
from django.core.files.base import ContentFile
from PIL import Image
import io
buffer = io.BytesIO()
Image.new('RGB', (4, 4), color='red').save(buffer, format='JPEG')
file_bytes = buffer.getvalue()
image = create_content_image(
user=self.user,
content_type=self.content_type,
object_id=str(self.location.id),
image_file=ContentFile(file_bytes, name='test.jpg'),
file_bytes=file_bytes,
explicit_source=ImageSource.GOOGLE,
source_url='https://places.googleapis.com/v1/photo/media',
)
image.refresh_from_db()
self.assertEqual(image.source, ImageSource.GOOGLE)
self.assertEqual(image.source_url, 'https://places.googleapis.com/v1/photo/media')
def test_create_with_coordinates(self):
from django.core.files.base import ContentFile
from PIL import Image
import io
buffer = io.BytesIO()
Image.new('RGB', (4, 4), color='red').save(buffer, format='JPEG')
file_bytes = buffer.getvalue()
point = make_point(2.3522, 48.8566)
image = create_content_image(
user=self.user,
content_type=self.content_type,
object_id=str(self.location.id),
image_file=ContentFile(file_bytes, name='test.jpg'),
file_bytes=file_bytes,
coordinates=point,
)
image.refresh_from_db()
lat, lon = point_to_lat_lon(image.coordinates)
self.assertAlmostEqual(lat, 48.8566, places=4)
self.assertAlmostEqual(lon, 2.3522, places=4)
class ContentImageSerializerMetadataTests(TestCase):
def setUp(self):
self.user = CustomUser.objects.create_user(
username='serializer-user',
email='serializer-user@example.com',
password='testpassword123',
)
self.factory = APIRequestFactory()
self.request = self.factory.get('/')
self.request.user = self.user
def test_serializer_exposes_source_and_coordinates(self):
from django.core.files.base import ContentFile
from PIL import Image
import io
buffer = io.BytesIO()
Image.new('RGB', (4, 4), color='red').save(buffer, format='JPEG')
image = ContentImage.objects.create(
user=self.user,
content_type=ContentType.objects.get_for_model(Location),
object_id='00000000-0000-0000-0000-000000000001',
image=ContentFile(buffer.getvalue(), name='test.jpg'),
source=ContentImage.Source.WIKIPEDIA,
source_url='https://upload.wikimedia.org/wikipedia/commons/test.jpg',
coordinates=make_point(-73.968285, 40.785091),
)
data = ContentImageSerializer(image, context={'request': self.request}).data
self.assertIsNotNone(data)
self.assertEqual(data['source'], ContentImage.Source.WIKIPEDIA)
self.assertEqual(data['source_url'], 'https://upload.wikimedia.org/wikipedia/commons/test.jpg')
self.assertAlmostEqual(data['latitude'], 40.785091, places=4)
self.assertAlmostEqual(data['longitude'], -73.968285, places=4)
class ImageMapPinSerializerTests(TestCase):
def setUp(self):
self.user = CustomUser.objects.create_user(
username='pin-user',
email='pin-user@example.com',
password='testpassword123',
)
def test_map_pin_serializer_includes_parent_fields(self):
from django.core.files.base import ContentFile
from PIL import Image
import io
location = Location.objects.create(
user=self.user,
name='GPS Place',
coordinates=make_point(2.0, 48.0),
)
buffer = io.BytesIO()
Image.new('RGB', (4, 4), color='blue').save(buffer, format='JPEG')
image = ContentImage.objects.create(
user=self.user,
content_type=ContentType.objects.get_for_model(Location),
object_id=str(location.id),
image=ContentFile(buffer.getvalue(), name='gps.jpg'),
coordinates=make_point(-122.4194, 37.7749),
source=ContentImage.Source.UPLOAD,
)
from adventures.serializers import ImageMapPinSerializer
data = ImageMapPinSerializer(image).data
self.assertIsNotNone(data)
self.assertEqual(data['parent_type'], 'location')
self.assertEqual(data['parent_id'], str(location.id))
self.assertEqual(data['parent_name'], 'GPS Place')
self.assertAlmostEqual(data['latitude'], 37.7749, places=4)
self.assertAlmostEqual(data['longitude'], -122.4194, places=4)