diff --git a/api/thing.py b/api/thing.py index c36d572a1..58dcfaa66 100644 --- a/api/thing.py +++ b/api/thing.py @@ -13,13 +13,16 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== -from typing import List - -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, Query, Request from fastapi_pagination.ext.sqlalchemy import paginate from sqlalchemy import select -from sqlalchemy.orm import Session -from starlette import status +from sqlalchemy.exc import ProgrammingError +from starlette.status import ( + HTTP_200_OK, + HTTP_201_CREATED, + HTTP_204_NO_CONTENT, + HTTP_409_CONFLICT, +) from api.pagination import CustomPage from core.dependencies import ( @@ -35,11 +38,8 @@ # no_permission_function, amp_editor_dependency, ) -from db.engine import get_db_session -from db.location import LocationThingAssociation, Location from db.thing import Thing, WellScreen from db.thing import ThingIdLink -from schemas.location import LocationResponse, UpdateLocation from schemas.thing import ( CreateThingIdLink, CreateWell, @@ -47,105 +47,147 @@ ThingResponse, WellResponse, WellScreenResponse, - UpdateThing, + UpdateSpring, UpdateWell, SpringResponse, CreateSpring, - CreateThing, ThingIdLinkResponse, UpdateThingIdLink, UpdateWellScreen, ) -from services.crud_helper import model_patcher, model_adder +from services.crud_helper import model_patcher, model_adder, model_deleter +from services.exceptions_helper import PydanticStyleException from services.query_helper import ( simple_get_by_id, paginated_all_getter, order_sort_filter, ) -from services.thing_helper import add_thing, get_db_things -from services.validation.well import validate_screens +from services.thing_helper import ( + add_thing, + patch_thing, + add_well_screen, + get_db_things, + get_thing_of_a_thing_type_by_id, +) +from services.lexicon_helper import get_terms_by_category router = APIRouter( prefix="/thing", tags=["thing"], dependencies=[Depends(viewer_function)] ) -@router.get("") -def get_things( - session: session_dependency, - thing_id: int = None, - thing_type: List[str] | str = Query(default=[]), - within: str = None, - query: str = None, - sort: str = None, - order: str = None, - filter_: str = Query( - default=None, - alias="filter", - ), -) -> CustomPage[ThingResponse]: - """ - Retrieve all things or filter by type. - """ - if thing_id: - sql = select(Thing).where(Thing.id == thing_id) - return paginate(query=sql, conn=session) - else: - return get_db_things( - filter_, - order, - query, - session, - sort, - thing_type, - with_location=True, - within=within, - ) - - -@router.get( - "/well", summary="Get all wells", dependencies=[Depends(amp_viewer_function)] -) -async def get_wells( +def database_error_handler( + payload: CreateWell | CreateSpring, error: ProgrammingError +) -> None: + """ + Handle errors raised by the database when adding or updating a thing. + """ + + error_message = error.orig.args[0]["M"] + + if ( + error_message + == 'insert or update on table "group_thing_association" violates foreign key constraint "group_thing_association_group_id_fkey"' + ): + + detail = { + "loc": ["body", "group_id"], + "msg": f"Group with ID {payload.group_id} not found.", + "type": "value_error", + "input": {"group_id": payload.group_id}, + } + elif ( + error_message + == 'insert or update on table "location_thing_association" violates foreign key constraint "location_thing_association_location_id_fkey"' + ): + + detail = { + "loc": ["body", "location_id"], + "msg": f"Location with ID {payload.location_id} not found.", + "type": "value_error", + "input": {"location_id": payload.location_id}, + } + elif ( + error_message + == 'insert or update on table "well_screen" violates foreign key constraint "well_screen_thing_id_fkey"' + ): + detail = { + "loc": ["body", "thing_id"], + "msg": f"Thing with ID {payload.thing_id} not found.", + "type": "value_error", + "input": {"thing_id": payload.thing_id}, + } + elif ( + error_message + == 'insert or update on table "well_screen" violates foreign key constraint "well_screen_screen_type_fkey"' + ): + valid_screen_types = get_terms_by_category("casing_material") + valid_screen_types_for_msg = " | ".join(valid_screen_types) + detail = { + "loc": ["body", "screen_type"], + "msg": f"{payload.screen_type} is an invalid screen type. Valid types are: {valid_screen_types_for_msg}.", + "type": "value_error", + "input": {"screen_type": payload.screen_type}, + } + elif ( + error_message + == 'insert or update on table "thing_id_link" violates foreign key constraint "thing_id_link_thing_id_fkey"' + ): + detail = { + "loc": ["body", "thing_id"], + "msg": f"Thing with ID {payload.thing_id} not found.", + "type": "value_error", + "input": {"thing_id": payload.thing_id}, + } + + raise PydanticStyleException(status_code=HTTP_409_CONFLICT, detail=[detail]) + + +# GET ========================================================================== + + +@router.get("/water-well", summary="Get all water wells", status_code=HTTP_200_OK) +async def get_water_wells( session: session_dependency, - # api_id: str = None, - # ose_pod_id: str = None, + request: Request, sort: str = None, order: str = None, filter_: str = Query(alias="filter", default=None), - thing_type: List[str] | str = Query(default="water well"), query: str = None, ) -> CustomPage[WellResponse]: """ Retrieve all wells from the database. """ + thing_type = request.url.path.split("/")[2].replace("-", " ") + return get_db_things(filter_, order, query, session, sort, thing_type=thing_type) - # if api_id: - # sql = select(WellThing).where(WellThing.api_id == api_id) - # elif ose_pod_id: - # sql = select(WellThing).where(WellThing.ose_pod_id == ose_pod_id) - return get_db_things(filter_, order, query, session, sort, thing_type) - # If no parameters, return all wells - # return simple_all_getter(session, Well) - # result = session.execute(sql) - # return result.scalars().all() +@router.get( + "/water-well/{thing_id}", summary="Get water well by ID", status_code=HTTP_200_OK +) +async def get_well_by_id( + thing_id: int, session: session_dependency, request: Request +) -> WellResponse: + """ + Retrieve a water well by ID from the database. + """ + return get_thing_of_a_thing_type_by_id(session, request, thing_id) @router.get( - "/spring", summary="Get all springs", dependencies=[Depends(amp_viewer_function)] + "/water-well/{thing_id}/well-screen", + summary="Get well screens by water well ID", + status_code=HTTP_200_OK, ) -async def get_springs( - session: session_dependency, - sort: str = None, - order: str = None, - filter_: str = Query(alias="filter", default=None), - thing_type: List[str] | str = Query(default="water well"), -) -> CustomPage[SpringResponse]: +async def get_well_screens_by_well_id( + thing_id: int, session: session_dependency, request: Request +) -> CustomPage[WellScreenResponse]: """ - Retrieve all springs from the database. + Retrieve all well screens for a specific water well by its ID. """ - return get_db_things(filter_, order, None, session, sort, thing_type) + thing = get_thing_of_a_thing_type_by_id(session, request, thing_id) + sql = select(WellScreen).where(WellScreen.thing_id == thing.id) + return paginate(query=sql, conn=session) @router.get( @@ -180,32 +222,33 @@ async def get_well_screen_by_id( Retrieve a well screen by ID from the database. """ well_screen = simple_get_by_id(session, WellScreen, wellscreen_id) - if not well_screen: - return {"message": "Well screen not found"} return well_screen -@router.get("/{thing_id}/id-link", summary="Get thing links by thing ID") -def get_thing_id_links( - thing_id: int, +@router.get("/spring", summary="Get all springs") +async def get_springs( session: session_dependency, -) -> CustomPage[ThingIdLinkResponse]: + request: Request, + sort: str = None, + order: str = None, + filter_: str = Query(alias="filter", default=None), + query: str = None, +) -> CustomPage[SpringResponse]: """ - Retrieve all links for a specific thing by its ID. + Retrieve all springs from the database. """ - sql = select(ThingIdLink).where(ThingIdLink.thing_id == thing_id) - return paginate(query=sql, conn=session) + thing_type = request.url.path.split("/")[2].replace("-", " ") + return get_db_things(filter_, order, query, session, sort, thing_type=thing_type) -@router.get("/id-link/{link_id}", summary="Get thing links by link ID") -def get_thing_id_links( - link_id: int, - session: session_dependency, -) -> ThingIdLinkResponse: +@router.get("/spring/{thing_id}", summary="Get spring by ID", status_code=HTTP_200_OK) +async def get_spring_by_id( + thing_id: int, session: session_dependency, request: Request +) -> SpringResponse: """ - Retrieve all links for a specific thing by its ID. + Retrieve a spring by ID from the database. """ - return simple_get_by_id(session, ThingIdLink, link_id) + return get_thing_of_a_thing_type_by_id(session, request, thing_id) @router.get( @@ -227,11 +270,75 @@ def get_thing_id_links( return paginate(query=sql, conn=session) -# ===== POST ============= +@router.get("/id-link/{link_id}", summary="Get thing links by link ID") +def get_thing_id_links( + link_id: int, + session: session_dependency, +) -> ThingIdLinkResponse: + """ + Retrieve all links for a specific thing by its ID. + """ + return simple_get_by_id(session, ThingIdLink, link_id) + + +@router.get("", summary="Get all things", status_code=HTTP_200_OK) +def get_things( + session: session_dependency, + thing_id: int = None, + within: str = None, + query: str = None, + sort: str = None, + order: str = None, + filter_: str = Query( + default=None, + alias="filter", + ), +) -> CustomPage[ThingResponse]: + """ + Retrieve all things or filter by type. + """ + if thing_id: + sql = select(Thing).where(Thing.id == thing_id) + return paginate(query=sql, conn=session) + else: + return get_db_things( + filter_, + order, + query, + session, + sort, + within=within, + ) + + +@router.get("/{thing_id}", summary="Get thing by ID", status_code=HTTP_200_OK) +async def get_thing_by_id( + thing_id: int, session: session_dependency, request: Request +) -> ThingResponse: + """ + Retrieve a thing by ID from the database. + """ + return simple_get_by_id(session, Thing, thing_id) + + +@router.get("/{thing_id}/id-link", summary="Get thing links by thing ID") +def get_thing_id_links( + thing_id: int, + session: session_dependency, +) -> CustomPage[ThingIdLinkResponse]: + """ + Retrieve all links for a specific thing by its ID. + """ + thing = simple_get_by_id(session, Thing, thing_id) + sql = select(ThingIdLink).where(ThingIdLink.thing_id == thing.id) + return paginate(query=sql, conn=session) + + +# POST ======================================================================== @router.post( - "/id-link", status_code=status.HTTP_201_CREATED, summary="Create a new thing link" + "/id-link", status_code=HTTP_201_CREATED, summary="Create a new thing link" ) def create_thing_id_link( link_data: CreateThingIdLink, @@ -241,138 +348,116 @@ def create_thing_id_link( """ Create a new link between a thing and an alternate ID. """ - return model_adder(session, ThingIdLink, link_data, user=user) + try: + return model_adder(session, ThingIdLink, link_data, user=user) + except ProgrammingError as e: + database_error_handler(link_data, e) @router.post( - "/well", - summary="Create a well", - status_code=status.HTTP_201_CREATED, + "/water-well", + summary="Create a water well", + status_code=HTTP_201_CREATED, ) def create_well( thing_data: CreateWell, session: session_dependency, + request: Request, user: amp_admin_dependency, ) -> WellResponse: """ - Create a new well in the database. + Create a new water well in the database. """ - # print("Creating well with data:", well_data, user) - - return add_thing(session, thing_data, thing_type="water well", user=user) + try: + return add_thing(session=session, data=thing_data, request=request, user=user) + except ProgrammingError as e: + database_error_handler(thing_data, e) @router.post( "/spring", summary="Create a new spring", - status_code=status.HTTP_201_CREATED, + status_code=HTTP_201_CREATED, ) def create_spring( thing_data: CreateSpring, session: session_dependency, + request: Request, user: amp_admin_dependency, ) -> SpringResponse: """ Create a new well in the database. """ - return add_thing(session, thing_data, thing_type="spring", user=user) - - -@router.post( - "", - summary="Create a new thing", - status_code=status.HTTP_201_CREATED, -) -def create_thing( - thing_data: CreateThing, - session: session_dependency, - user: admin_dependency, -) -> ThingResponse: - """ - Create a new well in the database. - """ - return add_thing(session, thing_data, user=user) + try: + return add_thing(session=session, data=thing_data, request=request, user=user) + except ProgrammingError as e: + database_error_handler(thing_data, e) @router.post( "/well-screen", summary="Create a new well screen", - status_code=status.HTTP_201_CREATED, + status_code=HTTP_201_CREATED, ) def create_wellscreen( session: session_dependency, user: amp_admin_dependency, - well_screen_data: CreateWellScreen = Depends(validate_screens), + well_screen_data: CreateWellScreen, ) -> WellScreenResponse: """ Create a new well screen in the database. """ - return model_adder(session, WellScreen, well_screen_data, user=user) - + try: + return add_well_screen(session, well_screen_data, user=user) + except ProgrammingError as e: + database_error_handler(well_screen_data, e) + except PydanticStyleException as e: + raise e -@router.patch("/{thing_id}", summary="Update thing") -def update_thing( - thing_id: int, - thing_data: UpdateWell | UpdateThing, - user: editor_dependency, - session: Session = Depends(get_db_session), -) -> ThingResponse: - """ - Update an existing thing by ID. - """ - return model_patcher(session, Thing, thing_id, thing_data, user=user) +# PATCH ======================================================================== -@router.patch("/{thing_id}/location", summary="Update thing location") -def update_thing_location( - thing_id: int, - location_data: UpdateLocation, - session: session_dependency, - user: editor_dependency, -) -> LocationResponse: - """ - Update the location of an existing thing by ID. - """ - - # get active location associated with the thing - location_id = session.execute( - select(LocationThingAssociation.location_id) - .where(LocationThingAssociation.thing_id == thing_id) - .order_by(LocationThingAssociation.effective_start.desc()) - ).scalar_one_or_none() - - return model_patcher(session, Location, location_id, location_data, user=user) - - -@router.patch("/{thing_id}", summary="Update thing") -def update_thing( +@router.patch( + "/water-well/{thing_id}", + summary="Update well by parent thing ID", + status_code=HTTP_200_OK, +) +async def update_water_well( thing_id: int, - thing_data: UpdateThing, + thing_data: UpdateWell, session: session_dependency, - user: editor_dependency, -) -> ThingResponse: + user: amp_editor_dependency, + request: Request, +) -> WellResponse: """ Update an existing well by ID. """ - return model_patcher(session, Thing, thing_id, thing_data, user=user) + return patch_thing(session, request, thing_id, thing_data, user=user) -@router.patch("/well/{thing_id}", summary="Update well by parent thing ID") -def update_thing( +@router.patch( + "/spring/{thing_id}", + summary="Update spring by parent thing ID", + status_code=HTTP_200_OK, +) +async def update_spring( thing_id: int, - thing_data: UpdateWell, + thing_data: UpdateSpring, session: session_dependency, user: amp_editor_dependency, -) -> WellResponse: + request: Request, +) -> SpringResponse: """ - Update an existing well by ID. + Update an existing spring by ID. """ - return model_patcher(session, Thing, thing_id, thing_data, user=user) + return patch_thing(session, request, thing_id, thing_data, user=user) -@router.patch("/id-link/{link_id}", summary="Update thing link by ID") -def update_thing_id_link( +@router.patch( + "/id-link/{link_id}", summary="Update thing link by ID", status_code=HTTP_200_OK +) +async def update_thing_id_link( link_id: int, link_data: UpdateThingIdLink, session: session_dependency, @@ -381,8 +466,12 @@ def update_thing_id_link( return model_patcher(session, ThingIdLink, link_id, link_data, user=user) -@router.patch("/well-screen/{well_screen_id}", summary="Update Well Screen by ID") -def update_well_screen( +@router.patch( + "/well-screen/{well_screen_id}", + summary="Update Well Screen by ID", + status_code=HTTP_200_OK, +) +async def update_well_screen( well_screen_id: int, well_screen_data: UpdateWellScreen, session: session_dependency, @@ -395,4 +484,53 @@ def update_well_screen( ) +# DELETE ======================================================================= + + +@router.delete( + "/{thing_id}", summary="Delete thing by ID", status_code=HTTP_204_NO_CONTENT +) +async def delete_thing( + thing_id: int, + session: session_dependency, + user: editor_dependency, +) -> None: + """ + Delete a thing by ID. + """ + return model_deleter(session, Thing, thing_id) + + +@router.delete( + "/well-screen/{well_screen_id}", + summary="Delete well screen by ID", + status_code=HTTP_204_NO_CONTENT, +) +async def delete_well_screen( + well_screen_id: int, + session: session_dependency, + user: editor_dependency, +) -> None: + """ + Delete a well screen by ID. + """ + return model_deleter(session, WellScreen, well_screen_id) + + +@router.delete( + "/id-link/{link_id}", + summary="Delete thing link by ID", + status_code=HTTP_204_NO_CONTENT, +) +async def delete_thing_id_link( + link_id: int, + session: session_dependency, + user: editor_dependency, +) -> None: + """ + Delete a thing link by ID. + """ + return model_deleter(session, ThingIdLink, link_id) + + # ============= EOF ============================================= diff --git a/schemas/location.py b/schemas/location.py index 34a082a0c..ed23fdf63 100644 --- a/schemas/location.py +++ b/schemas/location.py @@ -16,7 +16,6 @@ from geoalchemy2 import WKBElement from geoalchemy2.shape import to_shape from pydantic import BaseModel, field_validator -from shapely import wkt from schemas import ORMBaseModel from services.validation.geospatial import validate_wkt_geometry @@ -94,6 +93,7 @@ class UpdateLocation(BaseModel): Schema for updating a location. """ + name: str | None = None notes: str | None = None point: str | None = None release_status: str | None = None diff --git a/schemas/thing.py b/schemas/thing.py index a7504fab6..d581980c5 100644 --- a/schemas/thing.py +++ b/schemas/thing.py @@ -34,11 +34,19 @@ class CreateThingIdLink(BaseModel): class CreateBaseThing(BaseModel): + """ + Developer's notes + + thing_type does not need to be set by the user, this is determined by the + POST endpoint + + e.g. POST /thing/water-well, POST /thing/spring determines the thing_type + """ + location_id: int | None = None # Optional location ID for the thing + group_id: int | None = None # Optional group ID for the thing name: str # Name of the thing - group: str | None = None # Optional group ID for the thing - thing_type: str | None = None # Type of the thing (e.g., "Well", "Spring", etc.) - release_status: str | None = "draft" # Release status of the thing + release_status: str # Release status of the thing class CreateWell(CreateBaseThing): @@ -46,8 +54,6 @@ class CreateWell(CreateBaseThing): Schema for creating a well. """ - # api_id: str | None = None - # ose_pod_id: str | None = None well_type: str | None = None well_depth: float | None = None # in feet hole_depth: float | None = None # in feet @@ -79,19 +85,6 @@ class CreateWellScreen(BaseModel): screen_type: str | None = None screen_description: str | None = None - @model_validator(mode="after") - def validate_screen_type(self): - if self.screen_type is not None: - valid_screen_types = [ - "PVC", - ] # todo: get valid screen types from database - if self.screen_type not in valid_screen_types: - raise ValueError( - f"Invalid screen_type: {self.screen_type}. " - f"Valid options are: {', '.join(valid_screen_types)}." - ) - return self - # validate that screen depth bottom is greater than top @model_validator(mode="after") def check_depths(self): @@ -106,7 +99,6 @@ def check_depths(self): class BaseThingResponse(ORMBaseModel): name: str thing_type: str - id: int release_status: str @@ -135,8 +127,7 @@ class SpringResponse(BaseThingResponse): class ThingResponse(WellResponse, SpringResponse): - location: LocationResponse | None = None # Optional location details - geometry: dict | None = None + pass class ThingIdLinkResponse(ORMBaseModel): @@ -207,32 +198,26 @@ class UpdateThing(BaseModel): Schema for updating a thing. """ - # location_id: int | None = None # Optional location ID for the thing name: str | None = None # Optional name for the thing release_status: str | None = None - # group: str | None = None # Optional group for the thing - # description: str | None = None # Optional description of the thing - # tags: list[str] | None = None # Optional tags associated with the thing class UpdateWell(UpdateThing): - # location_id: int | None = None # Optional location ID for the well - # name: str | None = None # Optional name for the well - # api_id: str | None = None - # ose_pod_id: str | None = None + well_type: str | None = None well_depth: float | None = None # in feet hole_depth: float | None = None # in feet well_construction_notes: str | None = None - # group: str | None = None # Optional group for the well + +class UpdateSpring(UpdateThing): + spring_type: str | None = None class UpdateThingIdLink(BaseModel): alternate_organization: str | None = None alternate_id: str | None = None relation: str | None = None - thing_id: int | None = None class UpdateWellScreen(BaseModel): diff --git a/services/thing_helper.py b/services/thing_helper.py index de5b06c60..8ca1b750b 100644 --- a/services/thing_helper.py +++ b/services/thing_helper.py @@ -13,17 +13,20 @@ # See the License for the specific language governing permissions and # limitations under the License. # =============================================================================== +from fastapi import Request from fastapi_pagination.ext.sqlalchemy import paginate from pydantic import BaseModel -from sqlalchemy import select, func, and_ +from sqlalchemy import select from sqlalchemy.orm import Session +from starlette.status import HTTP_404_NOT_FOUND, HTTP_409_CONFLICT -from db import LocationThingAssociation, Thing, Base, Location -from schemas.location import LocationResponse -from db.group import Group, GroupThingAssociation +from db import LocationThingAssociation, Thing, Base, Location, WellScreen +from db.group import GroupThingAssociation from services.audit_helper import audit_add +from services.crud_helper import model_patcher +from services.exceptions_helper import PydanticStyleException from services.geospatial_helper import make_within_wkt -from services.query_helper import make_query, order_sort_filter +from services.query_helper import make_query, order_sort_filter, simple_get_by_id from shapely import wkb from shapely.geometry import mapping @@ -41,124 +44,158 @@ def get_db_things( query, session, sort, - thing_type: str | list[str] = None, - with_location: bool = False, + thing_type: str = None, within: str = None, -): +) -> list: if query: sql = select(Thing).where(make_query(Thing, query)) else: sql = select(Thing) - if with_location or within: + if thing_type: + sql = sql.where(Thing.thing_type == thing_type) + + if within: sql = sql.join( LocationThingAssociation, Thing.id == LocationThingAssociation.thing_id ) sql = sql.join(Location) + sql = make_within_wkt(sql, within) - if isinstance(thing_type, str): - thing_type = thing_type.lower() - thing_type = [thing_type] - elif isinstance(thing_type, list): - thing_type = [t.lower() for t in thing_type] - - sql = sql.where(Thing.thing_type.in_(thing_type)) if thing_type else sql sql = order_sort_filter(sql, Thing, sort, order, filter_) - if within: - sql = make_within_wkt(sql, within) - - def transformer(records): - thing_ids = sorted([record.id for record in records]) - subq = ( - select( - LocationThingAssociation.thing_id, - func.max(LocationThingAssociation.effective_start).label("max_start"), - ) - .where(LocationThingAssociation.thing_id.in_(thing_ids)) - .group_by(LocationThingAssociation.thing_id) - .subquery() - ) - stmt = ( - select(Location) - .join( - LocationThingAssociation, - Location.id == LocationThingAssociation.location_id, - ) - .join(Thing) - .join( - subq, - and_( - LocationThingAssociation.thing_id == subq.c.thing_id, - LocationThingAssociation.effective_start == subq.c.max_start, - ), - ) - .order_by(Thing.id.asc()) + return paginate(query=sql, conn=session) + + +def get_thing_type_from_request(request: Request) -> str: + path = request.url.path + path_components = path.split("/") + if len(path_components) == 2: + # no thing type specified in path + thing_type_in_path = path_components[1] + if len(path_components) >= 3: + # thing type specified in path + thing_type_in_path = path_components[2] + + thing_type = thing_type_in_path.replace("-", " ") + return thing_type + + +def verify_thing_type_correspondence(thing: Thing, request: Request): + thing_type = get_thing_type_from_request(request) + if thing.thing_type != thing_type: + raise PydanticStyleException( + status_code=HTTP_404_NOT_FOUND, + detail=[ + { + "loc": ["path", "thing_id"], + "type": "value_error", + "input": {"thing_id": thing.id}, + "msg": f"Thing with ID {thing.id} is not a {thing_type} Thing. It is a {thing.thing_type} Thing.", + } + ], ) - locations = session.scalars(stmt).all() - for r, l in zip(records, locations): - r.location = LocationResponse.model_validate(l) - r.geometry = wkb_to_geojson(l.point) if l.point else None +def get_thing_of_a_thing_type_by_id(session: Session, request: Request, thing_id: int): + thing = simple_get_by_id(session, Thing, thing_id) - return records + verify_thing_type_correspondence(thing, request) - return paginate(query=sql, conn=session, transformer=transformer) + return thing -# REFACTOR TODO: use enums (or enum-like object) for thing_type def add_thing( - session: Session, data: BaseModel | dict, thing_type: str = None, user: dict = None + session: Session, + data: BaseModel | dict, + user: dict = None, + request: Request | None = None, + thing_type: str | None = None, # to be used only for data transfers, not the API ) -> Base: + if request is not None: + thing_type = get_thing_type_from_request(request) if isinstance(data, BaseModel): data = data.model_dump() location_id = data.pop("location_id", None) - group_id = data.pop("group_id", None) - if not group_id: - group_name = data.pop("group", None) - if group_name is not None: - sql = select(Group).where(Group.name == group_name) - dbg = session.scalars(sql).one_or_none() - if dbg: - group_id = dbg.id - else: - raise ValueError(f"Group '{group_name}' not found.") - - if not thing_type: - thing_type = data.get("thing_type", None) - if not thing_type: - raise ValueError("Thing type must be specified.") - - thing = Thing(**data) - thing.thing_type = thing_type - - audit_add(user, thing) - - session.add(thing) - session.commit() - session.refresh(thing) - - if group_id: - assoc = GroupThingAssociation() - audit_add(user, assoc) - assoc.group_id = group_id - assoc.thing_id = thing.id - session.add(assoc) - - if location_id is not None: - assoc = LocationThingAssociation() - audit_add(user, assoc) - assoc.location_id = location_id - assoc.thing_id = thing.id - session.add(assoc) - - session.commit() + + try: + thing = Thing(**data) + thing.thing_type = thing_type + + audit_add(user, thing) + + session.add(thing) + session.flush() + session.refresh(thing) + + # endpoint catches ProgrammingError if location_id or group_id do not exist + if group_id: + assoc = GroupThingAssociation() + audit_add(user, assoc) + assoc.group_id = group_id + assoc.thing_id = thing.id + session.add(assoc) + + if location_id is not None: + assoc = LocationThingAssociation() + audit_add(user, assoc) + assoc.location_id = location_id + assoc.thing_id = thing.id + session.add(assoc) + + session.commit() + except Exception as e: + session.rollback() + raise e return thing +def add_well_screen(session, well_screen_data: BaseModel, user: dict = None): + try: + well_screen_data_dump = well_screen_data.model_dump() + well_screen = WellScreen(**well_screen_data_dump) + audit_add(user, well_screen) + + session.add(well_screen) + session.flush() + + thing = session.get(Thing, well_screen_data.thing_id) + if thing.thing_type != "water well": + raise PydanticStyleException( + status_code=HTTP_409_CONFLICT, + detail=[ + { + "loc": ["body", "thing_id"], + "type": "value_error", + "input": {"thing_id": thing.id}, + "msg": f"Thing with ID {thing.id} is not a water well Thing. It is a {thing.thing_type} Thing.", + } + ], + ) + + session.commit() + except Exception as e: + session.rollback() + raise e + return well_screen + + +def patch_thing( + session: Session, + request: Request, + thing_id: int, + payload: BaseModel, + user: dict, +): + thing = simple_get_by_id(session, Thing, thing_id) + + verify_thing_type_correspondence(thing, request) + + return model_patcher(session, Thing, thing_id, payload, user) + + # ============= EOF ============================================= diff --git a/tests/conftest.py b/tests/conftest.py index c80b5f22d..700f8637f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,6 @@ from db import * from db.engine import session_ctx -from services.thing_helper import add_thing @pytest.fixture(scope="session") @@ -34,20 +33,133 @@ def second_location(): @pytest.fixture(scope="session") -def thing(location): +def water_well_thing(location): with session_ctx() as session: - wt = add_thing( - session, - { - "location_id": location.id, - "name": "Test Well", - }, - "water well", + water_well = Thing( + name="Test Well", + thing_type="water well", + release_status="draft", + well_type="Production", + well_depth=10, + hole_depth=10, + well_construction_notes="Test well construction notes", ) + session.add(water_well) + session.commit() + session.refresh(water_well) + + assoc = LocationThingAssociation() + assoc.location_id = location.id + assoc.thing_id = water_well.id + session.add(assoc) + session.commit() + yield water_well - yield wt - session.close() +@pytest.fixture(scope="session") +def well_screen(water_well_thing): + with session_ctx() as session: + screen = WellScreen( + thing_id=water_well_thing.id, + screen_depth_top=10.0, + screen_depth_bottom=20.0, + screen_type="PVC", + screen_description="Test well screen description", + ) + session.add(screen) + session.commit() + yield screen + + +@pytest.fixture(scope="function") +def second_well_screen(water_well_thing): + with session_ctx() as session: + screen = WellScreen( + thing_id=water_well_thing.id, + screen_depth_top=30.0, + screen_depth_bottom=40.0, + screen_type="PVC", + screen_description="Test well screen description", + ) + session.add(screen) + session.commit() + yield screen + session.delete(screen) + session.commit() + + +@pytest.fixture(scope="session") +def thing_id_link(water_well_thing): + with session_ctx() as session: + id_link = ThingIdLink( + thing_id=water_well_thing.id, + relation="same_as", + alternate_id="4321-1234", + alternate_organization="USGS", + ) + session.add(id_link) + session.commit() + yield id_link + + +@pytest.fixture(scope="function") +def second_thing_id_link(water_well_thing): + with session_ctx() as session: + id_link = ThingIdLink( + thing_id=water_well_thing.id, + relation="same_as", + alternate_id="4321-1234", + alternate_organization="USGS", + ) + session.add(id_link) + session.commit() + yield id_link + session.delete(id_link) + session.commit() + + +@pytest.fixture(scope="session") +def spring_thing(location): + with session_ctx() as session: + spring = Thing( + name="Test Spring", + thing_type="spring", + release_status="draft", + spring_type="Artesian", + ) + session.add(spring) + session.commit() + session.refresh(spring) + + assoc = LocationThingAssociation() + assoc.location_id = location.id + assoc.thing_id = spring.id + session.add(assoc) + session.commit() + yield spring + + +@pytest.fixture(scope="function") +def second_spring_thing(location): + with session_ctx() as session: + spring = Thing( + name="Second Test Spring", + thing_type="spring", + release_status="draft", + spring_type="Artesian", + ) + session.add(spring) + session.commit() + session.refresh(spring) + + assoc = LocationThingAssociation() + assoc.location_id = location.id + assoc.thing_id = spring.id + session.add(assoc) + session.commit() + yield spring + session.delete(spring) + session.commit() @pytest.fixture(scope="session") @@ -65,7 +177,6 @@ def sensor(): session.add(sensor) session.commit() yield sensor - session.close() @pytest.fixture(scope="function") @@ -85,15 +196,14 @@ def second_sensor(): yield sensor session.delete(sensor) session.commit() - session.close() @pytest.fixture(scope="session") -def sample(thing, sensor): +def sample(water_well_thing, sensor): with session_ctx() as session: sample = Sample( sample_date="2025-01-01T00:00:00Z", - thing_id=thing.id, + thing_id=water_well_thing.id, sample_type="groundwater", sampler_name="Test Sampler", release_status="draft", @@ -110,14 +220,12 @@ def sample(thing, sensor): session.commit() yield sample - session.close() - @pytest.fixture(scope="function") -def second_sample(thing, sensor): +def second_sample(water_well_thing, sensor): with session_ctx() as session: sample = Sample( - thing_id=thing.id, + thing_id=water_well_thing.id, sample_type="groundwater", field_sample_id="FS-9999999", sample_date="2025-01-01T00:00:00Z", @@ -136,11 +244,10 @@ def second_sample(thing, sensor): yield sample session.delete(sample) session.commit() - session.close() @pytest.fixture(scope="session") -def contact(thing): +def contact(water_well_thing): with session_ctx() as session: contact = Contact( name="Test Contact", @@ -150,15 +257,15 @@ def contact(thing): session.commit() session.refresh(contact) - association = ThingContactAssociation(thing_id=thing.id, contact_id=contact.id) + association = ThingContactAssociation( + thing_id=water_well_thing.id, contact_id=contact.id + ) session.add(association) session.commit() session.refresh(association) yield contact - session.close() - @pytest.fixture(scope="session") def address(contact): @@ -178,8 +285,6 @@ def address(contact): session.refresh(address) yield address - session.close() - @pytest.fixture(scope="session") def email(contact): @@ -192,8 +297,6 @@ def email(contact): session.refresh(email) yield email - session.close() - @pytest.fixture(scope="session") def phone(contact): @@ -206,8 +309,6 @@ def phone(contact): session.refresh(phone) yield phone - session.close() - @pytest.fixture(scope="function") def second_contact(): @@ -224,7 +325,6 @@ def second_contact(): session.delete(contact) session.commit() - session.close() @pytest.fixture(scope="function") @@ -241,7 +341,6 @@ def second_email(second_contact): yield email session.delete(email) session.commit() - session.close() @pytest.fixture(scope="function") @@ -258,7 +357,6 @@ def second_phone(second_contact): yield phone session.delete(phone) session.commit() - session.close() @pytest.fixture(scope="function") @@ -280,7 +378,6 @@ def second_address(second_contact): yield address session.delete(address) session.commit() - session.close() @pytest.fixture(scope="session") @@ -300,14 +397,12 @@ def asset(): session.refresh(asset) yield asset - session.close() - @pytest.fixture(scope="function") -def asset_with_associated_thing(thing): +def asset_with_associated_thing(water_well_thing): with session_ctx() as session: asset = Asset( - name="Test Asset with thing", + name="Test Asset with water_well_thing", label="test label", mime_type="application/pdf", size=12345, @@ -319,7 +414,9 @@ def asset_with_associated_thing(thing): session.commit() session.refresh(asset) - association = AssetThingAssociation(asset_id=asset.id, thing_id=thing.id) + association = AssetThingAssociation( + asset_id=asset.id, thing_id=water_well_thing.id + ) session.add(association) session.commit() session.refresh(association) @@ -328,7 +425,6 @@ def asset_with_associated_thing(thing): session.delete(asset) session.delete(association) session.commit() - session.close() @pytest.fixture(scope="function") @@ -348,7 +444,7 @@ def second_asset(): session.refresh(asset) yield asset session.delete(asset) - session.close() + session.commit() @pytest.fixture(scope="session") @@ -369,8 +465,6 @@ def groundwater_level_observation(sensor, sample): session.commit() yield observation - session.close() - @pytest.fixture(scope="session") def water_chemistry_observation(sensor, sample): @@ -388,8 +482,6 @@ def water_chemistry_observation(sensor, sample): session.commit() yield observation - session.close() - @pytest.fixture(scope="session") def geothermal_observation(sensor, sample): @@ -408,8 +500,6 @@ def geothermal_observation(sensor, sample): session.commit() yield observation - session.close() - @pytest.fixture(scope="function") def observation_to_delete(sample, sensor): @@ -429,7 +519,7 @@ def observation_to_delete(sample, sensor): @pytest.fixture(scope="session") -def group(thing): +def group(water_well_thing): with session_ctx() as session: group = Group( name="Test Group", @@ -442,7 +532,7 @@ def group(thing): session.refresh(group) group_thing_association = GroupThingAssociation( - group_id=group.id, thing_id=thing.id + group_id=group.id, thing_id=water_well_thing.id ) session.add(group_thing_association) session.commit() @@ -450,11 +540,9 @@ def group(thing): yield group - session.close() - @pytest.fixture(scope="function") -def second_group(thing): +def second_group(water_well_thing): with session_ctx() as session: group = Group( name="Second Test Group", @@ -467,7 +555,7 @@ def second_group(thing): session.refresh(group) group_thing_association = GroupThingAssociation( - group_id=group.id, thing_id=thing.id + group_id=group.id, thing_id=water_well_thing.id ) session.add(group_thing_association) session.commit() @@ -475,8 +563,6 @@ def second_group(thing): yield group - session.close() - @pytest.fixture(scope="session") def lexicon_category(): @@ -545,7 +631,6 @@ def second_lexicon_term(lexicon_category): session.refresh(term_category_association) yield term - session.commit() @pytest.fixture(scope="session") @@ -567,7 +652,6 @@ def third_lexicon_term(lexicon_category): session.refresh(term_category_association) yield term - session.commit() @pytest.fixture(scope="session") @@ -589,7 +673,6 @@ def fourth_lexicon_term(lexicon_category): session.refresh(term_category_association) yield term - session.commit() @pytest.fixture(scope="session") diff --git a/tests/test_asset.py b/tests/test_asset.py index e56b463a4..7cce58112 100644 --- a/tests/test_asset.py +++ b/tests/test_asset.py @@ -86,9 +86,9 @@ def test_upload_asset(): assert "storage_path" in data -def test_add_asset(thing): +def test_add_asset(water_well_thing): payload = { - "thing_id": thing.id, + "thing_id": water_well_thing.id, "name": "test_asset.png", "label": "Test Asset", "uri": "https://storage.googleapis.com/mock-bucket/mock-asset", @@ -114,7 +114,7 @@ def test_add_asset(thing): cleanup_post_test(Asset, data["id"]) -def test_add_asset_409_bad_thing_id(thing): +def test_add_asset_409_bad_thing_id(water_well_thing): bad_thing_id = 99999 payload = { "thing_id": bad_thing_id, @@ -160,9 +160,9 @@ def test_get_assets(asset, asset_with_associated_thing): assert data["items"][1]["signed_url"] == None -def test_get_assets_thing_id(asset_with_associated_thing, thing): +def test_get_assets_thing_id(asset_with_associated_thing, water_well_thing): with patch("api.asset.get_storage_bucket", return_value=MockStorageBucket()): - query_parameters = {"thing_id": thing.id} + query_parameters = {"thing_id": water_well_thing.id} response = client.get("/asset", params=query_parameters) assert response.status_code == 200 data = response.json() diff --git a/tests/test_contact.py b/tests/test_contact.py index 19ec570e4..33a9ee1b0 100644 --- a/tests/test_contact.py +++ b/tests/test_contact.py @@ -59,11 +59,11 @@ def test_validate_email(): # ADD tests ==================================================================== -def test_add_contact(thing): +def test_add_contact(spring_thing): payload = { "name": "Test Contact 2", "role": "Owner", - "thing_id": thing.id, + "thing_id": spring_thing.id, "emails": [{"email": "testcontact2@gmail.com", "email_type": "Primary"}], "phones": [{"phone_number": "+14153334444", "phone_type": "Primary"}], "addresses": [ diff --git a/tests/test_geospatial.py b/tests/test_geospatial.py index 82592d195..442e96004 100644 --- a/tests/test_geospatial.py +++ b/tests/test_geospatial.py @@ -102,6 +102,8 @@ def populate(): session.delete(loc1) session.delete(loc2) session.delete(group) + session.delete(thing1) + session.delete(thing2) session.commit() @@ -112,7 +114,6 @@ def test_get_project_area(): assert "type" in data assert data["type"] == "FeatureCollection" assert "features" in data - print(data) assert len(data["features"]) > 0 assert data["features"][0]["properties"]["group_id"] == 1 assert data["features"][0]["properties"]["group_name"] == "Test Group Foo" diff --git a/tests/test_location.py b/tests/test_location.py index cee06ab2b..f69068c3f 100644 --- a/tests/test_location.py +++ b/tests/test_location.py @@ -63,13 +63,18 @@ def test_add_location(): def test_update_location(location): - payload = {"point": "POINT (10.1 20.2)", "release_status": "draft"} + payload = { + "point": "POINT (10.1 20.2)", + "release_status": "draft", + "name": "patched name", + } response = client.patch(f"/location/{location.id}", json=payload) assert response.status_code == 200 data = response.json() assert data["id"] == location.id assert data["point"] == payload["point"] assert data["release_status"] == payload["release_status"] + assert data["name"] == payload["name"] # cleanup after test cleanup_patch_test(Location, payload, location) diff --git a/tests/test_observation.py b/tests/test_observation.py index 6dcfc85de..1200af45b 100644 --- a/tests/test_observation.py +++ b/tests/test_observation.py @@ -429,10 +429,13 @@ def test_get_groundwater_observation_by_sample(sample): assert len(items) > 0, "Expected at least one groundwater observation for the thing" -def test_get_groundwater_observation_by_thing(thing): +def test_get_groundwater_observation_by_thing(water_well_thing): response = client.get( "/observation/groundwater-level", - params={"thing_id": thing.id, "observed_property": "groundwater level"}, + params={ + "thing_id": water_well_thing.id, + "observed_property": "groundwater level", + }, ) assert response.status_code == 200 data = response.json() diff --git a/tests/test_sample.py b/tests/test_sample.py index d3d432413..35961c99b 100644 --- a/tests/test_sample.py +++ b/tests/test_sample.py @@ -53,12 +53,12 @@ def test_validate_sample_top_and_bottom(): # ============= Post tests for samples ============================================= -def test_add_sample(thing, sensor): +def test_add_sample(spring_thing, sensor): """ Test adding a sample. """ payload = { - "thing_id": thing.id, + "thing_id": spring_thing.id, "sample_type": "groundwater", "field_sample_id": "FS-1234567", "sample_date": "2025-01-01T00:00:00Z", @@ -96,12 +96,12 @@ def test_add_sample(thing, sensor): cleanup_post_test(Sample, data["id"]) -def test_409_add_sample_invalid_field_sample_id(sample, thing): +def test_409_add_sample_invalid_field_sample_id(sample, spring_thing): """ Test adding a sample with an invalid field_sample_id. """ payload = { - "thing_id": thing.id, + "thing_id": spring_thing.id, "sample_type": "groundwater", "field_sample_id": sample.field_sample_id, # This should already exist "sample_date": "2025-01-01T00:00:00Z", diff --git a/tests/test_search.py b/tests/test_search.py index 620c74cc2..e7619b0c9 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -22,14 +22,14 @@ from tests import client -def test_search_api(thing, contact, email, phone, address): +def test_search_api(water_well_thing, spring_thing, contact): response = client.get("/search", params={"q": "Test"}) assert response.status_code == 200 data = response.json() assert isinstance(data, dict) items = data.get("items") assert isinstance(items, list) - assert len(items) == 2 + assert len(items) == 3 @pytest.mark.skip(reason="This test is not working .") diff --git a/tests/test_thing.py b/tests/test_thing.py index 8c0cfd0b7..a37ab3713 100644 --- a/tests/test_thing.py +++ b/tests/test_thing.py @@ -14,7 +14,9 @@ # limitations under the License. # =============================================================================== import pytest -from tests import client, override_authentication + +from db import Thing, WellScreen, ThingIdLink +from tests import client, override_authentication, cleanup_post_test, cleanup_patch_test from main import app from core.dependencies import ( admin_function, @@ -48,340 +50,829 @@ def override_authentication_dependency_fixture(): app.dependency_overrides = {} -def test_add_group(): - response = client.post( - "/group", - json={ - "name": "collabnet", - "description": "CollabNet Group for testing", - }, - ) +# POST tests =================================================================== + + +def test_add_water_well(location, group): + payload = { + "location_id": location.id, + "group_id": group.id, + "release_status": "draft", + "name": "Test Well", + "well_type": "Monitoring", + "well_depth": 100.0, + "hole_depth": 110, + "well_construction_notes": "this is a test of notes", + } + + response = client.post("/thing/water-well", json=payload) assert response.status_code == 201 data = response.json() assert "id" in data - assert data["name"] == "collabnet" - - -def test_add_well(location): - # response = client.post( - # "/lexicon/add", json={"term": "Monitoring", "definition": "Monitoring Well"} - # ) - # assert response.status_code == 200 - # response = client.post( - # "/lexicon/add", json={"term": "Production", "definition": "Production Well"} - # ) - # assert response.status_code == 200 - - response = client.post( - "/thing", - json={ - "thing_type": "water well", - "location_id": location.id, - "name": "Test Well", - "api_id": "1001-0001", - "ose_pod_id": "RA-0001", - "well_type": "Monitoring", - "well_depth": 100.0, - "well_construction_notes": "this is a test of notes", - }, - ) - assert response.status_code == 201 + assert "created_at" in data + assert data["release_status"] == payload["release_status"] + assert data["name"] == payload["name"] + assert data["thing_type"] == "water well" + assert data["well_type"] == payload["well_type"] + assert data["hole_depth"] == payload["hole_depth"] + assert data["well_depth"] == payload["well_depth"] + assert data["well_construction_notes"] == payload["well_construction_notes"] + + cleanup_post_test(Thing, data["id"]) + + +def test_add_water_well_409_bad_group_id(location): + bad_group_id = 9999 + payload = { + "location_id": location.id, + "group_id": bad_group_id, # Invalid group ID + "release_status": "draft", + "name": "Test Well", + "well_type": "Monitoring", + "well_depth": 100.0, + "hole_depth": 110, + "well_construction_notes": "this is a test of notes", + } + + response = client.post("/thing/water-well", json=payload) + assert response.status_code == 409 data = response.json() - assert "id" in data - assert data["name"] == "Test Well" - assert data["well_type"] == "Monitoring" - - response = client.post( - "/thing", - json={ - "thing_type": "water well", - "location_id": location.id, - "name": "Test Well 2", - "api_id": "1001-0002", - "ose_pod_id": "RA-0002", - "well_type": "Production", - "well_depth": 1200.0, - "group": "collabnet", - }, - ) + assert data["detail"][0]["loc"] == ["body", "group_id"] + assert data["detail"][0]["msg"] == f"Group with ID {bad_group_id} not found." + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"group_id": bad_group_id} + + +def test_add_water_well_409_bad_location_id(group): + bad_location_id = 9999 + payload = { + "location_id": bad_location_id, + "group_id": group.id, # Invalid group ID + "release_status": "draft", + "name": "Test Well", + "well_type": "Monitoring", + "well_depth": 100.0, + "hole_depth": 110, + "well_construction_notes": "this is a test of notes", + } + + response = client.post("/thing/water-well", json=payload) + assert response.status_code == 409 + data = response.json() + assert data["detail"][0]["loc"] == ["body", "location_id"] + assert data["detail"][0]["msg"] == f"Location with ID {bad_location_id} not found." + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"location_id": bad_location_id} + + +def test_add_spring(location, group): + payload = { + "location_id": location.id, + "group_id": group.id, + "name": "test spring", + "release_status": "draft", + "spring_type": "Ephemeral", + } + response = client.post("/thing/spring", json=payload) assert response.status_code == 201 data = response.json() assert "id" in data + assert "created_at" in data + assert data["name"] == payload["name"] + assert data["release_status"] == payload["release_status"] + assert data["spring_type"] == payload["spring_type"] + + cleanup_post_test(Thing, data["id"]) + + +def test_add_spring_409_bad_group_id(location): + bad_group_id = 9999 + payload = { + "location_id": location.id, + "group_id": bad_group_id, # Invalid group ID + "name": "test spring", + "release_status": "draft", + "spring_type": "Ephemeral", + } + response = client.post("/thing/spring", json=payload) + assert response.status_code == 409 + data = response.json() + assert data["detail"][0]["loc"] == ["body", "group_id"] + assert data["detail"][0]["msg"] == f"Group with ID {bad_group_id} not found." + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"group_id": bad_group_id} + + +def test_add_spring_409_bad_location_id(group): + bad_location_id = 9999 + payload = { + "location_id": bad_location_id, + "group_id": group.id, # Invalid group ID + "name": "test spring", + "release_status": "draft", + "spring_type": "Ephemeral", + } + response = client.post("/thing/spring", json=payload) + assert response.status_code == 409 + data = response.json() + assert data["detail"][0]["loc"] == ["body", "location_id"] + assert data["detail"][0]["msg"] == f"Location with ID {bad_location_id} not found." + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"location_id": bad_location_id} -def test_add_spring(): - response = client.post( - "/thing", - json={ - "location_id": 1, - "name": "Test Spring", - "thing_type": "spring", - "spring_type": "Ephemeral", - }, - ) +def test_add_well_screen(water_well_thing): + payload = { + "thing_id": water_well_thing.id, + "screen_depth_top": 10.0, + "screen_depth_bottom": 20.0, + "screen_type": "PVC", + } + response = client.post("/thing/well-screen", json=payload) + assert response.status_code == 201 data = response.json() assert "id" in data - - assert "name" in data - assert data["name"] == "Test Spring" - - assert "thing_type" in data - assert data["thing_type"] == "spring" - - assert "spring_type" in data - assert data["spring_type"] == "Ephemeral" - - -def test_add_well_screen(): - # response = client.post( - # "/lexicon/add", - # json={"term": "PVC", "definition": "PVC Well Screen"}, - # ) - # assert response.status_code == 200 - response = client.post( - "/thing/well-screen", - json={ - "thing_id": 1, - "screen_depth_top": 10.0, - "screen_depth_bottom": 20.0, - "screen_type": "PVC", - }, + assert "created_at" in data + assert data["thing_id"] == water_well_thing.id + assert data["screen_depth_top"] == payload["screen_depth_top"] + assert data["screen_depth_bottom"] == payload["screen_depth_bottom"] + assert data["screen_type"] == payload["screen_type"] + + cleanup_post_test(WellScreen, data["id"]) + + +def test_add_well_screen_409_bad_thing_id(): + bad_thing_id = 9999 + payload = { + "thing_id": bad_thing_id, + "screen_depth_top": 10.0, + "screen_depth_bottom": 20.0, + "screen_type": "PVC", + } + response = client.post("/thing/well-screen", json=payload) + assert response.status_code == 409 + data = response.json() + assert data["detail"][0]["loc"] == ["body", "thing_id"] + assert data["detail"][0]["msg"] == f"Thing with ID {bad_thing_id} not found." + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"thing_id": bad_thing_id} + + +def test_well_add_well_screen_409_wrong_thing_type(spring_thing): + payload = { + "thing_id": spring_thing.id, + "screen_depth_top": 10.0, + "screen_depth_bottom": 20.0, + "screen_type": "PVC", + } + response = client.post("/thing/well-screen", json=payload) + assert response.status_code == 409 + data = response.json() + assert data["detail"][0]["loc"] == ["body", "thing_id"] + assert ( + data["detail"][0]["msg"] + == f"Thing with ID {spring_thing.id} is not a water well Thing. It is a spring Thing." ) + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"thing_id": spring_thing.id} - assert response.status_code == 201 + +def test_add_well_screen_409_bad_screen_type(water_well_thing): + payload = { + "thing_id": water_well_thing.id, + "screen_depth_top": 10.0, + "screen_depth_bottom": 20.0, + "screen_type": "NotARealType", + } + response = client.post("/thing/well-screen", json=payload) + + assert response.status_code == 409 data = response.json() - assert "id" in data - assert data["thing_id"] == 1 - - -def test_add_thing_link(): - response = client.post( - "/thing/id-link", - json={ - "thing_id": 1, - "relation": "same_as", - "alternate_id": "4321-1234", - "alternate_organization": "USGS", - }, + assert data["detail"][0]["loc"] == ["body", "screen_type"] + assert ( + data["detail"][0]["msg"] + == f"{payload['screen_type']} is an invalid screen type. Valid types are: PVC | Steel | Concrete." ) + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"screen_type": payload["screen_type"]} + + +def test_add_thing_link(spring_thing): + payload = { + "thing_id": spring_thing.id, + "relation": "same_as", + "alternate_id": "4321-1234", + "alternate_organization": "USGS", + } + response = client.post("/thing/id-link", json=payload) assert response.status_code == 201 data = response.json() assert "id" in data - assert data["thing_id"] == 1 - assert data["alternate_id"] == "4321-1234" + assert "created_at" in data + assert data["thing_id"] == spring_thing.id + assert data["relation"] == payload["relation"] + assert data["alternate_id"] == payload["alternate_id"] + assert data["alternate_organization"] == payload["alternate_organization"] + + cleanup_post_test(ThingIdLink, data["id"]) + + +def test_add_thing_id_link_409_bad_thing_id(): + bad_thing_id = 9999 + payload = { + "thing_id": bad_thing_id, + "relation": "same_as", + "alternate_id": "4321-1234", + "alternate_organization": "USGS", + } + response = client.post("/thing/id-link", json=payload) + assert response.status_code == 409 + data = response.json() + assert data["detail"][0]["loc"] == ["body", "thing_id"] + assert data["detail"][0]["msg"] == f"Thing with ID {bad_thing_id} not found." + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"thing_id": bad_thing_id} -# ===================== get ========================== -# def test_get_thing_by_id(): -# # response = client.get("/thing?thing_id=1") -# response = client.get("/thing/base/1") -# assert response.status_code == 200 -# data = response.json() -# # assert "items" in data -# # items = data["items"] -# # assert len(items) == 1 -# assert data["id"] == 1 -# assert data["name"] == "Test Thing" +# GET tests ==================================================================== -def test_get_wells(): - response = client.get("/thing?thing_type=water well") +def test_get_water_wells(water_well_thing): + response = client.get("/thing/water-well") assert response.status_code == 200 - assert len(response.json()) > 0 + data = response.json() + assert data["total"] == 1 + assert data["items"][0]["id"] == water_well_thing.id + assert data["items"][0][ + "created_at" + ] == water_well_thing.created_at.isoformat().replace("+00:00", "Z") + assert data["items"][0]["name"] == water_well_thing.name + assert data["items"][0]["thing_type"] == water_well_thing.thing_type + assert data["items"][0]["release_status"] == water_well_thing.release_status + assert data["items"][0]["well_type"] == water_well_thing.well_type + assert data["items"][0]["well_depth"] == water_well_thing.well_depth + assert data["items"][0]["hole_depth"] == water_well_thing.hole_depth + assert ( + data["items"][0]["well_construction_notes"] + == water_well_thing.well_construction_notes + ) -def test_get_springs(): - response = client.get("/thing?thing_type=spring") +def test_get_water_well_by_id(water_well_thing): + response = client.get(f"/thing/water-well/{water_well_thing.id}") assert response.status_code == 200 - assert len(response.json()) > 0 + data = response.json() + assert data["id"] == water_well_thing.id + assert data["created_at"] == water_well_thing.created_at.isoformat().replace( + "+00:00", "Z" + ) + assert data["name"] == water_well_thing.name + assert data["thing_type"] == water_well_thing.thing_type + assert data["release_status"] == water_well_thing.release_status + assert data["well_type"] == water_well_thing.well_type + assert data["well_depth"] == water_well_thing.well_depth + assert data["hole_depth"] == water_well_thing.hole_depth + assert data["well_construction_notes"] == water_well_thing.well_construction_notes + + +def test_get_water_well_by_id_404_not_found(water_well_thing): + bad_id = 99999 + response = client.get(f"/thing/water-well/{bad_id}") + assert response.status_code == 404 + data = response.json() + assert "detail" in data + assert data["detail"] == f"Thing with ID {bad_id} not found." -# def test_get_well_by_id(): -# response = client.get("/thing/well/1") -# assert response.status_code == 200 -# data = response.json() -# assert data["id"] == 1 +def test_get_water_well_by_id_404_wrong_type(spring_thing): + response = client.get(f"/thing/water-well/{spring_thing.id}") + assert response.status_code == 404 + data = response.json() + assert ( + data["detail"][0]["msg"] + == f"Thing with ID {spring_thing.id} is not a water well Thing. It is a spring Thing." + ) + assert data["detail"][0]["loc"] == ["path", "thing_id"] + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"thing_id": spring_thing.id} -def test_get_well_screens(): - # TODO: improve test indepedence - response = client.get("/thing/well-screen") +def test_get_springs(spring_thing): + response = client.get("/thing/spring") + assert response.status_code == 200 + data = response.json() + assert data["total"] == 1 + assert data["items"][0]["id"] == spring_thing.id + assert data["items"][0][ + "created_at" + ] == spring_thing.created_at.isoformat().replace("+00:00", "Z") + assert data["items"][0]["name"] == spring_thing.name + assert data["items"][0]["thing_type"] == spring_thing.thing_type + assert data["items"][0]["release_status"] == spring_thing.release_status + assert data["items"][0]["spring_type"] == spring_thing.spring_type + + +def test_get_spring_by_id(spring_thing): + response = client.get(f"/thing/spring/{spring_thing.id}") assert response.status_code == 200 - assert len(response.json()) > 0 + data = response.json() + assert data["id"] == spring_thing.id + assert data["created_at"] == spring_thing.created_at.isoformat().replace( + "+00:00", "Z" + ) + assert data["name"] == spring_thing.name + assert data["thing_type"] == spring_thing.thing_type + assert data["release_status"] == spring_thing.release_status + assert data["spring_type"] == spring_thing.spring_type -def test_get_thing_links(): - # TODO: improve test indepedence - response = client.get("/thing/id-link") - assert response.status_code == 200 - assert len(response.json()) > 0 +def test_get_spring_by_id_404_not_found(spring_thing): + bad_id = 99999 + response = client.get(f"/thing/spring/{bad_id}") + assert response.status_code == 404 + data = response.json() + assert "detail" in data + assert data["detail"] == f"Thing with ID {bad_id} not found." -def test_get_thing_links_by_id(): - # TODO: improve test indepedence - response = client.get("/thing/id-link/1") - assert response.status_code == 200 +def test_get_spring_by_id_404_wrong_type(water_well_thing): + response = client.get(f"/thing/spring/{water_well_thing.id}") + assert response.status_code == 404 data = response.json() - assert data["id"] == 1 - assert data["thing_id"] == 1 - assert data["relation"] == "same_as" - assert data["alternate_id"] == "4321-1234" - assert data["alternate_organization"] == "USGS" + assert ( + data["detail"][0]["msg"] + == f"Thing with ID {water_well_thing.id} is not a spring Thing. It is a water well Thing." + ) + assert data["detail"][0]["loc"] == ["path", "thing_id"] + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"thing_id": water_well_thing.id} -def test_get_thing_links_by_thing_id(): - # TODO: improve test indepedence - response = client.get("/thing/1/id-link") +def test_get_well_screens(well_screen): + response = client.get("/thing/well-screen") assert response.status_code == 200 data = response.json() - assert isinstance(data, dict) - assert "items" in data - data = data["items"] - assert isinstance(data, list) - assert len(data) == 1 - item = data[0] - assert item["id"] == 1 - assert item["thing_id"] == 1 - assert item["relation"] == "same_as" - assert item["alternate_id"] == "4321-1234" - assert item["alternate_organization"] == "USGS" - - -def test_item_get_well_filter(): - response = client.get("/thing", params={"query": "well_type eq 'Monitoring'"}) + assert data["total"] == 1 + assert data["items"][0]["id"] == well_screen.id + assert data["items"][0]["thing_id"] == well_screen.thing_id + assert data["items"][0]["screen_depth_top"] == well_screen.screen_depth_top + assert data["items"][0]["screen_depth_bottom"] == well_screen.screen_depth_bottom + assert data["items"][0]["screen_type"] == well_screen.screen_type + assert data["items"][0]["screen_description"] == well_screen.screen_description + + +def test_get_well_screen_by_id(well_screen): + response = client.get(f"/thing/well-screen/{well_screen.id}") assert response.status_code == 200 data = response.json() - assert "items" in data - assert len(data["items"]) == 1 - # assert "api_id" in data["items"][0] - # assert data["items"][0]["api_id"] == "1001-0002" + assert data["id"] == well_screen.id + assert data["thing_id"] == well_screen.thing_id + assert data["screen_depth_top"] == well_screen.screen_depth_top + assert data["screen_depth_bottom"] == well_screen.screen_depth_bottom + assert data["screen_type"] == well_screen.screen_type + assert data["screen_description"] == well_screen.screen_description + + +def test_get_well_screen_by_id_404_not_found(well_screen): + bad_id = 99999 + response = client.get(f"/thing/well-screen/{bad_id}") + assert response.status_code == 404 + data = response.json() + assert "detail" in data + assert data["detail"] == f"WellScreen with ID {bad_id} not found." -# @pytest.mark.skip -def test_item_get_well_filter_nonexistent(): - # response = client.get("/thing/well", params={"well_type": "9999-9999"}) - response = client.get("/thing", params={"query": "well_type eq 'foo'"}) +def test_get_well_screens_by_water_well(water_well_thing, well_screen): + response = client.get(f"/thing/water-well/{water_well_thing.id}/well-screen") assert response.status_code == 200 data = response.json() - assert "items" in data - assert len(data["items"]) == 0 + assert data["total"] == 1 + assert data["items"][0]["id"] == well_screen.id + assert data["items"][0]["thing_id"] == well_screen.thing_id + assert data["items"][0]["screen_depth_top"] == well_screen.screen_depth_top + assert data["items"][0]["screen_depth_bottom"] == well_screen.screen_depth_bottom + assert data["items"][0]["screen_type"] == well_screen.screen_type + assert data["items"][0]["screen_description"] == well_screen.screen_description + + +def test_get_well_screens_by_water_well_id_404_not_found(water_well_thing, well_screen): + bad_id = 99999 + response = client.get(f"/thing/water-well/{bad_id}/well-screen") + assert response.status_code == 404 + data = response.json() + assert "detail" in data + assert data["detail"] == f"Thing with ID {bad_id} not found." + + +def test_get_well_screens_by_water_well_id_404_wrong_type(spring_thing): + response = client.get(f"/thing/water-well/{spring_thing.id}/well-screen") + assert response.status_code == 404 + data = response.json() + assert ( + data["detail"][0]["msg"] + == f"Thing with ID {spring_thing.id} is not a water well Thing. It is a spring Thing." + ) + assert data["detail"][0]["loc"] == ["path", "thing_id"] + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"thing_id": spring_thing.id} -# @pytest.mark.skip -def test_item_get_well_screens(): - response = client.get("/thing/well-screen/1") +def test_get_thing_id_links(thing_id_link): + response = client.get("/thing/id-link") assert response.status_code == 200 data = response.json() - assert data["id"] == 1 - assert data["thing_id"] == 1 - assert data["screen_depth_top"] == 10.0 - assert data["screen_depth_bottom"] == 20.0 + assert data["total"] == 1 + assert data["items"][0]["id"] == thing_id_link.id + assert data["items"][0]["thing_id"] == thing_id_link.thing_id + assert data["items"][0]["relation"] == thing_id_link.relation + assert data["items"][0]["alternate_id"] == thing_id_link.alternate_id + assert ( + data["items"][0]["alternate_organization"] + == thing_id_link.alternate_organization + ) -# weaver tests -def test_weaver_get_wells_geojson(): - response = client.get("/geospatial", params={"type": "well"}) +def test_get_thing_id_link_by_id(thing_id_link): + response = client.get(f"/thing/id-link/{thing_id_link.id}") assert response.status_code == 200 data = response.json() - assert "type" in data - assert data["type"] == "FeatureCollection" - assert len(data["features"]) > 0 - assert "id" in data["features"][0]["properties"] + assert data["id"] == thing_id_link.id + assert data["thing_id"] == thing_id_link.thing_id + assert data["relation"] == thing_id_link.relation + assert data["alternate_id"] == thing_id_link.alternate_id + assert data["alternate_organization"] == thing_id_link.alternate_organization -def test_weaver_get_all_collabnet_wells(): - response = client.get( - "/geospatial", params={"type": "well", "group": "collabnet"} - ) # TODO: QUESTION: use type filter and a group filter instead of /collabnet endpoint? +def test_get_thing_id_link_by_id_404_not_found(thing_id_link): + bad_id = 99999 + response = client.get(f"/thing/id-link/{bad_id}") + assert response.status_code == 404 + data = response.json() + assert "detail" in data + assert data["detail"] == f"ThingIdLink with ID {bad_id} not found." + + +def test_get_thing_links_by_thing_id(water_well_thing, thing_id_link): + response = client.get(f"/thing/{water_well_thing.id}/id-link") assert response.status_code == 200 data = response.json() + assert data["total"] == 1 + assert data["items"][0]["id"] == thing_id_link.id + assert data["items"][0]["thing_id"] == thing_id_link.thing_id + assert data["items"][0]["relation"] == thing_id_link.relation + assert data["items"][0]["alternate_id"] == thing_id_link.alternate_id + assert ( + data["items"][0]["alternate_organization"] + == thing_id_link.alternate_organization + ) - assert "features" in data - assert len(data["features"]) > 0 - for feature in data["features"]: - assert "geometry" in feature - assert isinstance(feature["geometry"], dict) - assert "properties" in feature - assert isinstance(feature["properties"], dict) - assert "coordinates" in feature["geometry"] - assert "id" in feature or "name" in feature["properties"] - assert "group" in feature["properties"] +def test_get_thing_links_by_thing_id_404_not_found(water_well_thing, thing_id_link): + bad_id = 99999 + response = client.get(f"/thing/{bad_id}/id-link") + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"Thing with ID {bad_id} not found." -def test_weaver_thing_contact_info_by_id(): - response = client.get("/contact?thing_id=1") # or something like this + +def test_get_things(water_well_thing, spring_thing): + response = client.get("/thing") assert response.status_code == 200 data = response.json() - assert isinstance(data, dict) - assert "items" in data - assert len(data["items"]) > 0 - item = data["items"][0] - assert "id" in item - assert "name" in item - assert "addresses" in item - assert "emails" in item - assert "phones" in item - - assert isinstance(item["addresses"], list) - assert isinstance(item["emails"], list) - assert isinstance(item["phones"], list) - - -# Patch tests -def test_patch_thing_link(): - response = client.patch( - "/thing/id-link/1", - json={ - "relation": "same_as", - "alternate_id": "USGS-43211234", - "alternate_organization": "USGS", - }, + assert data["total"] == 2 + + assert data["items"][0]["id"] == water_well_thing.id + assert data["items"][0][ + "created_at" + ] == water_well_thing.created_at.isoformat().replace("+00:00", "Z") + assert data["items"][0]["name"] == water_well_thing.name + assert data["items"][0]["thing_type"] == water_well_thing.thing_type + assert data["items"][0]["release_status"] == water_well_thing.release_status + assert data["items"][0]["well_type"] == water_well_thing.well_type + assert data["items"][0]["well_depth"] == water_well_thing.well_depth + assert data["items"][0]["hole_depth"] == water_well_thing.hole_depth + assert ( + data["items"][0]["well_construction_notes"] + == water_well_thing.well_construction_notes ) + assert data["items"][0]["spring_type"] is None + + assert data["items"][1]["id"] == spring_thing.id + assert data["items"][1][ + "created_at" + ] == spring_thing.created_at.isoformat().replace("+00:00", "Z") + assert data["items"][1]["name"] == spring_thing.name + assert data["items"][1]["thing_type"] == spring_thing.thing_type + assert data["items"][1]["release_status"] == spring_thing.release_status + assert data["items"][1]["spring_type"] == spring_thing.spring_type + assert data["items"][1]["well_type"] is None + assert data["items"][1]["well_depth"] is None + assert data["items"][1]["hole_depth"] is None + assert data["items"][1]["well_construction_notes"] is None + + +def test_get_thing_by_id(water_well_thing): + response = client.get(f"/thing/{water_well_thing.id}") assert response.status_code == 200 data = response.json() - assert data["id"] == 1 - assert data["relation"] == "same_as" - assert data["alternate_id"] == "USGS-43211234" - assert data["alternate_organization"] == "USGS" + + assert data["id"] == water_well_thing.id + assert data["created_at"] == water_well_thing.created_at.isoformat().replace( + "+00:00", "Z" + ) + assert data["name"] == water_well_thing.name + assert data["thing_type"] == water_well_thing.thing_type + assert data["release_status"] == water_well_thing.release_status + assert data["well_type"] == water_well_thing.well_type + assert data["well_depth"] == water_well_thing.well_depth + assert data["hole_depth"] == water_well_thing.hole_depth + assert data["well_construction_notes"] == water_well_thing.well_construction_notes + assert data["spring_type"] is None + + +def test_get_thing_by_id_404_not_found(water_well_thing): + bad_id = 99999 + response = client.get(f"/thing/{bad_id}") + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"Thing with ID {bad_id} not found." + + +# # weaver tests +# def test_weaver_get_wells_geojson(): +# response = client.get("/geospatial", params={"type": "well"}) +# assert response.status_code == 200 +# data = response.json() +# assert "type" in data +# assert data["type"] == "FeatureCollection" +# assert len(data["features"]) > 0 +# assert "id" in data["features"][0]["properties"] + + +# def test_weaver_get_all_collabnet_wells(): +# response = client.get( +# "/geospatial", params={"type": "well", "group": "collabnet"} +# ) # TODO: QUESTION: use type filter and a group filter instead of /collabnet endpoint? +# assert response.status_code == 200 +# data = response.json() + +# assert "features" in data +# assert len(data["features"]) > 0 +# for feature in data["features"]: +# assert "geometry" in feature +# assert isinstance(feature["geometry"], dict) +# assert "properties" in feature +# assert isinstance(feature["properties"], dict) +# assert "coordinates" in feature["geometry"] +# assert "id" in feature or "name" in feature["properties"] +# assert "group" in feature["properties"] -def test_patch_thing(): - response = client.patch( - "/thing/1", - json={ - "name": "Updated Test Thing", - }, +# def test_weaver_thing_contact_info_by_id(): +# response = client.get("/contact?thing_id=1") # or something like this +# assert response.status_code == 200 +# data = response.json() +# assert isinstance(data, dict) +# assert "items" in data +# assert len(data["items"]) > 0 +# item = data["items"][0] +# assert "id" in item +# assert "name" in item +# assert "addresses" in item +# assert "emails" in item +# assert "phones" in item + +# assert isinstance(item["addresses"], list) +# assert isinstance(item["emails"], list) +# assert isinstance(item["phones"], list) + + +# PATCH tests ================================================================== + + +def test_patch_water_well(water_well_thing): + payload = { + "name": "patched water well", + "release_status": "provisional", + "well_type": "Injection", + "well_depth": 20, + "hole_depth": 40, + "well_construction_notes": "patched well construction notes", + } + response = client.patch(f"/thing/water-well/{water_well_thing.id}", json=payload) + assert response.status_code == 200 + data = response.json() + assert data["name"] == payload["name"] + assert data["release_status"] == payload["release_status"] + assert data["well_type"] == payload["well_type"] + assert data["well_depth"] == payload["well_depth"] + assert data["hole_depth"] == payload["hole_depth"] + assert data["well_construction_notes"] == payload["well_construction_notes"] + + cleanup_patch_test(Thing, payload, water_well_thing) + + +def test_patch_water_well_404_not_found(): + bad_id = 99999 + payload = { + "name": "patched water well", + "release_status": "provisional", + "well_type": "Injection", + "well_depth": 20, + "hole_depth": 40, + "well_construction_notes": "patched well construction notes", + } + response = client.patch(f"/thing/water-well/{bad_id}", json=payload) + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"Thing with ID {bad_id} not found." + + +def test_patch_water_well_404_wrong_type(spring_thing): + payload = { + "name": "patched water well", + "release_status": "provisional", + "well_type": "Injection", + "well_depth": 20, + "hole_depth": 40, + "well_construction_notes": "patched well construction notes", + } + response = client.patch(f"/thing/water-well/{spring_thing.id}", json=payload) + assert response.status_code == 404 + data = response.json() + assert ( + data["detail"][0]["msg"] + == f"Thing with ID {spring_thing.id} is not a water well Thing. It is a spring Thing." ) + assert data["detail"][0]["loc"] == ["path", "thing_id"] + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"thing_id": spring_thing.id} + + +def test_patch_spring(spring_thing): + payload = { + "name": "patched spring", + "release_status": "private", + "spring_type": "Mineral", + } + response = client.patch(f"/thing/spring/{spring_thing.id}", json=payload) assert response.status_code == 200 data = response.json() - assert data["id"] == 1 - assert data["name"] == "Updated Test Thing" + assert data["name"] == payload["name"] + assert data["release_status"] == payload["release_status"] + assert data["spring_type"] == payload["spring_type"] + + cleanup_patch_test(Thing, payload, spring_thing) + + +def test_patch_spring_404_not_found(spring_thing): + bad_id = 99999 + payload = { + "name": "patched spring", + "release_status": "private", + "spring_type": "Mineral", + } + response = client.patch(f"/thing/spring/{bad_id}", json=payload) + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"Thing with ID {bad_id} not found." -def test_patch_well(): - response = client.patch( - "/thing/1", - json={ - "well_depth": 150.0, - }, +def test_patch_spring_404_wrong_type(water_well_thing): + payload = { + "name": "patched spring", + "release_status": "private", + "spring_type": "Mineral", + } + response = client.patch(f"/thing/spring/{water_well_thing.id}", json=payload) + assert response.status_code == 404 + data = response.json() + assert ( + data["detail"][0]["msg"] + == f"Thing with ID {water_well_thing.id} is not a spring Thing. It is a water well Thing." ) + assert data["detail"][0]["loc"] == ["path", "thing_id"] + assert data["detail"][0]["type"] == "value_error" + assert data["detail"][0]["input"] == {"thing_id": water_well_thing.id} + + +def test_patch_thing_id_link(thing_id_link): + payload = { + "relation": "related_to", + "alternate_id": "9999-8888", + "alternate_organization": "TWDB", + } + response = client.patch(f"/thing/id-link/{thing_id_link.id}", json=payload) assert response.status_code == 200 data = response.json() - assert data["id"] == 1 - assert data["well_depth"] == 150.0 + assert data["relation"] == payload["relation"] + assert data["alternate_id"] == payload["alternate_id"] + assert data["alternate_organization"] == payload["alternate_organization"] + + cleanup_patch_test(ThingIdLink, payload, thing_id_link) + + +def test_patch_thing_id_link_404_not_found(): + bad_id = 9999 + payload = { + "relation": "related_to", + "alternate_id": "9999-8888", + "alternate_organization": "EPA", + } + response = client.patch(f"/thing/id-link/{bad_id}", json=payload) + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"ThingIdLink with ID {bad_id} not found." -def test_patch_thing_location(): - response = client.patch( - "/thing/4/location", - json={ - "point": "POINT(-106.61 35.08)", - }, - ) +def test_patch_well_screen(well_screen): + payload = { + "screen_depth_bottom": 2, + "screen_depth_top": 1, + "screen_description": "patched screen description", + "screen_type": "Steel", + } + response = client.patch(f"/thing/well-screen/{well_screen.id}", json=payload) assert response.status_code == 200 data = response.json() - assert data["point"] == "POINT (-106.61 35.08)" + assert data["screen_depth_bottom"] == payload["screen_depth_bottom"] + assert data["screen_depth_top"] == payload["screen_depth_top"] + assert data["screen_description"] == payload["screen_description"] + assert data["screen_type"] == data["screen_type"] + + cleanup_patch_test(WellScreen, payload, well_screen) + + +def test_patch_well_screen_404_not_found(): + bad_id = 9999 + payload = { + "screen_depth_bottom": 2, + "screen_depth_top": 1, + "screen_desciption": "patched screen description", + "screen_type": "Steel", + } + response = client.patch(f"/thing/well-screen/{bad_id}", json=payload) + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"WellScreen with ID {bad_id} not found." + + +# DELETE tests ================================================================= + + +def test_delete_thing(second_spring_thing): + response = client.delete(f"/thing/{second_spring_thing.id}") + assert response.status_code == 204 + + # Verify the thing is deleted + response = client.get(f"/thing/spring/{second_spring_thing.id}") + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"Thing with ID {second_spring_thing.id} not found." + + +def test_delete_thing_404_not_found(): + bad_id = 9999 + response = client.delete(f"/thing/{bad_id}") + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"Thing with ID {bad_id} not found." + +def test_delete_well_screen(second_well_screen): + response = client.delete(f"/thing/well-screen/{second_well_screen.id}") + assert response.status_code == 204 + + # Verify the well screen is deleted + response = client.get(f"/thing/well-screen/{second_well_screen.id}") + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"WellScreen with ID {second_well_screen.id} not found." + + +def test_delete_well_screen_404_not_found(): + bad_id = 9999 + response = client.delete(f"/thing/well-screen/{bad_id}") + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"WellScreen with ID {bad_id} not found." -# ============= EOF ============================================= + +def test_delete_thing_id_link(second_thing_id_link): + response = client.delete(f"/thing/id-link/{second_thing_id_link.id}") + assert response.status_code == 204 + + # Verify the thing ID link is deleted + response = client.get(f"/thing/id-link/{second_thing_id_link.id}") + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"ThingIdLink with ID {second_thing_id_link.id} not found." + + +def test_delete_thing_id_link_404_not_found(second_thing_id_link): + bad_id = 9999 + response = client.delete(f"/thing/id-link/{bad_id}") + assert response.status_code == 404 + data = response.json() + assert data["detail"] == f"ThingIdLink with ID {bad_id} not found."