111 lines
3.7 KiB
Python
111 lines
3.7 KiB
Python
"""Tests for `openrun.ingest.manual` — CSV import + idempotency."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from openrun.ingest.manual import import_csv
|
|
|
|
|
|
def _write_csv(tmp_path: Path, text: str) -> Path:
|
|
p = tmp_path / "manual.csv"
|
|
p.write_text(text)
|
|
return p
|
|
|
|
|
|
def test_import_csv_inserts_required_columns(tmp_conn, tmp_path: Path) -> None:
|
|
csv = _write_csv(tmp_path, """\
|
|
activity_date,activity_type,distance_km,duration_min,training_load,notes
|
|
2026-05-15,strength,,45,30,upper body
|
|
2026-05-16,hike,8.0,120,40,morning hike
|
|
""")
|
|
result = import_csv(tmp_conn, csv)
|
|
assert result.inserted == 2
|
|
assert result.updated == 0
|
|
assert result.skipped == 0
|
|
assert result.errors == []
|
|
|
|
rows = tmp_conn.execute(
|
|
"SELECT activity_date, activity_type, distance_km, duration_min, training_load, notes "
|
|
"FROM manual_activities ORDER BY activity_date"
|
|
).fetchall()
|
|
assert rows[0]["activity_date"] == "2026-05-15"
|
|
assert rows[0]["activity_type"] == "strength"
|
|
assert rows[0]["distance_km"] is None
|
|
assert rows[0]["duration_min"] == 45
|
|
assert rows[0]["training_load"] == 30
|
|
assert rows[0]["notes"] == "upper body"
|
|
assert rows[1]["distance_km"] == 8.0
|
|
|
|
|
|
def test_import_csv_requires_header_columns(tmp_conn, tmp_path: Path) -> None:
|
|
csv = _write_csv(tmp_path, "date,type\n2026-05-15,strength\n") # wrong column names
|
|
with pytest.raises(ValueError, match="missing required columns"):
|
|
import_csv(tmp_conn, csv)
|
|
|
|
|
|
def test_import_csv_external_id_makes_reimport_idempotent(tmp_conn, tmp_path: Path) -> None:
|
|
csv = _write_csv(tmp_path, """\
|
|
activity_date,activity_type,training_load,external_id
|
|
2026-05-15,strength,30,strava-12345
|
|
""")
|
|
import_csv(tmp_conn, csv)
|
|
second = import_csv(tmp_conn, csv)
|
|
assert second.inserted == 0
|
|
assert second.updated == 1
|
|
count = tmp_conn.execute("SELECT COUNT(*) FROM manual_activities").fetchone()[0]
|
|
assert count == 1
|
|
|
|
|
|
def test_import_csv_reimport_with_new_tl_updates_value(tmp_conn, tmp_path: Path) -> None:
|
|
"""External_id upsert should reflect changed training_load on re-import."""
|
|
csv1 = _write_csv(tmp_path, """\
|
|
activity_date,activity_type,training_load,external_id
|
|
2026-05-15,strength,30,k1
|
|
""")
|
|
import_csv(tmp_conn, csv1)
|
|
csv2 = _write_csv(tmp_path, """\
|
|
activity_date,activity_type,training_load,external_id
|
|
2026-05-15,strength,55,k1
|
|
""")
|
|
import_csv(tmp_conn, csv2)
|
|
tl = tmp_conn.execute("SELECT training_load FROM manual_activities WHERE external_id='k1'").fetchone()[0]
|
|
assert tl == 55
|
|
|
|
|
|
def test_import_csv_blank_external_id_allows_duplicates(tmp_conn, tmp_path: Path) -> None:
|
|
"""No external_id = caller is intentionally logging without dedupe key. Two imports = two rows."""
|
|
csv = _write_csv(tmp_path, """\
|
|
activity_date,activity_type,training_load
|
|
2026-05-15,strength,30
|
|
""")
|
|
import_csv(tmp_conn, csv)
|
|
import_csv(tmp_conn, csv)
|
|
assert tmp_conn.execute("SELECT COUNT(*) FROM manual_activities").fetchone()[0] == 2
|
|
|
|
|
|
def test_import_csv_skips_bad_rows_but_keeps_going(tmp_conn, tmp_path: Path) -> None:
|
|
csv = _write_csv(tmp_path, """\
|
|
activity_date,activity_type,training_load
|
|
2026-05-15,strength,30
|
|
bad-date,hike,40
|
|
2026-05-17,run,
|
|
""")
|
|
result = import_csv(tmp_conn, csv)
|
|
assert result.inserted == 2
|
|
assert result.skipped == 1
|
|
assert len(result.errors) == 1
|
|
assert "line 3" in result.errors[0]
|
|
|
|
|
|
def test_import_csv_accepts_iso_datetime_in_date_column(tmp_conn, tmp_path: Path) -> None:
|
|
csv = _write_csv(tmp_path, """\
|
|
activity_date,activity_type,training_load
|
|
2026-05-15T08:30:00,strength,30
|
|
""")
|
|
import_csv(tmp_conn, csv)
|
|
d = tmp_conn.execute("SELECT activity_date FROM manual_activities").fetchone()[0]
|
|
assert d == "2026-05-15"
|