mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-07-30 23:38:02 -04:00
291 lines
11 KiB
Python
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)
|