from datetime import timedelta import pytest from birds.importer import ( import_birding_csv, parse_bool, parse_coords, parse_duration, parse_int, parse_timestamp, ) from birds.models import Bird, BirdingLocation, BirdingCSVImport from scrobbles.models import Scrobble class TestParserHelpers: def test_parse_duration(self): assert parse_duration("9 minute(s)") == 9 assert parse_duration("120 minute(s)") == 120 assert parse_duration("") is None assert parse_duration(None) is None assert parse_duration("not a duration") is None def test_parse_coords(self): loc = "Some Place, US (44.384, -68.805)" lat, lon = parse_coords(loc) assert lat == 44.384 assert lon == -68.805 def test_parse_coords_no_match(self): loc = "Some Place, US" lat, lon = parse_coords(loc) assert lat is None assert lon is None def test_parse_timestamp(self): dt = parse_timestamp("May 10, 2026", "4:15 PM") assert dt is not None assert dt.year == 2026 assert dt.month == 5 assert dt.day == 10 assert dt.hour == 16 assert dt.minute == 15 def test_parse_timestamp_no_time(self): dt = parse_timestamp("May 10, 2026", "") assert dt is not None assert dt.year == 2026 def test_parse_timestamp_invalid(self): assert parse_timestamp("not a date", "") is None def test_parse_bool(self): assert parse_bool("true") is True assert parse_bool("True") is True assert parse_bool("yes") is True assert parse_bool("1") is True assert parse_bool("false") is False assert parse_bool("") is None assert parse_bool(None) is None def test_parse_int(self): assert parse_int("42") == 42 assert parse_int("") is None assert parse_int(None) is None assert parse_int("not a number") is None class TestImportBirdingCSV: def test_import_creates_birds(self, user, birding_csv_file): import_birding_csv(birding_csv_file, user.id) assert Bird.objects.filter(common_name="Canada Goose").exists() assert Bird.objects.filter(common_name="Northern Cardinal").exists() def test_import_creates_location(self, user, birding_csv_file): import_birding_csv(birding_csv_file, user.id) assert BirdingLocation.objects.filter(title="Test Park").exists() def test_import_creates_scrobble(self, user, birding_csv_file): import_birding_csv(birding_csv_file, user.id) assert Scrobble.objects.filter( source="Birding CSV Import" ).count() == 1 def test_import_logdata_fields(self, user, birding_csv_file): import_birding_csv(birding_csv_file, user.id) scrobble = Scrobble.objects.filter(source="Birding CSV Import").first() log = scrobble.log assert log["duration_minutes"] == 9 assert log["observation_type"] == "Stationary" assert log["party_size"] == 4 assert log["complete_checklist"] is True assert len(log["birds"]) == 2 def test_import_sighting_details(self, user, birding_csv_file): import_birding_csv(birding_csv_file, user.id) scrobble = Scrobble.objects.filter(source="Birding CSV Import").first() birds = scrobble.log["birds"] cardinal = next(b for b in birds if b["quantity"] == 2) assert cardinal["sighting_notes"] == "At the feeder" def test_import_idempotent(self, user, birding_csv_file): import_birding_csv(birding_csv_file, user.id) import_birding_csv(birding_csv_file, user.id) assert Scrobble.objects.filter( source="Birding CSV Import" ).count() == 1 def test_import_bird_quantities(self, user, birding_csv_file): import_birding_csv(birding_csv_file, user.id) scrobble = Scrobble.objects.filter(source="Birding CSV Import").first() birds = scrobble.log["birds"] goose = next(b for b in birds if b["quantity"] == 6) assert goose is not None def test_import_sets_stop_timestamp(self, user, birding_csv_file): import_birding_csv(birding_csv_file, user.id) scrobble = Scrobble.objects.filter(source="Birding CSV Import").first() assert scrobble.stop_timestamp is not None expected = scrobble.timestamp + timedelta(minutes=9) assert scrobble.stop_timestamp == expected class TestBirdingCSVImportModel: def test_create_import_model(self, db, user): imp = BirdingCSVImport.objects.create(user=user) assert imp.uuid is not None assert imp.import_type == "Birding CSV" assert "Birding" in str(imp) @pytest.mark.django_db(transaction=True) def test_process_via_model(self, user, birding_csv_file): imp = BirdingCSVImport.objects.create(user=user) with open(birding_csv_file, "rb") as f: imp.csv_file.save("test.csv", f, save=True) imp.process() imp.refresh_from_db() assert imp.process_count == 1 assert imp.processed_finished is not None