Skip to content

Commit 576259f

Browse files
committed
✅ Add tests for Ripple-to-REDCap
1 parent 00019c6 commit 576259f

7 files changed

Lines changed: 749 additions & 2 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,6 @@ logs/
8484

8585
# Secrets
8686
_config_variables/
87+
88+
# Tests
89+
coverage.xml

python_jobs/conftest.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""Shared pytest configuration and fixtures."""
2+
3+
from pathlib import Path
4+
import tempfile
5+
from unittest.mock import Mock
6+
7+
import pandas as pd
8+
import pytest
9+
10+
11+
@pytest.fixture
12+
def temp_csv_file():
13+
"""Create a temporary CSV file."""
14+
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as tmp:
15+
tmp_path = tmp.name
16+
yield Path(tmp_path)
17+
Path(tmp_path).unlink(missing_ok=True)
18+
19+
20+
@pytest.fixture
21+
def temp_excel_file():
22+
"""Create a temporary Excel file."""
23+
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
24+
tmp_path = tmp.name
25+
yield Path(tmp_path)
26+
Path(tmp_path).unlink(missing_ok=True)
27+
28+
29+
@pytest.fixture
30+
def temp_dir():
31+
"""Create a temporary directory."""
32+
with tempfile.TemporaryDirectory() as tmpdir:
33+
yield Path(tmpdir)
34+
35+
36+
@pytest.fixture
37+
def mock_redcap_response():
38+
"""Mock successful REDCap API response."""
39+
response = Mock()
40+
response.status_code = 200
41+
response.text = "1"
42+
return response
43+
44+
45+
@pytest.fixture
46+
def mock_ripple_response():
47+
"""Mock successful Ripple API response."""
48+
response = Mock()
49+
response.status_code = 200
50+
response.text = "Success"
51+
return response
52+
53+
54+
@pytest.fixture
55+
def sample_ripple_data():
56+
"""Sample Ripple data for testing."""
57+
return pd.DataFrame(
58+
{
59+
"globalId": ["ST001", "AA001"],
60+
"firstName": ["Alec", "Abby"],
61+
"lastName": ["Holland", "Arcane"],
62+
"cv.consent_form": ["Send to RedCap", "Send to RedCap"],
63+
"customId": [12345, 67890],
64+
"contact.1.infos.1.contactType": ["email", "email"],
65+
"contact.1.infos.1.information": ["alec@swamp.com", "abby@parliament.org"],
66+
"importType": ["HBN - Main", "HBN - Waitlist"],
67+
}
68+
)
69+
70+
71+
@pytest.fixture
72+
def swamp_thing_participant():
73+
"""Fixture providing Alec Holland's data."""
74+
return pd.DataFrame(
75+
{
76+
"globalId": ["ST001"],
77+
"customId": [12345],
78+
"firstName": ["Alec"],
79+
"lastName": ["Holland"],
80+
"cv.consent_form": ["Send to RedCap"],
81+
"contact.1.infos.1.contactType": ["email"],
82+
"contact.1.infos.1.information": ["alec.holland@swampthing.com"],
83+
"importType": ["HBN - Main"],
84+
}
85+
)
86+
87+
88+
@pytest.fixture
89+
def parliament_of_trees_participants():
90+
"""Fixture providing multiple Parliament of Trees members."""
91+
return pd.DataFrame(
92+
{
93+
"globalId": ["ST001", "AA001", "TE001"],
94+
"customId": [12345, 67890, 11111],
95+
"firstName": ["Alec", "Abby", "Tefé"],
96+
"lastName": ["Holland", "Arcane", "Holland"],
97+
"cv.consent_form": ["Send to RedCap", "Send to RedCap", "Send to RedCap"],
98+
"contact.1.infos.1.contactType": ["email", "email", "email"],
99+
"contact.1.infos.1.information": [
100+
"alec@swamp.com",
101+
"abby@parliament.org",
102+
"tefe@green.org",
103+
],
104+
"importType": ["HBN - Main", "HBN - Main", "HBN - Waitlist"],
105+
}
106+
)
107+
108+
109+
@pytest.fixture
110+
def anton_arcane_corrupted_data():
111+
"""Fixture providing Anton Arcane's corrupted participant data."""
112+
return pd.DataFrame(
113+
{
114+
"globalId": ["ANT001"],
115+
"customId": [66666],
116+
"firstName": ["Anton"],
117+
"lastName": ["Arcane"],
118+
"cv.consent_form": ["Do Not Send"],
119+
"contact.1.infos.1.contactType": ["phone"],
120+
"contact.1.infos.1.information": ["666-666-6666"],
121+
"importType": ["HBN - Rejected"],
122+
}
123+
)
124+
125+
126+
@pytest.fixture
127+
def mock_ripple_variables():
128+
"""Mock ripple_variables configuration."""
129+
mock_vars = Mock()
130+
mock_vars.study_ids = {
131+
"HBN - Main": "main_study_id",
132+
"HBN - Waitlist": "waitlist_study_id",
133+
}
134+
mock_vars.column_dict.return_value = {}
135+
mock_vars.headers = {"import": {"Content-Type": "application/octet-stream"}}
136+
return mock_vars
137+
138+
139+
@pytest.fixture
140+
def mock_redcap_variables():
141+
"""Mock redcap_variables configuration."""
142+
mock_vars = Mock()
143+
mock_vars.Tokens.pid757 = "dev_token"
144+
mock_vars.Tokens.pid247 = "prod_token"
145+
return mock_vars
146+
147+
148+
@pytest.fixture
149+
def mock_endpoints():
150+
"""Mock Endpoints configuration."""
151+
mock = Mock()
152+
mock.Ripple.import_data.return_value = "https://ripple.swamp.org/import"
153+
mock.REDCap.base_url = "https://redcap.swamp.org/api/"
154+
return mock
155+
156+
157+
@pytest.fixture
158+
def setup_ripple_mocks(mock_ripple_variables):
159+
"""Set up common ripple variable mocks."""
160+
return mock_ripple_variables
161+
162+
163+
@pytest.fixture
164+
def setup_redcap_mocks(mock_redcap_variables, temp_csv_file):
165+
"""Set up common redcap variable mocks with temp file."""
166+
mock_redcap_variables.redcap_import_file = temp_csv_file
167+
return mock_redcap_variables
168+
169+
170+
@pytest.fixture
171+
def excel_file_with_data(temp_excel_file):
172+
"""Create Excel file with test data."""
173+
test_df = pd.DataFrame(
174+
{
175+
"globalId": ["ST001"],
176+
"cv.consent_form": ["consent_form_created_in_redcap"],
177+
}
178+
)
179+
test_df.to_excel(temp_excel_file, index=False)
180+
return temp_excel_file

python_jobs/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ dependencies = [
2020
"types-pytz>=2025.2.0.20251108",
2121
"pyspark>=4.1.1",
2222
"ipython>=9.10.0",
23+
"pytest>=9.0.2",
24+
"openpyxl>=3.1.5",
25+
"pytest-cov>=7.0.0",
2326
]
2427

2528
[project.optional-dependencies]
@@ -53,6 +56,9 @@ ignore = ["D203", "D212"]
5356
force-sort-within-sections = true
5457
order-by-type = false
5558

59+
[tool.ruff.lint.per-file-ignores]
60+
"test_ripple.py" = ["PLR0913", "PLR2004"]
61+
5662
[tool.mypy]
5763
python_version = "3.12"
5864
strict = true

python_jobs/src/hbnmigration/from_ripple/to_redcap.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,10 @@ def cleanup() -> None:
269269
redcap_variables.redcap_import_file,
270270
ripple_variables.ripple_import_file,
271271
]:
272-
filepath.unlink()
272+
try:
273+
filepath.unlink(missing_ok=True)
274+
except FileNotFoundError:
275+
logger.warning("%s already does not exist.", filepath)
273276

274277

275278
def main(project_status: Literal["dev", "prod"] = "dev") -> None:

python_jobs/src/hbnmigration/utility_functions/custom.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def _fetch_api_data(
134134
str(tmp_file_path), header=True, inferSchema=True
135135
)
136136
finally:
137-
tmp_file_path.unlink()
137+
tmp_file_path.unlink(missing_ok=True)
138138
case 0 | 1 | _:
139139
return pd.read_csv(csv_data, low_memory=False)
140140
logger.info("Empty response received from the API.")

0 commit comments

Comments
 (0)