"""Tests pour le module metadata_mapper.""" from zoneinfo import ZoneInfo import pytest from json_to_metadata.metadata_mapper import ( format_gps_for_exif, map_frame_to_exif, parse_aperture, parse_date, parse_shutter_speed, ) class TestParseShutterSpeed: """Tests pour la fonction parse_shutter_speed.""" def test_fraction_format(self): """Parse une fraction simple.""" assert parse_shutter_speed('1/125') == pytest.approx(0.008, rel=0.01) assert parse_shutter_speed('1/1000') == pytest.approx(0.001, rel=0.01) assert parse_shutter_speed('1/60') == pytest.approx(0.0167, rel=0.01) def test_fraction_with_suffix(self): """Parse une fraction avec suffixe 's'.""" assert parse_shutter_speed('1/125s') == pytest.approx(0.008, rel=0.01) def test_whole_seconds(self): """Parse des temps en secondes entières.""" assert parse_shutter_speed('2') == 2.0 assert parse_shutter_speed('2s') == 2.0 assert parse_shutter_speed('30') == 30.0 def test_seconds_with_quote(self): """Parse des temps avec guillemet (notation photographique).""" assert parse_shutter_speed('2"') == 2.0 assert parse_shutter_speed("4'") == 4.0 assert parse_shutter_speed('30"') == 30.0 def test_bulb_mode(self): """Retourne None pour le mode Bulb.""" assert parse_shutter_speed('B') is None assert parse_shutter_speed('b') is None assert parse_shutter_speed('Bulb') is None assert parse_shutter_speed('bulb') is None def test_numeric_input(self): """Accepte des valeurs numériques directes.""" assert parse_shutter_speed(0.5) == 0.5 assert parse_shutter_speed(2) == 2.0 def test_decimal_string(self): """Parse des chaînes décimales.""" assert parse_shutter_speed('0.5') == 0.5 def test_invalid_format(self): """Lève une exception pour un format invalide.""" with pytest.raises(ValueError): parse_shutter_speed('fast') def test_division_by_zero(self): """Lève une exception pour une division par zéro.""" with pytest.raises(ValueError): parse_shutter_speed('1/0') class TestParseAperture: """Tests pour la fonction parse_aperture.""" def test_f_stop_format(self): """Parse le format f/X.X.""" assert parse_aperture('f/2.8') == 2.8 assert parse_aperture('f/5.6') == 5.6 assert parse_aperture('f/16') == 16.0 def test_f_format_no_slash(self): """Parse le format fX.X.""" assert parse_aperture('f2.8') == 2.8 assert parse_aperture('F5.6') == 5.6 def test_numeric_string(self): """Parse une chaîne numérique simple.""" assert parse_aperture('2.8') == 2.8 assert parse_aperture('11') == 11.0 def test_numeric_input(self): """Accepte des valeurs numériques directes.""" assert parse_aperture(2.8) == 2.8 assert parse_aperture(8) == 8.0 def test_invalid_format(self): """Lève une exception pour un format invalide.""" with pytest.raises(ValueError): parse_aperture('wide') class TestParseDate: """Tests pour la fonction parse_date.""" def test_iso_format_with_timezone(self): """Parse le format ISO avec timezone.""" result = parse_date('2024-03-15T14:30:00+02:00') assert result.year == 2024 assert result.month == 3 assert result.day == 15 assert result.hour == 14 assert result.minute == 30 assert result.tzinfo is not None def test_iso_format_without_timezone(self): """Parse le format ISO sans timezone (utilise Europe/Paris).""" result = parse_date('2024-03-15T14:30:00') assert result.year == 2024 assert result.month == 3 assert result.day == 15 assert result.tzinfo == ZoneInfo('Europe/Paris') def test_iso_format_with_z(self): """Parse le format ISO avec Z pour UTC.""" result = parse_date('2024-03-15T14:30:00Z') assert result.tzinfo is not None def test_iso_format_without_seconds(self): """Parse le format ISO sans secondes.""" result = parse_date('2025-11-16T16:47') assert result.year == 2025 assert result.month == 11 assert result.day == 16 assert result.hour == 16 assert result.minute == 47 assert result.second == 0 def test_date_only(self): """Parse une date sans heure.""" result = parse_date('2024-03-15') assert result.year == 2024 assert result.month == 3 assert result.day == 15 def test_simple_format(self): """Parse le format simple avec espace.""" result = parse_date('2024-03-15 14:30:00') assert result.hour == 14 assert result.minute == 30 def test_invalid_format(self): """Lève une exception pour un format invalide.""" with pytest.raises(ValueError): parse_date('15/03/2024') class TestFormatGpsForExif: """Tests pour la fonction format_gps_for_exif.""" def test_north_east_coordinates(self): """Formate des coordonnées Nord-Est.""" result = format_gps_for_exif(48.8584, 2.2945) assert result['GPSLatitude'] == 48.8584 assert result['GPSLatitudeRef'] == 'N' assert result['GPSLongitude'] == 2.2945 assert result['GPSLongitudeRef'] == 'E' def test_south_west_coordinates(self): """Formate des coordonnées Sud-Ouest.""" result = format_gps_for_exif(-22.9068, -43.1729) assert result['GPSLatitude'] == 22.9068 assert result['GPSLatitudeRef'] == 'S' assert result['GPSLongitude'] == 43.1729 assert result['GPSLongitudeRef'] == 'W' def test_zero_coordinates(self): """Formate des coordonnées à zéro (équateur/méridien).""" result = format_gps_for_exif(0, 0) assert result['GPSLatitude'] == 0 assert result['GPSLatitudeRef'] == 'N' assert result['GPSLongitude'] == 0 assert result['GPSLongitudeRef'] == 'E' class TestMapFrameToExif: """Tests pour la fonction map_frame_to_exif.""" def test_basic_mapping(self): """Mappe les champs de base.""" frame = {'id': 1} roll = { 'id': 'roll-1', 'frames': [frame], 'camera': {'make': 'Nikon', 'model': 'FM2'}, 'iso': 400 } result = map_frame_to_exif(frame, roll) assert result['Make'] == 'Nikon' assert result['Model'] == 'FM2' assert result['ISO'] == 400 def test_exposure_mapping(self): """Mappe les paramètres d'exposition.""" frame = { 'id': 1, 'aperture': 'f/2.8', 'shutter': '1/125', 'focalLength': 50 } roll = {'id': 1, 'frames': [frame]} result = map_frame_to_exif(frame, roll) assert result['FNumber'] == 2.8 assert result['ExposureTime'] == pytest.approx(0.008, rel=0.01) assert result['FocalLength'] == 50.0 def test_date_mapping(self): """Mappe la date correctement.""" frame = { 'id': 1, 'date': '2024-03-15T14:30:00' } roll = {'id': 1, 'frames': [frame]} result = map_frame_to_exif(frame, roll) assert result['DateTimeOriginal'] == '2024:03:15 14:30:00' assert result['CreateDate'] == '2024:03:15 14:30:00' def test_gps_mapping(self): """Mappe les coordonnées GPS.""" frame = { 'id': 1, 'location': { 'latitude': 48.8584, 'longitude': 2.2945 } } roll = {'id': 1, 'frames': [frame]} result = map_frame_to_exif(frame, roll) assert result['GPSLatitude'] == 48.8584 assert result['GPSLatitudeRef'] == 'N' assert result['GPSLongitude'] == 2.2945 assert result['GPSLongitudeRef'] == 'E' def test_note_mapping(self): """Mappe la note en description.""" frame = { 'id': 1, 'note': 'Belle photo du coucher de soleil' } roll = {'id': 1, 'frames': [frame]} result = map_frame_to_exif(frame, roll) assert result['ImageDescription'] == 'Belle photo du coucher de soleil' assert result['UserComment'] == 'Belle photo du coucher de soleil' def test_flash_mapping(self): """Mappe l'utilisation du flash.""" frame_with_flash = {'id': 1, 'flashUsed': True} frame_without_flash = {'id': 2, 'flashUsed': False} roll = {'id': 1, 'frames': []} result_with = map_frame_to_exif(frame_with_flash, roll) result_without = map_frame_to_exif(frame_without_flash, roll) assert result_with['Flash'] == 1 assert result_without['Flash'] == 0 def test_lens_mapping(self): """Mappe les informations de l'objectif.""" frame = {'id': 1} roll = { 'id': 1, 'frames': [frame], 'lens': {'make': 'Nikon', 'model': 'Nikkor 50mm f/1.4'} } result = map_frame_to_exif(frame, roll) assert result['LensMake'] == 'Nikon' assert result['LensModel'] == 'Nikkor 50mm f/1.4' def test_film_stock_mapping(self): """Mappe les informations du film.""" frame = {'id': 1} roll = { 'id': 1, 'frames': [frame], 'filmStock': {'make': 'Kodak', 'model': 'Portra 400'} } result = map_frame_to_exif(frame, roll) assert 'Kodak Portra 400' in result['ImageDescription'] def test_empty_frame(self): """Gère un frame vide.""" frame = {'id': 1} roll = {'id': 1, 'frames': [frame]} result = map_frame_to_exif(frame, roll) assert result['ImageNumber'] == 1 def test_invalid_shutter_logged(self): """Les vitesses invalides sont ignorées avec un warning.""" frame = { 'id': 1, 'shutter': 'invalid' } roll = {'id': 1, 'frames': [frame]} result = map_frame_to_exif(frame, roll) assert 'ExposureTime' not in result