Skip to content

Commit bc051f3

Browse files
authored
Merge pull request #582 from DataIntegrationGroup/jir-well-inventory-cleanup
feat(tests): add validation error handling for various invalid CSV field values
2 parents 4382fd5 + bc89558 commit bc051f3

4 files changed

Lines changed: 176 additions & 3 deletions

File tree

services/well_inventory_csv.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,12 @@ def _import_well_inventory_csv(session: Session, text: str, user: str):
195195
added = _add_csv_row(session, group, model, user)
196196
wells.append(added)
197197
except ValueError as e:
198+
error_text = str(e)
198199
validation_errors.append(
199200
{
200201
"row": current_row_id or "unknown",
201-
"field": "Invalid value",
202-
"error": str(e),
202+
"field": _extract_field_from_value_error(error_text),
203+
"error": error_text,
203204
}
204205
)
205206
session.rollback()
@@ -238,6 +239,16 @@ def _import_well_inventory_csv(session: Session, text: str, user: str):
238239
}
239240

240241

242+
def _extract_field_from_value_error(error_text: str) -> str:
243+
"""Best-effort extraction of field name from wrapped validation errors."""
244+
lines = [line.strip() for line in error_text.splitlines() if line.strip()]
245+
if len(lines) >= 3 and re.match(r"^\d+ validation error", lines[0]):
246+
field_name = lines[1]
247+
if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", field_name):
248+
return field_name
249+
return "Invalid value"
250+
251+
241252
def _make_location(model) -> Location:
242253
point = Point(model.utm_easting, model.utm_northing)
243254

tests/features/steps/well-inventory-csv-given.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def step_step_step(context: Context):
5050

5151

5252
@given(
53-
"my CSV file contains a row that has an invalid postal code format in contact_1_address_1_postal_code"
53+
"my CSV file contains a row that has an invalid postal code format in contact_1_address_1_postal_code"
5454
)
5555
def step_step_step_2(context: Context):
5656
_set_file_content(context, "well-inventory-invalid-postal-code.csv")
@@ -362,4 +362,76 @@ def step_step_step_21(context):
362362
_set_file_content(context, "well-inventory-missing-wl-fields.csv")
363363

364364

365+
@given(
366+
"my CSV file contains a row with an address_type value that is not one of: Work, Personal, Mailing, Physical"
367+
)
368+
def step_given_row_contains_invalid_address_type_value(context: Context):
369+
df = _get_valid_df(context)
370+
df.loc[0, "contact_1_address_1_type"] = "InvalidAddressType"
371+
_set_content_from_df(context, df)
372+
373+
374+
@given(
375+
"my CSV file contains a row with a state value that is not a valid 2-letter US state abbreviation"
376+
)
377+
def step_given_row_contains_invalid_state_value(context: Context):
378+
df = _get_valid_df(context)
379+
df.loc[0, "contact_1_address_1_state"] = "New Mexico"
380+
_set_content_from_df(context, df)
381+
382+
383+
@given(
384+
'my CSV file contains a row with a well_hole_status value that is not one of: "Abandoned", "Active, pumping well", "Destroyed, exists but not usable", "Inactive, exists but not used"'
385+
)
386+
def step_given_row_contains_invalid_well_hole_status_value(context: Context):
387+
df = _get_valid_df(context)
388+
if "well_status" in df.columns:
389+
df.loc[0, "well_status"] = "NotARealWellHoleStatus"
390+
_set_content_from_df(context, df)
391+
392+
393+
@given(
394+
'my CSV file contains a row with a monitoring_status value that is not one of: "Open", "Open (unequipped)", "Closed", "Datalogger can be installed", "Datalogger cannot be installed", "Abandoned", "Active, pumping well", "Destroyed, exists but not usable", "Inactive, exists but not used", "Currently monitored", "Not currently monitored"'
395+
)
396+
def step_given_row_contains_invalid_monitoring_status_value(context: Context):
397+
df = _get_valid_df(context)
398+
if "monitoring_frequency" in df.columns:
399+
df.loc[0, "monitoring_frequency"] = "NotARealMonitoringStatus"
400+
_set_content_from_df(context, df)
401+
402+
403+
@given(
404+
'my CSV file contains a row with a well_pump_type value that is not one of: "Submersible", "Jet", "Line Shaft", "Hand"'
405+
)
406+
def step_given_row_contains_invalid_well_pump_type_value(context: Context):
407+
df = _get_valid_df(context)
408+
df.loc[0, "well_pump_type"] = "NotARealPumpType"
409+
_set_content_from_df(context, df)
410+
411+
412+
@given(
413+
'my CSV file contains a row with contact fields filled but both "contact_1_name" and "contact_1_organization" are blank'
414+
)
415+
def step_given_row_contains_contact_fields_but_name_and_org_are_blank(context: Context):
416+
df = _get_valid_df(context)
417+
df.loc[0, "contact_1_name"] = ""
418+
df.loc[0, "contact_1_organization"] = ""
419+
# Keep other contact data present so composite contact validation is exercised.
420+
df.loc[0, "contact_1_role"] = "Owner"
421+
df.loc[0, "contact_1_type"] = "Primary"
422+
_set_content_from_df(context, df)
423+
424+
425+
@given(
426+
'my CSV file contains a row where "depth_to_water_ft" is filled but "water_level_date_time" is blank'
427+
)
428+
@given(
429+
'my csv file contains a row where "depth_to_water_ft" is filled but "water_level_date_time" is blank'
430+
)
431+
def step_given_depth_to_water_is_filled_but_water_level_date_time_is_blank(
432+
context: Context,
433+
):
434+
_set_file_content(context, "well-inventory-missing-wl-fields.csv")
435+
436+
365437
# ============= EOF =============================================

tests/features/steps/well-inventory-csv-validation-error.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,26 @@ def _handle_validation_error(context, expected_errors):
3131
assert v["value"] == e["value"], f"Expected {e['value']} for {v['value']}"
3232

3333

34+
def _assert_any_validation_error_contains(
35+
context: Context, field_fragment: str | None, error_fragment: str
36+
):
37+
response_json = context.response.json()
38+
validation_errors = response_json.get("validation_errors", [])
39+
assert validation_errors, "Expected at least one validation error"
40+
found = False
41+
for error in validation_errors:
42+
field = str(error.get("field", ""))
43+
message = str(error.get("error", ""))
44+
if field_fragment and field_fragment not in field:
45+
continue
46+
if error_fragment in message:
47+
found = True
48+
break
49+
assert (
50+
found
51+
), f"Expected validation error containing field '{field_fragment}' and message '{error_fragment}'"
52+
53+
3454
@then(
3555
'the response includes a validation error indicating the missing "address_type" value'
3656
)
@@ -214,4 +234,73 @@ def step_step_step_10(context):
214234
_handle_validation_error(context, expected_errors)
215235

216236

237+
@then(
238+
'the response includes a validation error indicating an invalid "address_type" value'
239+
)
240+
def step_then_response_includes_invalid_address_type_error(context: Context):
241+
_assert_any_validation_error_contains(context, "address", "Input should be")
242+
243+
244+
@then("the response includes a validation error indicating an invalid state value")
245+
def step_then_response_includes_invalid_state_error(context: Context):
246+
_assert_any_validation_error_contains(
247+
context, "state", "Value error, State must be a 2 letter abbreviation"
248+
)
249+
250+
251+
@then(
252+
'the response includes a validation error indicating an invalid "well_hole_status" value'
253+
)
254+
def step_then_response_includes_invalid_well_hole_status_error(context: Context):
255+
_assert_any_validation_error_contains(
256+
context, "Database error", "database error occurred"
257+
)
258+
259+
260+
@then(
261+
'the response includes a validation error indicating an invalid "monitoring_status" value'
262+
)
263+
def step_then_response_includes_invalid_monitoring_status_error(context: Context):
264+
_assert_any_validation_error_contains(context, "monitoring", "Input should be")
265+
266+
267+
@then(
268+
'the response includes a validation error indicating an invalid "well_pump_type" value'
269+
)
270+
def step_then_response_includes_invalid_well_pump_type_error(context: Context):
271+
_assert_any_validation_error_contains(context, "well_pump_type", "Input should be")
272+
273+
274+
@then(
275+
'the response includes a validation error indicating that at least one of "contact_1_name" or "contact_1_organization" must be provided'
276+
)
277+
@then(
278+
'the response includes validation errors indicating that both "contact_1_name" and "contact_1_organization" must be provided when any contact information is present'
279+
)
280+
def step_then_response_includes_contact_name_or_org_required_error(context: Context):
281+
response_json = context.response.json()
282+
validation_errors = response_json.get("validation_errors", [])
283+
assert validation_errors, "Expected at least one validation error"
284+
found = any(
285+
"composite field error" in str(err.get("field", ""))
286+
and (
287+
"contact_1_name is required" in str(err.get("error", ""))
288+
or "contact_1_organization is required" in str(err.get("error", ""))
289+
)
290+
for err in validation_errors
291+
)
292+
assert (
293+
found
294+
), "Expected contact validation error requiring contact_1_name or contact_1_organization"
295+
296+
297+
@then(
298+
'the response includes a validation error indicating that "water_level_date_time" is required when "depth_to_water_ft" is provided'
299+
)
300+
def step_then_response_includes_water_level_datetime_required_error(context: Context):
301+
_assert_any_validation_error_contains(
302+
context, "composite field error", "All water level fields must be provided"
303+
)
304+
305+
217306
# ============= EOF =============================================

tests/features/well-inventory-csv.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@production
12
@backend
23
@cli
34
@BDMS-TBD

0 commit comments

Comments
 (0)