Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue #3293] Create POST /users/:userId/saved-opportunities API schema and stub endpoint #3330

Merged
merged 20 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions api/openapi.generated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,49 @@ paths:
'
security:
- ApiKeyAuth: []
/v1/users/{user_id}/saved-opportunities:
post:
parameters:
- in: path
name: user_id
schema:
type: string
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/UserSaveOpportunityResponse'
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 Opportunity
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UserSaveOpportunityRequest'
security:
- ApiJwtAuth: []
/v1/opportunities/{opportunity_id}/versions:
get:
parameters:
Expand Down Expand Up @@ -1868,6 +1911,26 @@ components:
type: integer
description: The HTTP status code
example: 200
UserSaveOpportunityRequest:
type: object
properties:
opportunity_id:
type: integer
required:
- opportunity_id
UserSaveOpportunityResponse:
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
OpportunityVersionV1:
type: object
properties:
Expand Down
40 changes: 39 additions & 1 deletion api/src/api/users/user_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
from src.api.users.user_blueprint import user_blueprint
from src.api.users.user_schemas import (
UserGetResponseSchema,
UserSaveOpportunityRequestSchema,
UserSaveOpportunityResponseSchema,
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 UserTokenSession
from src.db.models.user_models import UserSavedOpportunity, UserTokenSession
from src.services.users.get_user import get_user
from src.services.users.login_gov_callback_handler import (
handle_login_gov_callback_request,
Expand Down Expand Up @@ -146,3 +148,39 @@ def user_get(db_session: db.Session, user_id: UUID) -> response.ApiResponse:
return response.ApiResponse(message="Success", data=user)

raise_flask_error(401, "Unauthorized user")


@user_blueprint.post("/<uuid:user_id>/saved-opportunities")
@user_blueprint.input(UserSaveOpportunityRequestSchema, location="json")
@user_blueprint.output(UserSaveOpportunityResponseSchema)
@user_blueprint.doc(responses=[200, 401])
@user_blueprint.auth_required(api_jwt_auth)
@flask_db.with_db_session()
def user_save_opportunity(
db_session: db.Session, user_id: UUID, json_data: dict
) -> response.ApiResponse:
logger.info("POST /v1/users/:user_id/saved-opportunities")

user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore

# Verify the authenticated user matches the requested user_id
if user_token_session.user_id != user_id:
raise_flask_error(401, "Unauthorized user")
mikehgrantsgov marked this conversation as resolved.
Show resolved Hide resolved

# Create the saved opportunity record
saved_opportunity = UserSavedOpportunity(
user_id=user_id, opportunity_id=json_data["opportunity_id"]
)

with db_session.begin():
db_session.add(saved_opportunity)

logger.info(
"Saved opportunity for user",
extra={
"user.id": str(user_id),
"opportunity.id": json_data["opportunity_id"],
},
)

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

class UserGetResponseSchema(AbstractResponseSchema):
data = fields.Nested(UserSchema)


class UserSaveOpportunityRequestSchema(Schema):
opportunity_id = fields.Integer(required=True)


class UserSaveOpportunityResponseSchema(AbstractResponseSchema):
data = fields.MixinField(metadata={"example": None})
106 changes: 106 additions & 0 deletions api/tests/src/api/users/test_user_save_opportunity_post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import uuid

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


@pytest.fixture(autouse=True)
def clear_opportunities(db_session):
db_session.query(UserSavedOpportunity).delete()
db_session.commit()
yield


def test_user_save_opportunity_post_unauthorized_user(
client, db_session, user, user_auth_token, enable_factory_create
):
# Create an opportunity
opportunity = OpportunityFactory.create()

# Try to save an opportunity for a different user ID
different_user_id = uuid.uuid4()
response = client.post(
f"/v1/users/{different_user_id}/saved-opportunities",
headers={"X-SGG-Token": user_auth_token},
json={"opportunity_id": opportunity.opportunity_id},
)

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

# Verify no opportunity was saved
saved_opportunities = db_session.query(UserSavedOpportunity).all()
assert len(saved_opportunities) == 0


def test_user_save_opportunity_post_no_auth(client, db_session, user, enable_factory_create):
# Create an opportunity
opportunity = OpportunityFactory.create()

# Try to save an opportunity without authentication
response = client.post(
f"/v1/users/{user.user_id}/saved-opportunities",
json={"opportunity_id": opportunity.opportunity_id},
)

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

# Verify no opportunity was saved
saved_opportunities = db_session.query(UserSavedOpportunity).all()
assert len(saved_opportunities) == 0


def test_user_save_opportunity_post_invalid_request(
client, user, user_auth_token, enable_factory_create, db_session
):
# Make request with missing opportunity_id
response = client.post(
f"/v1/users/{user.user_id}/saved-opportunities",
headers={"X-SGG-Token": user_auth_token},
json={},
)

assert response.status_code == 422 # Validation error

# Verify no opportunity was saved
saved_opportunities = db_session.query(UserSavedOpportunity).all()
assert len(saved_opportunities) == 0


def test_user_save_opportunity_post(
client, user, user_auth_token, enable_factory_create, db_session
):
# Create an opportunity
opportunity = OpportunityFactory.create()

# Make the request to save an opportunity
response = client.post(
f"/v1/users/{user.user_id}/saved-opportunities",
headers={"X-SGG-Token": user_auth_token},
json={"opportunity_id": opportunity.opportunity_id},
)

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

# Verify the opportunity was saved in the database
saved_opportunity = db_session.query(UserSavedOpportunity).one()
assert saved_opportunity.user_id == user.user_id
assert saved_opportunity.opportunity_id == opportunity.opportunity_id