From e21a736e9068a05ff3c5f9ba9b48a479d7c33e81 Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Wed, 8 Jan 2025 17:00:35 -0500 Subject: [PATCH 01/17] Add schema and route for create API --- api/src/api/users/user_routes.py | 45 ++++++++- api/src/api/users/user_schemas.py | 18 ++++ .../api/users/test_user_save_search_post.py | 97 +++++++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 api/tests/src/api/users/test_user_save_search_post.py diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index fc8c4cb9f..75a8bb54c 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -15,13 +15,19 @@ UserSavedOpportunitiesResponseSchema, UserSaveOpportunityRequestSchema, UserSaveOpportunityResponseSchema, + UserSaveSearchRequestSchema, + UserSaveSearchResponseSchema, UserTokenLogoutResponseSchema, UserTokenRefreshResponseSchema, ) from src.auth.api_jwt_auth import api_jwt_auth, refresh_token_expiration from src.auth.auth_utils import with_login_redirect_error_handler from src.auth.login_gov_jwt_auth import get_final_redirect_uri, get_login_gov_redirect_uri -from src.db.models.user_models import UserSavedOpportunity, UserTokenSession +from src.db.models.user_models import ( + UserSavedOpportunity, + UserTokenSession, + UserSavedSearch, +) from src.services.users.delete_saved_opportunity import delete_saved_opportunity from src.services.users.get_saved_opportunities import get_saved_opportunities from src.services.users.get_user import get_user @@ -231,3 +237,40 @@ def user_get_saved_opportunities(db_session: db.Session, user_id: UUID) -> respo saved_opportunities = get_saved_opportunities(db_session, user_id) return response.ApiResponse(message="Success", data=saved_opportunities) + + +@user_blueprint.post("//saved-searches") +@user_blueprint.input(UserSaveSearchRequestSchema, location="json") +@user_blueprint.output(UserSaveSearchResponseSchema) +@user_blueprint.doc(responses=[200, 401]) +@user_blueprint.auth_required(api_jwt_auth) +@flask_db.with_db_session() +def user_save_search( + db_session: db.Session, user_id: UUID, json_data: dict +) -> response.ApiResponse: + logger.info("POST /v1/users/:user_id/saved-searches") + + user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore + + # Verify the authenticated user matches the requested user_id + print(user_token_session.user_id, user_id) + if user_token_session.user_id != user_id: + raise_flask_error(401, "Unauthorized user") + + # Create the saved search record + saved_search = UserSavedSearch( + user_id=user_id, name=json_data["name"], search_query=json_data["search_query"] + ) + + with db_session.begin(): + db_session.add(saved_search) + + logger.info( + "Saved search for user", + extra={ + "user.id": str(user_id), + "saved_search.id": str(saved_search.saved_search_id), + }, + ) + + return response.ApiResponse(message="Success") diff --git a/api/src/api/users/user_schemas.py b/api/src/api/users/user_schemas.py index 651fee7df..56cb10191 100644 --- a/api/src/api/users/user_schemas.py +++ b/api/src/api/users/user_schemas.py @@ -87,3 +87,21 @@ class UserSavedOpportunitiesResponseSchema(AbstractResponseSchema): fields.Nested(SavedOpportunityResponseV1Schema), metadata={"description": "List of saved opportunities"}, ) + + +class UserSaveSearchRequestSchema(Schema): + name = fields.String( + required=True, + metadata={"description": "Name of the saved search", "example": "Tech jobs in California"}, + ) + search_query = fields.Dict( + required=True, + metadata={ + "description": "The search query parameters to save", + "example": {"keywords": "technology", "location": "CA"}, + }, + ) + + +class UserSaveSearchResponseSchema(AbstractResponseSchema): + data = fields.MixinField(metadata={"example": None}) diff --git a/api/tests/src/api/users/test_user_save_search_post.py b/api/tests/src/api/users/test_user_save_search_post.py new file mode 100644 index 000000000..0e8511cb0 --- /dev/null +++ b/api/tests/src/api/users/test_user_save_search_post.py @@ -0,0 +1,97 @@ +import uuid + +import pytest + +from src.auth.api_jwt_auth import create_jwt_for_user +from src.db.models.user_models import UserSavedSearch +from tests.src.db.models.factories import UserFactory + + +@pytest.fixture +def user(enable_factory_create, db_session): + user = UserFactory.create() + db_session.commit() + return user + + +@pytest.fixture +def user_auth_token(user, db_session): + token, _ = create_jwt_for_user(user, db_session) + return token + + +@pytest.fixture(autouse=True) +def clear_saved_searches(db_session): + db_session.query(UserSavedSearch).delete() + db_session.commit() + yield + + +def test_user_save_search_post_unauthorized_user(client, db_session, user, user_auth_token): + # Try to save a search for a different user ID + different_user = UserFactory.create() + + response = client.post( + f"/v1/users/{different_user.user_id}/saved-searches", + headers={"X-SGG-Token": user_auth_token}, + json={"name": "Test Search", "search_query": {"keywords": "python", "location": "remote"}}, + ) + + assert response.status_code == 401 + assert response.json["message"] == "Unauthorized user" + + # Verify no search was saved + saved_searches = db_session.query(UserSavedSearch).all() + assert len(saved_searches) == 0 + + +def test_user_save_search_post_no_auth(client, db_session, user): + # Try to save a search without authentication + response = client.post( + f"/v1/users/{user.user_id}/saved-searches", + json={"name": "Test Search", "search_query": {"keywords": "python", "location": "remote"}}, + ) + + assert response.status_code == 401 + assert response.json["message"] == "Unable to process token" + + # Verify no search was saved + saved_searches = db_session.query(UserSavedSearch).all() + assert len(saved_searches) == 0 + + +def test_user_save_search_post_invalid_request(client, user, user_auth_token, db_session): + # Make request with missing required fields + response = client.post( + f"/v1/users/{user.user_id}/saved-searches", + headers={"X-SGG-Token": user_auth_token}, + json={}, + ) + + assert response.status_code == 422 # Validation error + + # Verify no search was saved + saved_searches = db_session.query(UserSavedSearch).all() + assert len(saved_searches) == 0 + + +def test_user_save_search_post(client, user, user_auth_token, enable_factory_create, db_session): + # Test data + search_name = "Test Search" + search_query = {"keywords": "python", "location": "remote"} + + # Make the request to save a search + response = client.post( + f"/v1/users/{user.user_id}/saved-searches", + headers={"X-SGG-Token": user_auth_token}, + json={"name": search_name, "search_query": search_query}, + ) + + assert response.status_code == 200 + assert response.json["message"] == "Success" + + # Verify the search was saved in the database + saved_search = db_session.query(UserSavedSearch).one() + assert saved_search.user_id == user.user_id + assert saved_search.name == search_name + assert saved_search.search_query == search_query From 71cde063d385af3ed5cbd5126cb0b3ad1ef49a98 Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Thu, 9 Jan 2025 11:15:43 -0500 Subject: [PATCH 02/17] Update examples --- api/src/api/users/user_schemas.py | 4 ++-- api/tests/src/api/users/test_user_save_search_post.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/api/users/user_schemas.py b/api/src/api/users/user_schemas.py index 56cb10191..3d3785573 100644 --- a/api/src/api/users/user_schemas.py +++ b/api/src/api/users/user_schemas.py @@ -92,13 +92,13 @@ class UserSavedOpportunitiesResponseSchema(AbstractResponseSchema): class UserSaveSearchRequestSchema(Schema): name = fields.String( required=True, - metadata={"description": "Name of the saved search", "example": "Tech jobs in California"}, + metadata={"description": "Name of the saved search", "example": "Example search"}, ) search_query = fields.Dict( required=True, metadata={ "description": "The search query parameters to save", - "example": {"keywords": "technology", "location": "CA"}, + "example": {"keywords": "search", "location": "Foo, Bar"}, }, ) diff --git a/api/tests/src/api/users/test_user_save_search_post.py b/api/tests/src/api/users/test_user_save_search_post.py index 0e8511cb0..f1a628f15 100644 --- a/api/tests/src/api/users/test_user_save_search_post.py +++ b/api/tests/src/api/users/test_user_save_search_post.py @@ -34,7 +34,7 @@ def test_user_save_search_post_unauthorized_user(client, db_session, user, user_ response = client.post( f"/v1/users/{different_user.user_id}/saved-searches", headers={"X-SGG-Token": user_auth_token}, - json={"name": "Test Search", "search_query": {"keywords": "python", "location": "remote"}}, + json={"name": "Test Search", "search_query": {"keywords": "python"}}, ) assert response.status_code == 401 @@ -49,7 +49,7 @@ def test_user_save_search_post_no_auth(client, db_session, user): # Try to save a search without authentication response = client.post( f"/v1/users/{user.user_id}/saved-searches", - json={"name": "Test Search", "search_query": {"keywords": "python", "location": "remote"}}, + json={"name": "Test Search", "search_query": {"keywords": "python"}}, ) assert response.status_code == 401 @@ -78,7 +78,7 @@ def test_user_save_search_post_invalid_request(client, user, user_auth_token, db def test_user_save_search_post(client, user, user_auth_token, enable_factory_create, db_session): # Test data search_name = "Test Search" - search_query = {"keywords": "python", "location": "remote"} + search_query = {"keywords": "python"} # Make the request to save a search response = client.post( From 27613ef5d17a828017782a1ce884cec0dfde7a98 Mon Sep 17 00:00:00 2001 From: nava-platform-bot Date: Thu, 9 Jan 2025 17:13:33 +0000 Subject: [PATCH 03/17] Create ERD diagram and Update OpenAPI spec --- api/openapi.generated.yml | 72 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/api/openapi.generated.yml b/api/openapi.generated.yml index 764b3ce59..679f111b1 100644 --- a/api/openapi.generated.yml +++ b/api/openapi.generated.yml @@ -431,6 +431,49 @@ paths: ' security: - ApiKeyAuth: [] + /v1/users/{user_id}/saved-searches: + post: + parameters: + - in: path + name: user_id + schema: + type: string + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserSaveSearchResponse' + description: Successful response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Validation error + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Authentication error + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Not found + tags: + - User v1 + summary: User Save Search + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserSaveSearchRequest' + security: + - ApiJwtAuth: [] /v1/users/{user_id}/saved-opportunities: get: parameters: @@ -1973,6 +2016,35 @@ components: type: integer description: The HTTP status code example: 200 + UserSaveSearchRequest: + type: object + properties: + name: + type: string + description: Name of the saved search + example: Example search + search_query: + type: object + description: The search query parameters to save + example: + keywords: search + location: Foo, Bar + required: + - name + - search_query + UserSaveSearchResponse: + type: object + properties: + message: + type: string + description: The message to return + example: Success + data: + example: null + status_code: + type: integer + description: The HTTP status code + example: 200 SavedOpportunitySummaryV1: type: object properties: From 499dc5d0e4b2dcc689773080c980921d3e38a9be Mon Sep 17 00:00:00 2001 From: Mike H Date: Thu, 9 Jan 2025 12:17:13 -0500 Subject: [PATCH 04/17] Update api/src/api/users/user_routes.py Co-authored-by: Michael Chouinard <46358556+chouinar@users.noreply.github.com> --- api/src/api/users/user_routes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index 75a8bb54c..45d243683 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -253,7 +253,6 @@ def user_save_search( user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore # Verify the authenticated user matches the requested user_id - print(user_token_session.user_id, user_id) if user_token_session.user_id != user_id: raise_flask_error(401, "Unauthorized user") From 54f007f097285d4b21ae7799f8f90400ce709ae8 Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Thu, 9 Jan 2025 12:33:37 -0500 Subject: [PATCH 05/17] Update test for actual search body --- api/tests/src/api/users/test_user_save_search_post.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/api/tests/src/api/users/test_user_save_search_post.py b/api/tests/src/api/users/test_user_save_search_post.py index f1a628f15..f8e075595 100644 --- a/api/tests/src/api/users/test_user_save_search_post.py +++ b/api/tests/src/api/users/test_user_save_search_post.py @@ -2,6 +2,9 @@ import pytest +from tests.src.api.opportunities_v1.conftest import get_search_request + +from src.constants.lookup_constants import FundingInstrument from src.auth.api_jwt_auth import create_jwt_for_user from src.db.models.user_models import UserSavedSearch from tests.src.db.models.factories import UserFactory @@ -78,7 +81,11 @@ def test_user_save_search_post_invalid_request(client, user, user_auth_token, db def test_user_save_search_post(client, user, user_auth_token, enable_factory_create, db_session): # Test data search_name = "Test Search" - search_query = {"keywords": "python"} + search_query = get_search_request( + funding_instrument_one_of=[FundingInstrument.GRANT], + agency_one_of=["LOC"], + post_date={"gte": "2024-01-01"}, + ) # Make the request to save a search response = client.post( From 23e428ec9993a10de4509715e0ae13216eb2a952 Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Thu, 9 Jan 2025 12:37:34 -0500 Subject: [PATCH 06/17] Format --- api/src/api/users/user_routes.py | 6 +----- api/src/services/users/create_saved_search.py | 17 +++++++++++++++++ .../src/api/users/test_user_save_search_post.py | 7 ++----- 3 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 api/src/services/users/create_saved_search.py diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index 45d243683..c5f948edf 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -23,11 +23,7 @@ from src.auth.api_jwt_auth import api_jwt_auth, refresh_token_expiration from src.auth.auth_utils import with_login_redirect_error_handler from src.auth.login_gov_jwt_auth import get_final_redirect_uri, get_login_gov_redirect_uri -from src.db.models.user_models import ( - UserSavedOpportunity, - UserTokenSession, - UserSavedSearch, -) +from src.db.models.user_models import UserSavedOpportunity, UserSavedSearch, UserTokenSession from src.services.users.delete_saved_opportunity import delete_saved_opportunity from src.services.users.get_saved_opportunities import get_saved_opportunities from src.services.users.get_user import get_user diff --git a/api/src/services/users/create_saved_search.py b/api/src/services/users/create_saved_search.py new file mode 100644 index 000000000..0e0eed987 --- /dev/null +++ b/api/src/services/users/create_saved_search.py @@ -0,0 +1,17 @@ +import logging +from uuid import UUID + +from sqlalchemy import db + +from src.db.models.user_models import UserSavedSearch + +logger = logging.getLogger(__name__) + + +def create_saved_search(db_session: db.Session, user_id: UUID, json_data: dict) -> None: + saved_search = UserSavedSearch( + user_id=user_id, name=json_data["name"], search_query=json_data["search_query"] + ) + + with db_session.begin(): + db_session.add(saved_search) diff --git a/api/tests/src/api/users/test_user_save_search_post.py b/api/tests/src/api/users/test_user_save_search_post.py index f8e075595..7d7bb21bc 100644 --- a/api/tests/src/api/users/test_user_save_search_post.py +++ b/api/tests/src/api/users/test_user_save_search_post.py @@ -1,12 +1,9 @@ -import uuid - import pytest -from tests.src.api.opportunities_v1.conftest import get_search_request - -from src.constants.lookup_constants import FundingInstrument from src.auth.api_jwt_auth import create_jwt_for_user +from src.constants.lookup_constants import FundingInstrument from src.db.models.user_models import UserSavedSearch +from tests.src.api.opportunities_v1.conftest import get_search_request from tests.src.db.models.factories import UserFactory From c97fc307edb9cdaeb0ff59bee98a4432ff65d900 Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Thu, 9 Jan 2025 12:38:43 -0500 Subject: [PATCH 07/17] Return saved search --- api/src/api/users/user_routes.py | 6 ++---- api/src/services/users/create_saved_search.py | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index c5f948edf..73684def9 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -24,6 +24,7 @@ from src.auth.auth_utils import with_login_redirect_error_handler from src.auth.login_gov_jwt_auth import get_final_redirect_uri, get_login_gov_redirect_uri from src.db.models.user_models import UserSavedOpportunity, UserSavedSearch, UserTokenSession +from src.services.users.create_saved_search import create_saved_search from src.services.users.delete_saved_opportunity import delete_saved_opportunity from src.services.users.get_saved_opportunities import get_saved_opportunities from src.services.users.get_user import get_user @@ -252,10 +253,7 @@ def user_save_search( if user_token_session.user_id != user_id: raise_flask_error(401, "Unauthorized user") - # Create the saved search record - saved_search = UserSavedSearch( - user_id=user_id, name=json_data["name"], search_query=json_data["search_query"] - ) + saved_search = create_saved_search(db_session, user_id, json_data) with db_session.begin(): db_session.add(saved_search) diff --git a/api/src/services/users/create_saved_search.py b/api/src/services/users/create_saved_search.py index 0e0eed987..c1d61bbef 100644 --- a/api/src/services/users/create_saved_search.py +++ b/api/src/services/users/create_saved_search.py @@ -15,3 +15,5 @@ def create_saved_search(db_session: db.Session, user_id: UUID, json_data: dict) with db_session.begin(): db_session.add(saved_search) + + return saved_search From 50c900a5762f59870aa6c0b90e07f3bae64c35a0 Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Thu, 9 Jan 2025 12:39:02 -0500 Subject: [PATCH 08/17] Lint --- api/src/api/users/user_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index 73684def9..daff1ab98 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -23,7 +23,7 @@ from src.auth.api_jwt_auth import api_jwt_auth, refresh_token_expiration from src.auth.auth_utils import with_login_redirect_error_handler from src.auth.login_gov_jwt_auth import get_final_redirect_uri, get_login_gov_redirect_uri -from src.db.models.user_models import UserSavedOpportunity, UserSavedSearch, UserTokenSession +from src.db.models.user_models import UserSavedOpportunity, UserTokenSession from src.services.users.create_saved_search import create_saved_search from src.services.users.delete_saved_opportunity import delete_saved_opportunity from src.services.users.get_saved_opportunities import get_saved_opportunities From 14c9e4693272e7fe68b1b6f9070278951e97084d Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Thu, 9 Jan 2025 12:39:29 -0500 Subject: [PATCH 09/17] Update return type --- api/src/services/users/create_saved_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/services/users/create_saved_search.py b/api/src/services/users/create_saved_search.py index c1d61bbef..8aead37cf 100644 --- a/api/src/services/users/create_saved_search.py +++ b/api/src/services/users/create_saved_search.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -def create_saved_search(db_session: db.Session, user_id: UUID, json_data: dict) -> None: +def create_saved_search(db_session: db.Session, user_id: UUID, json_data: dict) -> UserSavedSearch: saved_search = UserSavedSearch( user_id=user_id, name=json_data["name"], search_query=json_data["search_query"] ) From 2887afc2725da1cd977c65ba8be7186e57e43f4a Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Thu, 9 Jan 2025 12:40:11 -0500 Subject: [PATCH 10/17] Update import --- api/src/services/users/create_saved_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/services/users/create_saved_search.py b/api/src/services/users/create_saved_search.py index 8aead37cf..cb613e818 100644 --- a/api/src/services/users/create_saved_search.py +++ b/api/src/services/users/create_saved_search.py @@ -1,7 +1,7 @@ import logging from uuid import UUID -from sqlalchemy import db +from src.adapters import db from src.db.models.user_models import UserSavedSearch From 262053013498406baa0c3c6d5094b9a5ac10a017 Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Thu, 9 Jan 2025 12:43:47 -0500 Subject: [PATCH 11/17] Format --- api/src/services/users/create_saved_search.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/services/users/create_saved_search.py b/api/src/services/users/create_saved_search.py index cb613e818..177a9de98 100644 --- a/api/src/services/users/create_saved_search.py +++ b/api/src/services/users/create_saved_search.py @@ -2,7 +2,6 @@ from uuid import UUID from src.adapters import db - from src.db.models.user_models import UserSavedSearch logger = logging.getLogger(__name__) From f56a1d53fd5e95a42132a6ccea97d632363e9655 Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Thu, 9 Jan 2025 13:17:15 -0500 Subject: [PATCH 12/17] Format / move user fixtures to conftest and save token session correctly --- api/tests/conftest.py | 13 +++++++++++++ .../api/users/test_user_save_opportunity_post.py | 16 +--------------- .../src/api/users/test_user_save_search_post.py | 16 +--------------- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/api/tests/conftest.py b/api/tests/conftest.py index efc09ebe4..292db8df8 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -19,6 +19,7 @@ import tests.src.db.models.factories as factories from src.adapters import search from src.adapters.oauth.login_gov.mock_login_gov_oauth_client import MockLoginGovOauthClient +from src.auth.api_jwt_auth import create_jwt_for_user from src.constants.schema import Schemas from src.db import models from src.db.models.foreign import metadata as foreign_metadata @@ -32,6 +33,18 @@ logger = logging.getLogger(__name__) +@pytest.fixture +def user(enable_factory_create, db_session): + return factories.UserFactory.create() + + +@pytest.fixture +def user_auth_token(user, db_session): + token, _ = create_jwt_for_user(user, db_session) + db_session.commit() + return token + + @pytest.fixture(scope="session", autouse=True) def env_vars(): """ diff --git a/api/tests/src/api/users/test_user_save_opportunity_post.py b/api/tests/src/api/users/test_user_save_opportunity_post.py index f50082b42..77c3f4dc7 100644 --- a/api/tests/src/api/users/test_user_save_opportunity_post.py +++ b/api/tests/src/api/users/test_user_save_opportunity_post.py @@ -2,22 +2,8 @@ import pytest -from src.auth.api_jwt_auth import create_jwt_for_user from src.db.models.user_models import UserSavedOpportunity -from tests.src.db.models.factories import OpportunityFactory, UserFactory - - -@pytest.fixture -def user(enable_factory_create, db_session): - user = UserFactory.create() - db_session.commit() - return user - - -@pytest.fixture -def user_auth_token(user, db_session): - token, _ = create_jwt_for_user(user, db_session) - return token +from tests.src.db.models.factories import OpportunityFactory @pytest.fixture(autouse=True) diff --git a/api/tests/src/api/users/test_user_save_search_post.py b/api/tests/src/api/users/test_user_save_search_post.py index 7d7bb21bc..248ec459f 100644 --- a/api/tests/src/api/users/test_user_save_search_post.py +++ b/api/tests/src/api/users/test_user_save_search_post.py @@ -1,26 +1,12 @@ import pytest -from src.auth.api_jwt_auth import create_jwt_for_user from src.constants.lookup_constants import FundingInstrument from src.db.models.user_models import UserSavedSearch from tests.src.api.opportunities_v1.conftest import get_search_request from tests.src.db.models.factories import UserFactory -@pytest.fixture -def user(enable_factory_create, db_session): - user = UserFactory.create() - db_session.commit() - return user - - -@pytest.fixture -def user_auth_token(user, db_session): - token, _ = create_jwt_for_user(user, db_session) - return token - - -@pytest.fixture(autouse=True) +@pytest.fixture(autouse=True, scope="function") def clear_saved_searches(db_session): db_session.query(UserSavedSearch).delete() db_session.commit() From 0fdf13c70ddc096350c7cb5b0f4052423f70ca46 Mon Sep 17 00:00:00 2001 From: Mike H Date: Thu, 9 Jan 2025 15:56:18 -0500 Subject: [PATCH 13/17] Update api/src/api/users/user_routes.py Co-authored-by: Michael Chouinard <46358556+chouinar@users.noreply.github.com> --- api/src/api/users/user_routes.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index daff1ab98..de8dd180f 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -253,10 +253,8 @@ def user_save_search( if user_token_session.user_id != user_id: raise_flask_error(401, "Unauthorized user") - saved_search = create_saved_search(db_session, user_id, json_data) - with db_session.begin(): - db_session.add(saved_search) + create_saved_search(db_session, user_id, json_data) logger.info( "Saved search for user", From e144b685d8b409edf9ada51bb8dbb5d8e15b83ef Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Thu, 9 Jan 2025 16:13:06 -0500 Subject: [PATCH 14/17] Update query request format --- api/src/api/users/user_schemas.py | 13 ++++------ .../api/users/test_user_save_search_post.py | 26 ++++++++++++++++--- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/api/src/api/users/user_schemas.py b/api/src/api/users/user_schemas.py index 3d3785573..08ad32eb2 100644 --- a/api/src/api/users/user_schemas.py +++ b/api/src/api/users/user_schemas.py @@ -1,4 +1,7 @@ -from src.api.opportunities_v1.opportunity_schemas import SavedOpportunityResponseV1Schema +from src.api.opportunities_v1.opportunity_schemas import ( + OpportunitySearchRequestV1Schema, + SavedOpportunityResponseV1Schema, +) from src.api.schemas.extension import Schema, fields from src.api.schemas.response_schema import AbstractResponseSchema from src.constants.lookup_constants import ExternalUserType @@ -94,13 +97,7 @@ class UserSaveSearchRequestSchema(Schema): required=True, metadata={"description": "Name of the saved search", "example": "Example search"}, ) - search_query = fields.Dict( - required=True, - metadata={ - "description": "The search query parameters to save", - "example": {"keywords": "search", "location": "Foo, Bar"}, - }, - ) + search_query = search_query = fields.Nested(OpportunitySearchRequestV1Schema) class UserSaveSearchResponseSchema(AbstractResponseSchema): diff --git a/api/tests/src/api/users/test_user_save_search_post.py b/api/tests/src/api/users/test_user_save_search_post.py index 248ec459f..c8e0ba2bd 100644 --- a/api/tests/src/api/users/test_user_save_search_post.py +++ b/api/tests/src/api/users/test_user_save_search_post.py @@ -17,10 +17,15 @@ def test_user_save_search_post_unauthorized_user(client, db_session, user, user_ # Try to save a search for a different user ID different_user = UserFactory.create() + search_query = get_search_request( + funding_instrument_one_of=[FundingInstrument.GRANT], + agency_one_of=["LOC"], + ) + response = client.post( f"/v1/users/{different_user.user_id}/saved-searches", headers={"X-SGG-Token": user_auth_token}, - json={"name": "Test Search", "search_query": {"keywords": "python"}}, + json={"name": "Test Search", "search_query": search_query}, ) assert response.status_code == 401 @@ -32,10 +37,15 @@ def test_user_save_search_post_unauthorized_user(client, db_session, user, user_ def test_user_save_search_post_no_auth(client, db_session, user): + search_query = get_search_request( + funding_instrument_one_of=[FundingInstrument.GRANT], + agency_one_of=["LOC"], + ) + # Try to save a search without authentication response = client.post( f"/v1/users/{user.user_id}/saved-searches", - json={"name": "Test Search", "search_query": {"keywords": "python"}}, + json={"name": "Test Search", "search_query": search_query}, ) assert response.status_code == 401 @@ -67,7 +77,6 @@ def test_user_save_search_post(client, user, user_auth_token, enable_factory_cre search_query = get_search_request( funding_instrument_one_of=[FundingInstrument.GRANT], agency_one_of=["LOC"], - post_date={"gte": "2024-01-01"}, ) # Make the request to save a search @@ -84,4 +93,13 @@ def test_user_save_search_post(client, user, user_auth_token, enable_factory_cre saved_search = db_session.query(UserSavedSearch).one() assert saved_search.user_id == user.user_id assert saved_search.name == search_name - assert saved_search.search_query == search_query + assert saved_search.search_query == { + "format": "json", + "filters": {"agency": {"one_of": ["LOC"]}, "funding_instrument": {"one_of": ["grant"]}}, + "pagination": { + "order_by": "opportunity_id", + "page_size": 25, + "page_offset": 1, + "sort_direction": "ascending", + }, + } From c16acf7bddf799b6c9283782bd2795340a7fb0d2 Mon Sep 17 00:00:00 2001 From: nava-platform-bot Date: Thu, 9 Jan 2025 21:15:48 +0000 Subject: [PATCH 15/17] Create ERD diagram and Update OpenAPI spec --- api/openapi.generated.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/api/openapi.generated.yml b/api/openapi.generated.yml index 679f111b1..f7626b5ee 100644 --- a/api/openapi.generated.yml +++ b/api/openapi.generated.yml @@ -2024,14 +2024,12 @@ components: description: Name of the saved search example: Example search search_query: - type: object - description: The search query parameters to save - example: - keywords: search - location: Foo, Bar + type: + - object + allOf: + - $ref: '#/components/schemas/OpportunitySearchRequestV1' required: - name - - search_query UserSaveSearchResponse: type: object properties: From ea6dca6705fe98b4e0aded1beefcd14755752cda Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Mon, 13 Jan 2025 10:19:07 -0500 Subject: [PATCH 16/17] Remove begin from service --- api/src/services/users/create_saved_search.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/services/users/create_saved_search.py b/api/src/services/users/create_saved_search.py index 177a9de98..7a3b02075 100644 --- a/api/src/services/users/create_saved_search.py +++ b/api/src/services/users/create_saved_search.py @@ -12,7 +12,6 @@ def create_saved_search(db_session: db.Session, user_id: UUID, json_data: dict) user_id=user_id, name=json_data["name"], search_query=json_data["search_query"] ) - with db_session.begin(): - db_session.add(saved_search) + db_session.add(saved_search) return saved_search From 811bdb85cbb6e98f5835cee36fa8923b4b576969 Mon Sep 17 00:00:00 2001 From: Michael Huneke Date: Mon, 13 Jan 2025 10:24:09 -0500 Subject: [PATCH 17/17] Lint --- api/src/api/users/user_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index de8dd180f..b643dbc75 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -254,7 +254,7 @@ def user_save_search( raise_flask_error(401, "Unauthorized user") with db_session.begin(): - create_saved_search(db_session, user_id, json_data) + saved_search = create_saved_search(db_session, user_id, json_data) logger.info( "Saved search for user",