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

#2172 - Push Celery Migration #2266

Merged
merged 110 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
f3019ce
First pass
k-macmillan Jan 23, 2025
e80ccc2
Merge branch 'main' into 2172-push-retries
k-macmillan Jan 23, 2025
a1ceb86
stuff was being import in init.py, causing circular imports
k-macmillan Jan 24, 2025
04fd501
Updated rest_push tests
k-macmillan Jan 24, 2025
abad0f0
Built tests for vetext responses
k-macmillan Jan 24, 2025
54efa68
Formatted
k-macmillan Jan 24, 2025
9c5a073
Built new tests
k-macmillan Jan 24, 2025
507e5c2
Merge branch 'main' into 2172-push-retries
k-macmillan Jan 24, 2025
d388b6a
Updated tests
k-macmillan Jan 28, 2025
21b8c24
Merge branch 'main' into 2172-push-retries
MackHalliday Jan 28, 2025
d73fe3d
Merge github.com:department-of-veterans-affairs/notification-api into…
MackHalliday Jan 28, 2025
9f8dc2e
#2171 - Update to resolve issues with test tests/app/v2/notifications…
MackHalliday Jan 29, 2025
8c6c9d6
Merge branch '2172-push-retries' of github.com:department-of-veterans…
MackHalliday Jan 29, 2025
1b9e1ee
#2172 - Add new internal
MackHalliday Jan 30, 2025
724dc33
#2172 - Update message for NonRetryablesException, missing field dict
MackHalliday Jan 30, 2025
70bddf5
#2172 - Fixing test in test_client for updates to vatext_client
MackHalliday Jan 30, 2025
badaaae
#2172 - Update mock for new celery task instead of vaclient
MackHalliday Jan 30, 2025
d21629e
Merge branch 'main' into 2172-push-retries
MackHalliday Jan 30, 2025
f64dfd7
#2172 - Test new internal endpoints
MackHalliday Jan 30, 2025
8b38fa2
#2172 - Clean up new internal endpoint, remove generic and correct sleep
MackHalliday Jan 30, 2025
cfec08d
#2172 - Removing assert statements from teststhat are no longer using…
MackHalliday Jan 30, 2025
a4cb75f
#2172 - Clean up testing for push_notifications
MackHalliday Jan 30, 2025
e17694e
#2172 - Remove TODO after talking to Kyle
MackHalliday Jan 30, 2025
6a9ba77
#2172 - Revert back changes on Exception after talking to Kyle
MackHalliday Jan 30, 2025
a22f5cf
#2172 - Clean up to test_push_broadcast
MackHalliday Jan 30, 2025
e05d4ce
#2172 - Fix test in test_client - test still failing'
MackHalliday Jan 30, 2025
b1f2562
#2172 - Correct keys in payload when sending to Vetext
MackHalliday Jan 30, 2025
260d21c
#2172 - Correct test that expected validation and formatting in the s…
MackHalliday Jan 30, 2025
4ac1ff8
#2172 - Fix tests for reformatting payload to vetext client
MackHalliday Jan 31, 2025
199b765
#2172 - Update testing for check auth fo new internal endpoint
MackHalliday Jan 31, 2025
805e040
#2172 - Clean up testing in the test_client, move tests into class
MackHalliday Jan 31, 2025
7b4852d
#2172 - Include missing docstring
MackHalliday Jan 31, 2025
d9a9c33
2172 - PR feedback Dave: Update parametrize. Do not need expected sta…
MackHalliday Feb 3, 2025
2bbba70
2172 - PR feedback Dave: Remove unnecessary general Exception in vali…
MackHalliday Feb 3, 2025
af738a8
2172 - Update logs to help with debugging
MackHalliday Feb 3, 2025
ad7ad30
2172 - Minor fix to logging
MackHalliday Feb 3, 2025
6aed1a4
2172 - Make adjustments to logging
MackHalliday Feb 3, 2025
8076deb
2172 - remove static method from format_for_vetext
MackHalliday Feb 3, 2025
a126db8
2172 - undo making format_for_vetext
MackHalliday Feb 3, 2025
a89ac4b
2172 - use apply async instead of delay
MackHalliday Feb 3, 2025
d2f2d22
2172 - Update async_apply to correctly pass in payload
MackHalliday Feb 3, 2025
c99f7df
2172 - Add mark where celery task failing
MackHalliday Feb 4, 2025
e2ae94b
#2172 - Fix logging in retry
MackHalliday Feb 4, 2025
2f4efe0
#2172 - Remove TODO
MackHalliday Feb 4, 2025
3690d3d
#2172 - Move format_for_vetext out of celery task and into API.
MackHalliday Feb 4, 2025
41f0297
Merge branch 'main' into 2172-push-retries
MackHalliday Feb 4, 2025
539e8f2
#2172 - Add extra log for request.text
MackHalliday Feb 4, 2025
a7dffc1
#2172 - Try reordering the keys in the vetext body
MackHalliday Feb 4, 2025
99a5d0f
#2172 - add payload to Vetext response log
MackHalliday Feb 4, 2025
556471b
#2172 - Add 2172 to logs
MackHalliday Feb 4, 2025
c5ce747
#2172 - Update format_for_vetext to return a v2pushload
MackHalliday Feb 4, 2025
0f1cf98
#2172 - Update format_for_vetext to return a v2pushload
MackHalliday Feb 4, 2025
c310886
Merge branch 'main' into 2172-push-retries
MackHalliday Feb 4, 2025
d90ede0
#2172 - Add logging for base url"
MackHalliday Feb 5, 2025
5392409
Merge branch '2172-push-retries' of github.com:department-of-veterans…
MackHalliday Feb 5, 2025
bb6a0a1
#2172 - Update payload to be dict in all logs
MackHalliday Feb 5, 2025
5643965
Merge branch 'main' into 2172-push-retries
MackHalliday Feb 5, 2025
27d888d
#2172 - Ruff clean up
MackHalliday Feb 5, 2025
10254ad
Merge branch '2172-push-retries' of github.com:department-of-veterans…
MackHalliday Feb 5, 2025
f561628
#2172 - Update to EMAIL Queue
MackHalliday Feb 5, 2025
f68e79f
#2172 - Revert payload passed into celery back to dict, correct testi…
MackHalliday Feb 5, 2025
70ad51f
#2172 - Add log to confirm payload is dict
MackHalliday Feb 5, 2025
6a9d081
#2172 - Add log to confirm payload is dict
MackHalliday Feb 5, 2025
685620d
#2172 - Add additional log
MackHalliday Feb 5, 2025
a945b05
#2172 - Remove log
MackHalliday Feb 5, 2025
82b1abe
#2172 - Add vetext user name and password
MackHalliday Feb 5, 2025
9c2a3da
#2172 - Update logging to remove number
MackHalliday Feb 6, 2025
5d66364
#2172 - Add vetext variables to perf
MackHalliday Feb 6, 2025
35afc98
#2172 - Add vetext variables to prod
MackHalliday Feb 6, 2025
c86e156
#2172 - Add vetext variables to staging
MackHalliday Feb 6, 2025
72c4eec
#2172 - Remove log for base url
MackHalliday Feb 6, 2025
0edfd08
#2172 - Remove password to trigger 500
MackHalliday Feb 6, 2025
60dfd17
#2172 - Remove password to trigger 500
MackHalliday Feb 6, 2025
7dc9253
#2172 - Add back password
MackHalliday Feb 6, 2025
363fbe6
#2172 - Update to aviod Missing environment sid for type
MackHalliday Feb 6, 2025
4b4e4e5
Revert "#2172 - Add back password"
MackHalliday Feb 6, 2025
0909ebc
#2172 - revert changes
MackHalliday Feb 6, 2025
f79e030
Merge branch 'main' into 2172-push-retries
MackHalliday Feb 6, 2025
88f7556
#2172 - PR feedback Cris - update to logger exception
MackHalliday Feb 6, 2025
6b7fd34
#2172 - PR feedback Cris - Update format to f string
MackHalliday Feb 6, 2025
152f5fd
#2172 - PR feedback Cris - Update exception for unable to reach celer…
MackHalliday Feb 6, 2025
33211c7
#2172 - PR feedback Cris - DEAFULT_MOBILE_APP_TYPE to DEFAULT_MOBILE_…
MackHalliday Feb 6, 2025
885d478
Merge branch '2172-push-retries' of github.com:department-of-veterans…
MackHalliday Feb 6, 2025
c2ad753
#2172 - PR feedback Dave - Try to mock call to vetext
MackHalliday Feb 7, 2025
4c8040e
#2172 - PR feedback Dave - Move log out of try/except
MackHalliday Feb 7, 2025
99df24a
#2172 - PR feedback Dave - Assert call vetext
MackHalliday Feb 7, 2025
eea2606
#2172 - PR feedback Dave - Assert call vetext, sad path
MackHalliday Feb 7, 2025
426bca0
#2172 - PR feedback Cris - refactor to format_for_vetext
MackHalliday Feb 7, 2025
98f2c88
#2172 - PR feedback Cris - Update logging to error
MackHalliday Feb 7, 2025
2913a02
#2172 - PR feedback Kyle - Redact icn from logs
MackHalliday Feb 7, 2025
85a7655
#2172 - Fix appSid value in testing
MackHalliday Feb 7, 2025
0c774ff
#2172 - Fix failing test due to celery mock, use request_history inst…
MackHalliday Feb 10, 2025
6dbd8f3
2172 - Update logs for vetext payload
MackHalliday Feb 10, 2025
6b02a97
2172 - Revert back run_test script
MackHalliday Feb 10, 2025
df80697
2172 - Throw a 500 error test
MackHalliday Feb 10, 2025
b6b1f50
2172 - REVERT Throw a 500 error test
MackHalliday Feb 10, 2025
0c73498
2172 - Throw a 500 error test
MackHalliday Feb 10, 2025
936c75b
Merge branch 'main' into 2172-push-retries
MackHalliday Feb 10, 2025
9f82c7a
2172 - Correct test to throw 500 error
MackHalliday Feb 10, 2025
8bc6eaf
Merge branch '2172-push-retries' of github.com:department-of-veterans…
MackHalliday Feb 10, 2025
285e790
2172 - REVERT Throw a 500 error test
MackHalliday Feb 10, 2025
2c6d960
Merge branch 'main' into 2172-push-retries
MackHalliday Feb 10, 2025
314698e
2172 - TODO #1487
MackHalliday Feb 10, 2025
dacadb5
Merge branch '2172-push-retries' of github.com:department-of-veterans…
MackHalliday Feb 10, 2025
c82550c
2172 - TODO #1487
MackHalliday Feb 10, 2025
5eace56
Update app/internal/rest.py
MackHalliday Feb 10, 2025
558f147
2172 - Try to copy payload before redact
MackHalliday Feb 10, 2025
500b82d
2172 - Try deepcopy instead
MackHalliday Feb 10, 2025
27223bb
2172 - throw 500 error
MackHalliday Feb 10, 2025
f5daa41
2172 - Revert throw 500 back
MackHalliday Feb 10, 2025
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
6 changes: 6 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ fileignoreconfig:
checksum: 572fb3a52e89ae19c5a94be881bcbfe0247bae112bce58d4ae1e9eff5a5c91b7
- filename: tests/app/v2/notifications/test_get_notifications.py
checksum: 167aa80c8aac01566b3cf627cdaa839feac9dc77f0b13b6d0a008035aa8e15f1
- filename: tests/app/v2/notifications/test_push_broadcast_notifications.py
checksum: a2e9c2d625158d39622cfe82f1cc38d9623acc6bf4c2d56ea816910143ad15dd
- filename: tests/app/v2/notifications/test_push_notifications.py
checksum: 8781698e08707a8c9f50fc4f6e4342065a65a069614b88a1192e8ee8f89c268c
- filename: tests/app/va/vetext/test_client.py
checksum: 36fb86b6221e49703e51eda249e1a6d090483968a1c7a198be231470e021642c
version: "1.0"
51 changes: 44 additions & 7 deletions app/celery/provider_tasks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from uuid import UUID

from app import notify_celery
from celery import Task
from flask import current_app
from notifications_utils.field import NullValueForNonConditionalPlaceholderException
from notifications_utils.recipients import InvalidEmailError, InvalidPhoneError
from notifications_utils.statsd_decorators import statsd

from app import notify_celery, vetext_client
from app.celery.common import (
can_retry,
handle_max_retries_exceeded,
Expand Down Expand Up @@ -28,14 +34,9 @@
NotificationTechnicalFailureException,
)
from app.models import Notification
from app.v2.dataclasses import V2PushPayload
from app.v2.errors import RateLimitError

from celery import Task
from flask import current_app
from notifications_utils.field import NullValueForNonConditionalPlaceholderException
from notifications_utils.recipients import InvalidEmailError, InvalidPhoneError
from notifications_utils.statsd_decorators import statsd


# Including sms_sender_id is necessary in case it's passed in when being called
@notify_celery.task(
Expand Down Expand Up @@ -269,3 +270,39 @@ def _handle_delivery_failure(
if notification:
check_and_queue_callback_task(notification)
raise NotificationTechnicalFailureException(msg)


@notify_celery.task(
bind=True,
serializer='pickle',
name='deliver_push',
throws=(AutoRetryException,),
autoretry_for=(AutoRetryException,),
max_retries=2886,
retry_backoff=True,
retry_backoff_max=60,
)
@statsd(namespace='tasks')
def deliver_push(
MackHalliday marked this conversation as resolved.
Show resolved Hide resolved
celery_task: Task,
payload: V2PushPayload,
) -> None:
"""Deliver a validated push (or broadcast) payload to the provider client."""
vetext_payload = vetext_client.format_for_vetext(payload)
try:
vetext_client.send_push_notification(vetext_payload)
except RetryableException:
max_retries = celery_task.max_retries
retries = celery_task.request.retries

if retries < max_retries:
current_app.logger.warning(
'Push notification retrying: %s, max retries: %s, retries: %s', max_retries, retries
)
raise AutoRetryException('Found RetryableException, autoretrying...')
else:
current_app.logger.exception('deliver_push: Notification failed by exceeding retry limits')
raise NonRetryableException('Exceeded retries')
except NonRetryableException:
current_app.logger.exception('deliver_push encountered a NonRetryableException')
raise
2 changes: 2 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ class Config(object):
'app.celery.process_pinpoint_receipt_tasks',
'app.celery.process_pinpoint_inbound_sms',
'app.celery.process_delivery_status_result_tasks',
'app.celery.provider_tasks',
'app.celery.v3.notification_tasks',
'app.celery.process_ses_receipts_tasks',
'app.celery.send_va_profile_notification_status_tasks',
Expand Down Expand Up @@ -325,6 +326,7 @@ class Config(object):
},
'task_queues': [Queue(queue, Exchange('default'), routing_key=queue) for queue in QueueNames.all_queues()],
'task_routes': {
'app.celery.provider_tasks.deliver_push': {'queue': QueueNames.SEND_EMAIL},
'app.celery.v3.notification_tasks.v3_process_notification': {'queue': QueueNames.NOTIFY},
'app.celery.v3.notification_tasks.v3_send_email_notification': {'queue': QueueNames.SEND_EMAIL},
'app.celery.v3.notification_tasks.v3_send_sms_notification': {'queue': QueueNames.SEND_SMS},
Expand Down
28 changes: 27 additions & 1 deletion app/internal/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
- trace_id
"""

import time
from werkzeug.exceptions import UnsupportedMediaType
from contextlib import suppress
from flask import Blueprint, current_app, request
from flask import Blueprint, current_app, jsonify, request


internal_blueprint = Blueprint('internal', __name__, url_prefix='/internal')
Expand Down Expand Up @@ -56,4 +57,29 @@
else:
response_body = {generic: request.json}

return response_body, status_code

Check warning

Code scanning / CodeQL

Reflected server-side cross-site scripting Medium

Cross-site scripting vulnerability due to a
user-provided value
.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered in tests tests/app/internal/test_rest.py

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to resolve the CodeQL issue here. @k-macmillan maybe dismiss as false positive?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure - This internal endpoint does log the header from the incoming request.

@k-macmillan
Screenshot 2025-02-06 at 10 30 28 PM

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut tells me this is not great.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should never be logging the header since the headers all contain bearer tokens. But I don't think that is what CodeQL is complaining about.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cris-oddball talk to @k-macmillan about this - and we decided to make a ticket to address this issue since the code throwing the error was not code written for this PR.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put the ticket number in a comment near this, so the next time it flags we won't create a new ticket. AC of the new ticket should include removal of the comment. You can fill out the ticket details after requesting re-review of this PR.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


@internal_blueprint.route('/502', methods=['POST', 'GET'])
def throw_502():
"""
Returns a 502 Bad Gateway response for testing purposes.

Returns:
Response: A JSON response with 'result' set to 'error' and 'message',
along with a 502 status code.
"""
return jsonify(result='error', message='hello world'), 502


@internal_blueprint.route('/sleep', methods=['POST', 'GET'])
def sleep():
"""
Simulates a delayed response by sleeping for 30 seconds.

Returns:
Response: A JSON response indicating success, with a 'message' stating the sleep duration,
along with a 201 status code.
"""
time.sleep(30)
return jsonify(result='success', message='slept for 30s'), 201
cris-oddball marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion app/mobile_app/mobile_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def __init__(
self.type: MobileAppType = type
self.sid = self._get_sid_from_env()

def _get_sid_from_env(self):
def _get_sid_from_env(self) -> str:
sid = os.getenv(f'{self.type.value}_SID', None)
if not sid:
raise ValueError(f'Missing SID for app: {self.type.value}')
Expand Down
8 changes: 6 additions & 2 deletions app/mobile_app/mobile_app_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ def __init__(self, logger):
def get_app(
self,
app_type: MobileAppType,
) -> MobileApp | None:
return self._registry.get(app_type)
) -> MobileApp:
try:
return self._registry[app_type]
except KeyError:
self.logger.warning('Attempted to use an app that is not initialized: %s', app_type)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be an exception or critical, instead of warning?

Copy link

@MackHalliday MackHalliday Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

raise

def get_registered_apps(self) -> List[MobileAppType]:
return list(self._registry.keys())
27 changes: 27 additions & 0 deletions app/v2/dataclasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from dataclasses import dataclass


@dataclass
class V2PushPayload:
app_sid: str
template_id: str
icn: str | None = None
topic_sid: str | None = None
personalisation: dict[str, str] | None = None

def is_valid(self) -> bool:
"""Identify if the payload is valid

Returns:
bool: True if valid
"""
# icn alone means it's push. topic_sid alone means broadcast. Cannot be both.
return (self.icn and self.topic_sid is None) or (self.topic_sid and self.icn is None)

def is_broadcast(self) -> bool:
"""Identify if the payload is a broadcast

Returns:
bool: True if it is a broadcast
"""
return self.topic_sid is not None
98 changes: 69 additions & 29 deletions app/v2/notifications/rest_push.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,99 @@
from flask import jsonify, request
import json

from app import authenticated_service, vetext_client, mobile_app_registry
from celery.exceptions import CeleryError
from jsonschema import ValidationError
from kombu.exceptions import OperationalError
from flask import current_app, jsonify, request

from app import authenticated_service, mobile_app_registry
from app.celery.provider_tasks import deliver_push
from app.constants import PUSH_TYPE
from app.feature_flags import FeatureFlag, is_feature_enabled
from app.mobile_app import DEAFULT_MOBILE_APP_TYPE, MobileAppType
from app.schema_validation import validate
from app.utils import get_public_notify_type_text
from app.v2.errors import BadRequestError
from app.v2.notifications import v2_notification_blueprint
from app.v2.dataclasses import V2PushPayload
from app.v2.notifications.notification_schemas import push_notification_broadcast_request, push_notification_request
from app.va.vetext import VETextBadRequestException, VETextNonRetryableException, VETextRetryableException


@v2_notification_blueprint.route('/push', methods=['POST'])
def send_push_notification():
return push_notification_helper(push_notification_request, False)
return push_notification_helper(push_notification_request)


@v2_notification_blueprint.route('/push/broadcast', methods=['POST'])
def send_push_broadcast_notification():
return push_notification_helper(push_notification_broadcast_request, True)
return push_notification_helper(push_notification_broadcast_request)


def push_notification_helper(schema: dict, is_broadcast: bool):
def push_notification_helper(schema: dict):
"""
kalbfled marked this conversation as resolved.
Show resolved Hide resolved
Note that this helper cannot be called other than as part of a request because it accesses
the Flask "request" instance.
"""

if not is_feature_enabled(FeatureFlag.PUSH_NOTIFICATIONS_ENABLED):
raise NotImplementedError()

if not authenticated_service.has_permissions(PUSH_TYPE):
raise BadRequestError(
message='Service is not allowed to send {}'.format(get_public_notify_type_text(PUSH_TYPE, plural=True))
message='Service is not allowed to send {}'.format(
get_public_notify_type_text(PUSH_TYPE, plural=True),
),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we convert this to an f-string while we are here?

Suggested change
message='Service is not allowed to send {}'.format(
get_public_notify_type_text(PUSH_TYPE, plural=True),
message = f"Service is not allowed to send {get_public_notify_type_text(PUSH_TYPE, plural=True)}"

Copy link

@MackHalliday MackHalliday Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

status_code=403,
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this isn't 400. If it should be 403, it seems like we should be raising a different exception.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is currently a 400. A 403 status code means access to a requested resource is forbidden. It occurs when a server understands a request but refuses to authorize it. In this case, that seems more appropriate (the service is not authorized to send push).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cris-oddball Thank you for responding to Dave

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I concur that 403 makes more sense. I'm suggesting that we should create a new exception, perhaps "PermissionDeniedError," rather than hack the BadRequestError. A bad request is 400.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh I gotcha. Yea, the BadRequestError threw me too. PermissionDeniedError seems better.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not use PermissionDeniedError anywhere in this code base - For 403 errors we generally use InvalidRequest, AuthError, or BadRequestError. I'm hesitant to add error type to this code base

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no issues with the BadRequestError. The class allows for any status code. The name of it could have been more narrow or InvalidRequest could have more derived classes but I don't think any of that is necessary for this ticket.


req_json = validate(request.get_json(), schema)
payload = validate_push_payload(schema)

if 'mobile_app' in req_json:
app_instance = mobile_app_registry.get_app(MobileAppType[req_json['mobile_app']])
try:
# Choosing to use the email queue for push to limit the number of empty queues
deliver_push.delay(payload)
except (CeleryError, OperationalError):
current_app.logger.exception('Failed to enqueue deliver_push request')
response = jsonify(result='error', message='VA Notify service impaired, please try again'), 502
else:
MackHalliday marked this conversation as resolved.
Show resolved Hide resolved
app_instance = mobile_app_registry.get_app(DEAFULT_MOBILE_APP_TYPE)
response = jsonify(result='success'), 201
# Flask turns the tuple into a json and status_code
return response


def validate_push_payload(schema: dict[str, str]) -> V2PushPayload:
"""Validate an incoming push request.

Args:
schema (dict[str, str]): The incoming request

Raises:
BadRequestError: Failed validation

Returns:
dict[str, str]: Validated request dictionary
"""
try:
vetext_client.send_push_notification(
app_instance.sid,
req_json['template_id'],
req_json['topic_sid'] if is_broadcast else req_json['recipient_identifier']['id_value'],
is_broadcast,
req_json.get('personalisation'),
)
except VETextBadRequestException as e:
raise BadRequestError(message=e.message, status_code=400)
except (VETextNonRetryableException, VETextRetryableException):
return jsonify(result='error', message='Invalid response from downstream service'), 502
else:
return jsonify(result='success'), 201
req_json: dict[str, str] = validate(request.get_json(), schema)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally need to stop writing these long, Java-esq try/except blocks. It's not PEP8 compliant. This line might raise ValidationError, and it is the only line that should be in this "try" block. The subsequent if/else should not raise any exception. (Please verify that, if necessary.)

Copy link

@MackHalliday MackHalliday Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok - I believe we can remove the TypeError and the general Exception.

I believe this still needs to stay in the Try/Except block. The get_app method was updated to throw a KeyError we may need to catch and log.

Copy link
Member

@kalbfled kalbfled Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should still be broken up. Similar to this . . .

try:
  req_json = validate(...)
except ValidationError:
  pass

try:
  app_sid = ...
except KeyError:
  pass

This is in line with PEP8, and it makes it obvious what code might raise what exception.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah gotcha. I like that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a specific section of the pep8 for me @kalbfled ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated but would also appreciate seeing an reference @kalbfled

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attempted to update this which lead down a rabbit hole - Reverted.

#2266 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From PEP8: "Additionally, for all try/except clauses, limit the try clause to the absolute minimum amount of code necessary. Again, this avoids masking bugs."

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link to that in PEP8 (scroll down or just search for the sentence) is https://peps.python.org/pep-0008/#programming-recommendations

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you - I will keep this in mind for the future. At this time - I would prefer not to refactor this unless its a requirement.

# Validate the application they sent us is valid or use the default
if 'mobile_app' in req_json:
app_sid = mobile_app_registry.get_app(MobileAppType[req_json['mobile_app']]).sid
else:
app_sid = mobile_app_registry.get_app(DEAFULT_MOBILE_APP_TYPE).sid
except (KeyError, TypeError) as e:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we've been using DEAFULT_MOBILE_APP_TYPE for the past four years (inherited typo) but since we are here, can we fix the spelling now, wherever it appears in the repo?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating.

current_app.logger.warning('Push request failed validation due to mobile app setup: %s', e)
raise BadRequestError(message=str(e), status_code=400)
except ValidationError as e:
Comment on lines +88 to +90
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still relevant? You deleted the call to send_push_notification, which might have raised KeyError via the calling parameters.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - validate method raises a ValidationError. We would like to include the logs below if a ValidationError is thrown.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided not to remove the TypeError due to issues with test tests/app/integration/test_push_notification.py::test_mobile_app_push_notification_failed_validation. I tried to update this test and remove testing for TypeError. While updating the test, TypeError continued to be thrown - appears a TypeError is thrown if mobile_app is None in the request_body.

current_app.logger.warning('Push request failed validation: %s', e)
error_data = json.loads(e.message)
error_data['errors'] = error_data['errors'][0]
raise e
except Exception:
msg = 'Unable to process request for push notification - bad request'
current_app.logger.exception(msg)
raise BadRequestError(message=msg, status_code=400)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What condition would put you in this block?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None - We should remove this.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

# Use get() on optionals - schema validated it is correct
payload = V2PushPayload(
app_sid,
req_json['template_id'],
req_json.get('recipient_identifier', {}).get('id_value'), # ICN
req_json.get('topic_sid'),
req_json.get('personalisation'),
)
current_app.logger.debug('V2PushPayload is: %s', payload)
return payload
Loading