diff --git a/tests/features/environment.py b/tests/features/environment.py index ac97355c1..61ee82709 100644 --- a/tests/features/environment.py +++ b/tests/features/environment.py @@ -29,6 +29,8 @@ Parameter, Deployment, TransducerObservationBlock, + StatusHistory, + ThingIdLink, ) from db.engine import session_ctx @@ -142,7 +144,9 @@ def add_sensor(context, session, sid): @add_context_object_container("groups") def add_group(context, session, wells, gid): - group = Group(name="Collabnet") + group = Group( + name="Collabnet", description="Healy Collaborative Network", project_area=None + ) for w in wells: assoc = GroupThingAssociation(group=group, thing=w) session.add(assoc) @@ -187,6 +191,54 @@ def add_block(context, session, parameter): return block +@add_context_object_container("status_histories") +def add_status_history( + context, + session, + status_type, + status_value, + start_date, + end_date, + reason, + statusable_id, + statusable_type, +): + status_history = StatusHistory( + status_type=status_type, + status_value=status_value, + start_date=start_date, + end_date=end_date, + reason=reason, + statusable_id=statusable_id, + statusable_type=statusable_type, + ) + + session.add(status_history) + session.commit() + session.refresh(status_history) + + context.objects["status_histories"].append(status_history) + return status_history + + +@add_context_object_container("id_links") +def add_id_link( + context, session, thing, relation, alternate_id, alternate_organization +): + id_link = ThingIdLink( + thing_id=thing.id, + relation=relation, + alternate_id=alternate_id, + alternate_organization=alternate_organization, + ) + session.add(id_link) + session.commit() + session.refresh(id_link) + + context.objects["id_links"].append(id_link) + return id_link + + def before_all(context): context.objects = {} @@ -209,6 +261,81 @@ def before_all(context): sensor_1 = add_sensor(context, session, well_1.id) deployment = add_deployment(context, session, well_1.id, sensor_1.id) + well_status_1 = add_status_history( + context, + session, + status_type="well_status", + status_value="Active, pumping well", + start_date=datetime(2020, 1, 1), + end_date=datetime(2021, 1, 1), + reason="Initial status", + statusable_id=well_1.id, + statusable_type="Thing", + ) + + well_status_2 = add_status_history( + context, + session, + status_type="well_status", + status_value="Destroyed, exists but not usable", + start_date=datetime(2021, 1, 1), + end_date=None, + reason="Roving bovine", + statusable_id=well_1.id, + statusable_type="Thing", + ) + + monitoring_status_1 = add_status_history( + context, + session, + status_type="monitoring_status", + status_value="currently monitored", + start_date=datetime(2020, 1, 1), + end_date=datetime(2021, 1, 1), + reason="Initial monitoring status", + statusable_id=well_1.id, + statusable_type="Thing", + ) + + monitoring_status_2 = add_status_history( + context, + session, + status_type="monitoring_status", + status_value="not monitored", + start_date=datetime(2021, 1, 1), + end_date=None, + reason="Roving bovine destroyed well", + statusable_id=well_1.id, + statusable_type="Thing", + ) + + id_link_1 = add_id_link( + context, + session, + thing=well_1, + relation="same_as", + alternate_id="12345678", + alternate_organization="USGS", + ) + + id_link_2 = add_id_link( + context, + session, + thing=well_1, + relation="same_as", + alternate_id="OSE-0001", + alternate_organization="NMOSE", + ) + + id_link_3 = add_id_link( + context, + session, + thing=well_1, + relation="same_as", + alternate_id="Roving Bovine Ranch Well #1", + alternate_organization="NMBGMR", + ) + # parameter ID can be hardcoded because init_parameter always creates the same one parameter = session.get(Parameter, 1) add_obs = add_block(context, session, parameter) diff --git a/tests/features/steps/well-core-information.py b/tests/features/steps/well-core-information.py new file mode 100644 index 000000000..5e773b73f --- /dev/null +++ b/tests/features/steps/well-core-information.py @@ -0,0 +1,326 @@ +from constants import SRID_WGS84, SRID_UTM_ZONE_13N +from services.util import transform_srid +from behave import when, then + + +# TODO: move to commonly used step definitions +@when("the user retrieves the well by ID via path parameter") +def step_impl(context): + well_id = context.objects["wells"][0].id + context.response = context.client.get(f"/thing/water-well/{well_id}") + context.water_well_data = context.response.json() + + +@then("the response should be in JSON format") +def step_impl(context): + assert context.response["Content-Type"] == "application/json" + + +@then( + "null values in the response should be represented as JSON null (not placeholder strings)" +) +def step_impl(context): + for key, value in context.water_well_data.items(): + if value is None: + assert value is None # JSON null is represented as None in Python + + +# ------------------------------------------------------------------------------ +# Well names and projects +# ------------------------------------------------------------------------------ + + +@then("the response should include the well name (point ID) (i.e. NM-1234)") +def step_impl(context): + assert "name" in context.water_well_data + + assert context.water_well_data["name"] == context.objects["wells"][0].name + + +# TODO: model schema, and test data need to be udpated +@then("the response should include the project(s) or group(s) associated with the well") +def step_impl(context): + assert "groups" in context.water_well_data + + assert ( + context.water_well_data["groups"][0]["description"] + == context.objects["groups"][0].description + ) + assert ( + context.water_well_data["groups"][0]["name"] + == context.objects["groups"][0].name + ) + assert ( + context.water_well_data["groups"][0]["project_area"] + == context.objects["groups"][0].project_area + ) + + +# ------------------------------------------------------------------------------ +# Well Purpose and Status and Monitoring Status +# ------------------------------------------------------------------------------ + + +@then("the response should include the purpose of the well (current use)") +def step_impl(context): + assert "Domestic" in context.water_well_data["well_purposes"] + assert "Irrigation" in context.water_well_data["well_purposes"] + + assert ( + context.water_well_data["well_purposes"][0] + == context.objects.wells[0].well_purposes[0].purpose + ) + assert ( + context.water_well_data["well_purposes"][1] + == context.objects.wells[0].well_purposes[1].purpose + ) + + +# TODO: this needs to be added to the ThingResponse and thing_helper via StatusHistory +@then( + "the response should include the well hole status of the well as the status of the hole in the ground (from previous Status field)" +) +def step_impl(context): + assert "well_status" in context.water_well_data + + status_history = context.objects["wells"][0].status_history + well_status = [sh for sh in status_history if sh.status_type == "well_status"] + well_status_sorted = sorted(well_status, key=lambda sh: sh.start_date, reverse=True) + + assert context.water_well_data["well_status"] == well_status_sorted[0].status_value + + +# TODO: this needs to be added to the model, schema, and test data +# TODO: the monitoring frequency field needs to be added to lexicon +# the monitoring status field from NM_Aquifer contains a multitude of information, like having three codes (6AC), so the transfer and model/schemas will need to take this into account +# could create descriptor table like WellPurpose and CasingMaterial +@then("the response should include the monitoring frequency (new field)") +def step_impl(context): + for group in context.water_well_data["groups"]: + assert "monitoring_frequency" in group + + assert context.water_well_data["monitoring_frequency"] == "Monthly" + + +# TODO: this needs to be added to the model, schema, and test data +# the monitoring status field from NM_Aquifer contains a multitude of information, like having three codes (6AC), so the transfer and model/schemas will need to take this into account +# could create descriptor table like WellPurpose and CasingMaterial +@then( + "the response should include whether the well is currently being monitored with status text if applicable (from previous MonitoringStatus field)" +) +def step_impl(context): + assert "monitoring_status" in context.water_well_data + + status_history = context.objects["wells"][0].status_history + monitoring_status = [ + sh for sh in status_history if sh.status_type == "monitoring_status" + ] + monitoring_status_sorted = sorted( + monitoring_status, key=lambda sh: sh.start_date, reverse=True + ) + + assert ( + context.water_well_data["monitoring_status"] + == monitoring_status_sorted[0].status_value + ) + + +# ------------------------------------------------------------------------------ +# Data Lifecycle and Public Visibility +# ------------------------------------------------------------------------------ + + +@then("the response should include the release status of the well record") +def step_impl(context): + assert "release_status" in context.water_well_data + + assert ( + context.water_well_data["release_status"] + == context.objects["wells"][0].release_status + ) + + +# ------------------------------------------------------------------------------ +# Well Physical Properties +# ------------------------------------------------------------------------------ + + +@then("the response should include the hole depth in feet") +def step_impl(context): + assert "hole_depth" in context.water_well_data + assert "hole_depth_unit" in context.water_well_data + + assert ( + context.water_well_data["hole_depth"] == context.objects["wells"][0].hole_depth + ) + assert context.water_well_data["hole_depth_unit"] == "ft" + + +@then("the response should include the well depth in feet") +def step_impl(context): + assert "well_depth" in context.water_well_data + assert "well_depth_unit" in context.water_well_data + + assert ( + context.water_well_data["well_depth"] == context.objects["wells"][0].well_depth + ) + assert context.water_well_data["well_depth_unit"] == "ft" + + +# TODO: this needs to be added to the model, schema, and test data +@then("the response should include the source of the well depth information") +def step_impl(context): + assert "well_depth_source" in context.water_well_data + + assert ( + context.water_well_data["well_depth_source"] + == context.objects["wells"][0].well_depth_source + ) + + +# ------------------------------------------------------------------------------ +# Measuring Point Information +# ------------------------------------------------------------------------------ + + +# TODO: this needs to be added to the model, schema, and test data +@then("the response should include the description of the measuring point") +def step_impl(context): + assert "measuring_point_description" in context.water_well_data + + assert ( + context.water_well_data["measuring_point_description"] + == context.objects["wells"][0].measuring_point_description + ) + + +# TODO: this needs to be added to the model, schema, and test data +@then("the response should include the measuring point height in feet") +def step_impl(context): + assert "measuring_point_height" in context.water_well_data + assert "measuring_point_height_unit" in context.water_well_data + + assert ( + context.water_well_data["measuring_point_height"] + == context.objects["wells"][0].measuring_point_height + ) + assert context.water_well_data["measuring_point_height_unit"] == "ft" + + +# ------------------------------------------------------------------------------ +# Location Information +# ------------------------------------------------------------------------------ + + +@then("the response should include location information in GeoJSON format") +def step_impl(context): + assert "current_location" in context.water_well_data + assert "type" in context.water_well_data["current_location"] + assert "geometry" in context.water_well_data["current_location"] + assert "properties" in context.water_well_data["current_location"] + + assert context.water_well_data["current_location"]["type"] == "Feature" + + +# TODO: the LocationResponse schema needs to be updated +@then( + 'the response should include a geometry object with type "Point" and coordinates array [longitude, latitude, elevation] in decimal degrees with datum WGS84' +) +def step_impl(context): + latitude = context.objects["locations"][0].point.y + longitude = context.objects["locations"][0].point.x + elevation_m = context.objects["locations"][0].elevation + + assert context.water_well_data["current_location"]["geometry"] == { + "type": "Point", + "coordinates": [longitude, latitude, elevation_m], + } + + +# TODO: elevation should be returned in ft, not meters, conversion should occur in schema +# TODO: add elevation_unit: str = "ft" to LocationResponse schema +@then( + "the response should include the elevation in feet with vertical datum NAVD88 in the properties" +) +def step_impl(context): + assert "elevation" in context.water_well_data["current_location"]["properties"] + assert "elevation_unit" in context.water_well_data["current_location"]["properties"] + assert "vertical_datum" in context.water_well_data["current_location"]["properties"] + + elevation_ft = context.objects["locations"][0].elevation * 3.28084 + + assert ( + context.water_well_data["current_location"]["properties"]["elevation"] + == elevation_ft + ) + assert ( + context.water_well_data["current_location"]["properties"]["elevation_unit"] + == "ft" + ) + assert ( + context.water_well_data["current_location"]["properties"]["vertical_datum"] + == "NAVD88" + ) + + +@then( + "the response should include the elevation method (i.e. interpolated from digital elevation model) in the properties" +) +def step_impl(context): + assert ( + "elevation_method" in context.water_well_data["current_location"]["properties"] + ) + assert ( + context.water_well_data["current_location"]["properties"]["elevation_method"] + == context.objects["locations"][0].elevation_method + ) + + +# TODO: this needs to be added to the LocationResponse schema +@then( + "the response should include the UTM coordinates with datum NAD83 in the properties" +) +def step_impl(context): + + assert ( + "utm_coordinates" in context.water_well_data["current_location"]["properties"] + ) + + point_utm_zone_13 = transform_srid( + context.objects["locations"][0].point, SRID_WGS84, SRID_UTM_ZONE_13N + ) + + assert context.water_well_data["current_location"]["properties"][ + "utm_coordinates" + ] == { + "easting": point_utm_zone_13.x, + "northing": point_utm_zone_13.y, + "utm_zone": 13, + "horizontal_datum": "NAD83", + } + + +# ------------------------------------------------------------------------------ +# Alternate Identifiers +# ------------------------------------------------------------------------------ + + +# TODO: This needs to be added to the test data +# TODO: id link schema needs to use lexicon enums for relation and alternate_organization +@then( + "the response should include any alternate IDs for the well like the NMBGMR site_name (i.e. John Smith Well), USGS site number, or the OSE well ID and OSE well tag ID" +) +def step_impl(context): + assert "alternate_ids" in context.water_well_data + + assert len(context.water_well_data["alternate_ids"]) == 3 + for item in context.water_well_data["alternate_ids"]: + if item["alternate_organization"] == "USGS": + assert item["relation"] == context.objects["id_links"][0].relation + assert item["alternate_id"] == context.objects["id_links"][0].alternate_id + elif item["alternate_organization"] == "NMOSE": + assert item["relation"] == context.objects["id_links"][1].relation + assert item["alternate_id"] == context.objects["id_links"][1].alternate_id + elif item["alternate_organization"] == "NMBGMR": + assert item["relation"] == context.objects["id_links"][2].relation + assert item["alternate_id"] == context.objects["id_links"][2].alternate_id diff --git a/tests/features/steps/well-location.py b/tests/features/steps/well-location.py index 54f228e43..665fcdf3c 100644 --- a/tests/features/steps/well-location.py +++ b/tests/features/steps/well-location.py @@ -17,6 +17,7 @@ from behave.runner import Context +# TODO: should this use fixtures to populate and access data from the database? @given("the system has valid well and location data in the database") def step_impl(context): context.database = { diff --git a/tests/features/steps/well-sensor-deployment.py b/tests/features/steps/well-sensor-deployment.py index fef467888..b7d023fdc 100644 --- a/tests/features/steps/well-sensor-deployment.py +++ b/tests/features/steps/well-sensor-deployment.py @@ -25,6 +25,7 @@ # ----------------------------------------------------------------------------- +# TODO: should this use fixtures to populate and access data from the database? @given("the system has valid well and deployment data in the database") def step_impl_valid_data(context: Context): """ @@ -48,6 +49,7 @@ def step_impl_valid_data(context: Context): context.api_connected = True +# TODO: this step could be moved to a common steps file if reused elsewhere @given("the user is authenticated as a field technician") def step_impl_authenticated_user(context: Context): """Simulates user authentication."""