Skip to content

Commit

Permalink
[Issue #3691] Update user saved search (#3744)
Browse files Browse the repository at this point in the history
## Summary
Fixes #{[3691](#3691)}

### Time to review: __10 mins__

## Changes proposed
New route PUT /users/:userID/save-searches/:saved_search_id added
Input and Output schema for route added
Pydantic Model for input update fields
Tests added

---------

Co-authored-by: nava-platform-bot <[email protected]>
  • Loading branch information
babebe and nava-platform-bot authored Feb 3, 2025
1 parent c4ce4a5 commit c46afb3
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 0 deletions.
69 changes: 69 additions & 0 deletions api/openapi.generated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,53 @@ paths:
security:
- ApiKeyAuth: []
/v1/users/{user_id}/saved-searches/{saved_search_id}:
put:
parameters:
- in: path
name: user_id
schema:
type: string
required: true
- in: path
name: saved_search_id
schema:
type: string
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/UserUpdateSavedSearchResponse'
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 Update Saved Search
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UserUpdateSavedSearchRequest'
security:
- ApiJwtAuth: []
delete:
parameters:
- in: path
Expand Down Expand Up @@ -2297,6 +2344,28 @@ components:
type: integer
description: The HTTP status code
example: 200
UserUpdateSavedSearchRequest:
type: object
properties:
name:
type: string
description: Name of the saved search
example: Example search
required:
- name
UserUpdateSavedSearchResponse:
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
UserDeleteSavedSearchResponse:
type: object
properties:
Expand Down
34 changes: 34 additions & 0 deletions api/src/api/users/user_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
UserSaveSearchResponseSchema,
UserTokenLogoutResponseSchema,
UserTokenRefreshResponseSchema,
UserUpdateSavedSearchRequestSchema,
UserUpdateSavedSearchResponseSchema,
)
from src.auth.api_jwt_auth import api_jwt_auth, refresh_token_expiration
from src.auth.auth_utils import with_login_redirect_error_handler
Expand All @@ -36,6 +38,7 @@
handle_login_gov_callback_request,
handle_login_gov_token,
)
from src.services.users.update_saved_searches import update_saved_search

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -318,3 +321,34 @@ def user_get_saved_searches(db_session: db.Session, user_id: UUID) -> response.A
saved_searches = get_saved_searches(db_session, user_id)

return response.ApiResponse(message="Success", data=saved_searches)


@user_blueprint.put("/<uuid:user_id>/saved-searches/<uuid:saved_search_id>")
@user_blueprint.input(UserUpdateSavedSearchRequestSchema, location="json")
@user_blueprint.output(UserUpdateSavedSearchResponseSchema)
@user_blueprint.doc(responses=[200, 401, 404])
@user_blueprint.auth_required(api_jwt_auth)
@flask_db.with_db_session()
def user_update_saved_search(
db_session: db.Session, user_id: UUID, saved_search_id: UUID, json_data: dict
) -> response.ApiResponse:
logger.info("PUT /v1/users/:user_id/saved-searches/:saved_search_id")

user_token_session: UserTokenSession = api_jwt_auth.get_user_token_session()

# Verify the authenticated user matches the requested user_id
if user_token_session.user_id != user_id:
raise_flask_error(401, "Unauthorized user")

with db_session.begin():
updated_saved_search = update_saved_search(db_session, user_id, saved_search_id, json_data)

logger.info(
"Updated saved search for user",
extra={
"user.id": str(user_id),
"saved_search.id": str(updated_saved_search.saved_search_id),
},
)

return response.ApiResponse(message="Success")
11 changes: 11 additions & 0 deletions api/src/api/users/user_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,14 @@ class UserSavedSearchesResponseSchema(AbstractResponseSchema):

class UserDeleteSavedSearchResponseSchema(AbstractResponseSchema):
data = fields.MixinField(metadata={"example": None})


class UserUpdateSavedSearchRequestSchema(Schema):
name = fields.String(
required=True,
metadata={"description": "Name of the saved search", "example": "Example search"},
)


class UserUpdateSavedSearchResponseSchema(AbstractResponseSchema):
data = fields.MixinField(metadata={"example": None})
33 changes: 33 additions & 0 deletions api/src/services/users/update_saved_searches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from uuid import UUID

from pydantic import BaseModel
from sqlalchemy import select

from src.adapters import db
from src.api.route_utils import raise_flask_error
from src.db.models.user_models import UserSavedSearch


class UpdateSavedSearchInput(BaseModel):
name: str


def update_saved_search(
db_session: db.Session, user_id: UUID, saved_search_id: UUID, json_data: dict
) -> UserSavedSearch:
"""Update saved search for a user"""
update_input = UpdateSavedSearchInput.model_validate(json_data)

saved_search = db_session.execute(
select(UserSavedSearch).where(
UserSavedSearch.saved_search_id == saved_search_id, UserSavedSearch.user_id == user_id
)
).scalar_one_or_none()

if not saved_search:
raise_flask_error(404, "Saved search not found")

# Update
saved_search.name = update_input.name

return saved_search
97 changes: 97 additions & 0 deletions api/tests/src/api/users/test_user_update_saved_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import uuid

import pytest

from src.db.models.user_models import UserSavedSearch, UserTokenSession
from tests.src.db.models.factories import UserFactory, UserSavedSearchFactory


@pytest.fixture
def saved_search(enable_factory_create, user, db_session):
search = UserSavedSearchFactory.create(
user=user, name="Save Search", search_query={"keywords": "python"}
)
return search


@pytest.fixture(autouse=True)
def clear_data(db_session):
db_session.query(UserSavedSearch).delete()
db_session.query(UserTokenSession).delete()
yield


def test_user_update_saved_search(client, db_session, user, user_auth_token, saved_search):
updated_name = "Update Search"
response = client.put(
f"/v1/users/{user.user_id}/saved-searches/{saved_search.saved_search_id}",
headers={"X-SGG-Token": user_auth_token},
json={"name": updated_name},
)

db_session.refresh(saved_search)

assert response.status_code == 200
assert response.json["message"] == "Success"

# Verify search was updated
updated_saved_search = db_session.query(UserSavedSearch).first()

assert updated_saved_search.name == updated_name


def test_user_update_saved_search_not_found(
client,
enable_factory_create,
db_session,
user,
user_auth_token,
):
# Try to update a non-existent search
response = client.put(
f"/v1/users/{user.user_id}/saved-searches/{uuid.uuid4()}",
headers={"X-SGG-Token": user_auth_token},
json={"name": "Update Search"},
)

assert response.status_code == 404
assert response.json["message"] == "Saved search not found"


def test_user_update_saved_search_unauthorized(
client, enable_factory_create, db_session, user, user_auth_token, saved_search
):
# Try to update a search with another user
unauthorized_user = UserFactory.create()
response = client.put(
f"/v1/users/{unauthorized_user.user_id}/saved-searches/{saved_search.saved_search_id}",
headers={"X-SGG-Token": user_auth_token},
json={"name": "Update Search"},
)

db_session.refresh(saved_search)

assert response.status_code == 401
assert response.json["message"] == "Unauthorized user"

# Verify search was not updated
saved_searches = db_session.query(UserSavedSearch).first()
assert saved_searches.name == saved_search.name


def test_user_update_saved_search_no_auth(
client, enable_factory_create, db_session, user, user_auth_token, saved_search
):
# Try to update a search without authentication
response = client.put(
f"/v1/users/{user.user_id}/saved-searches/{saved_search.saved_search_id}",
json={"name": "Update Search"},
)
db_session.refresh(saved_search)

assert response.status_code == 401
assert response.json["message"] == "Unable to process token"

# Verify search was not updated
saved_searches = db_session.query(UserSavedSearch).first()
assert saved_searches.name == saved_search.name

0 comments on commit c46afb3

Please sign in to comment.