code review feedback and merge from main

This commit is contained in:
Kenneth Kehl
2024-09-11 09:39:18 -07:00
34 changed files with 256 additions and 161 deletions
+2 -2
View File
@@ -209,7 +209,7 @@
"filename": "tests/app/aws/test_s3.py", "filename": "tests/app/aws/test_s3.py",
"hashed_secret": "67a74306b06d0c01624fe0d0249a570f4d093747", "hashed_secret": "67a74306b06d0c01624fe0d0249a570f4d093747",
"is_verified": false, "is_verified": false,
"line_number": 25, "line_number": 27,
"is_secret": false "is_secret": false
} }
], ],
@@ -384,5 +384,5 @@
} }
] ]
}, },
"generated_at": "2024-08-22T18:00:24Z" "generated_at": "2024-09-10T18:12:39Z"
} }
+2 -3
View File
@@ -265,7 +265,7 @@ def init_app(app):
@app.errorhandler(Exception) @app.errorhandler(Exception)
def exception(error): def exception(error):
app.logger.exception("Handling error:", exc_info=True) app.logger.exception(f"Handling error: {error}")
# error.code is set for our exception types. # error.code is set for our exception types.
msg = getattr(error, "message", str(error)) msg = getattr(error, "message", str(error))
code = getattr(error, "code", 500) code = getattr(error, "code", 500)
@@ -354,7 +354,7 @@ def setup_sqlalchemy_events(app):
} }
except Exception: except Exception:
current_app.logger.exception( current_app.logger.exception(
"Exception caught for checkout event.", exc_info=True "Exception caught for checkout event.",
) )
@event.listens_for(db.engine, "checkin") @event.listens_for(db.engine, "checkin")
@@ -406,7 +406,6 @@ def make_task(app):
task_name=self.name, task_name=self.name,
queue_name=self.queue_name, queue_name=self.queue_name,
), ),
exc_info=True,
) )
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
+42 -6
View File
@@ -1,3 +1,4 @@
import os
import uuid import uuid
from flask import current_app, g, request from flask import current_app, g, request
@@ -62,17 +63,25 @@ def requires_admin_auth():
def requires_internal_auth(expected_client_id): def requires_internal_auth(expected_client_id):
if expected_client_id not in current_app.config.get("INTERNAL_CLIENT_API_KEYS"):
raise TypeError("Unknown client_id for internal auth") # Looks like we are hitting this for some reason
# expected_client_id looks like ADMIN_CLIENT_USERNAME on the admin side, and
# INTERNAL_CLIENT_API_KEYS is a dict
keys = current_app.config.get("INTERNAL_CLIENT_API_KEYS")
if keys.get(expected_client_id) is None:
err_msg = "Unknown client_id for internal auth"
current_app.logger.error(err_msg)
raise TypeError(err_msg)
request_helper.check_proxy_header_before_request() request_helper.check_proxy_header_before_request()
auth_token = _get_auth_token(request) auth_token = _get_auth_token(request)
client_id = _get_token_issuer(auth_token) client_id = _get_token_issuer(auth_token)
if client_id != expected_client_id: if client_id != expected_client_id:
current_app.logger.info("client_id: %s", client_id) current_app.logger.info("client_id: %s", client_id)
current_app.logger.info("expected_client_id: %s", expected_client_id) current_app.logger.info("expected_client_id: %s", expected_client_id)
raise AuthError("Unauthorized: not allowed to perform this action", 401) err_msg = "Unauthorized: not allowed to perform this action"
current_app.logger.error(err_msg)
raise AuthError(err_msg, 401)
api_keys = [ api_keys = [
InternalApiKey(client_id, secret) InternalApiKey(client_id, secret)
@@ -125,19 +134,37 @@ def requires_auth():
def _decode_jwt_token(auth_token, api_keys, service_id=None): def _decode_jwt_token(auth_token, api_keys, service_id=None):
# Temporary expedient to get e2e tests working. If we are in
# the development or staging environments, just return the first
# api key.
if os.getenv("NOTIFY_ENVIRONMENT") in ["development", "staging"]:
for api_key in api_keys:
return api_key
for api_key in api_keys: for api_key in api_keys:
try: try:
decode_jwt_token(auth_token, api_key.secret) decode_jwt_token(auth_token, api_key.secret)
except TypeError:
err_msg = "Invalid token: type error"
current_app.logger.exception(err_msg)
raise AuthError(
"Invalid token: type error",
403,
service_id=service_id,
api_key_id=api_key.id,
)
except TokenExpiredError: except TokenExpiredError:
if not current_app.config.get("ALLOW_EXPIRED_API_TOKEN", False): if not current_app.config.get("ALLOW_EXPIRED_API_TOKEN", False):
err_msg = ( err_msg = (
"Error: Your system clock must be accurate to within 30 seconds" "Error: Your system clock must be accurate to within 30 seconds"
) )
current_app.logger.exception(err_msg)
raise AuthError( raise AuthError(
err_msg, 403, service_id=service_id, api_key_id=api_key.id err_msg, 403, service_id=service_id, api_key_id=api_key.id
) )
except TokenAlgorithmError: except TokenAlgorithmError:
err_msg = "Invalid token: algorithm used is not HS256" err_msg = "Invalid token: algorithm used is not HS256"
current_app.logger.exception(err_msg)
raise AuthError(err_msg, 403, service_id=service_id, api_key_id=api_key.id) raise AuthError(err_msg, 403, service_id=service_id, api_key_id=api_key.id)
except TokenDecodeError: except TokenDecodeError:
# we attempted to validate the token but it failed meaning it was not signed using this api key. # we attempted to validate the token but it failed meaning it was not signed using this api key.
@@ -145,8 +172,12 @@ def _decode_jwt_token(auth_token, api_keys, service_id=None):
# TODO: Change this so it doesn't also catch `TokenIssuerError` or `TokenIssuedAtError` exceptions (which # TODO: Change this so it doesn't also catch `TokenIssuerError` or `TokenIssuedAtError` exceptions (which
# are children of `TokenDecodeError`) as these should cause an auth error immediately rather than # are children of `TokenDecodeError`) as these should cause an auth error immediately rather than
# continue on to check the next API key # continue on to check the next API key
current_app.logger.exception(
"TokenDecodeError. Couldn't decode auth token for given api key"
)
continue continue
except TokenError: except TokenError:
current_app.logger.exception("TokenError")
# General error when trying to decode and validate the token # General error when trying to decode and validate the token
raise AuthError( raise AuthError(
GENERAL_TOKEN_ERROR_MESSAGE, GENERAL_TOKEN_ERROR_MESSAGE,
@@ -156,8 +187,10 @@ def _decode_jwt_token(auth_token, api_keys, service_id=None):
) )
if api_key.expiry_date: if api_key.expiry_date:
err_msg = "Invalid token: API key revoked"
current_app.logger.error(err_msg, exc_info=True)
raise AuthError( raise AuthError(
"Invalid token: API key revoked", err_msg,
403, 403,
service_id=service_id, service_id=service_id,
api_key_id=api_key.id, api_key_id=api_key.id,
@@ -166,7 +199,10 @@ def _decode_jwt_token(auth_token, api_keys, service_id=None):
return api_key return api_key
else: else:
# service has API keys, but none matching the one the user provided # service has API keys, but none matching the one the user provided
raise AuthError("Invalid token: API key not found", 403, service_id=service_id) # if we get here, we probably hit TokenDecodeErrors earlier
err_msg = "Invalid token: API key not found"
current_app.logger.error(err_msg, exc_info=True)
raise AuthError(err_msg, 403, service_id=service_id)
def _get_auth_token(req): def _get_auth_token(req):
+46 -18
View File
@@ -77,9 +77,42 @@ def list_s3_objects():
else: else:
break break
except Exception: except Exception:
current_app.logger.error( current_app.logger.exception(
"An error occurred while regenerating cache #notify-admin-1200", "An error occurred while regenerating cache #notify-admin-1200",
exc_info=True, )
def get_bucket_name():
return current_app.config["CSV_UPLOAD_BUCKET"]["bucket"]
def cleanup_old_s3_objects():
bucket_name = get_bucket_name()
s3_client = get_s3_client()
# Our reports only support 7 days, but can be scheduled 3 days in advance
# Use 14 day for the v1.0 version of this behavior
time_limit = aware_utcnow() - datetime.timedelta(days=14)
try:
response = s3_client.list_objects_v2(Bucket=bucket_name)
print(f"RESPONSE = {response}")
while True:
for obj in response.get("Contents", []):
if obj["LastModified"] <= time_limit:
current_app.logger.info(
f"#delete-old-s3-objects Wanting to delete: {obj['LastModified']} {obj['Key']}"
)
if "NextContinuationToken" in response:
response = s3_client.list_objects_v2(
Bucket=bucket_name,
ContinuationToken=response["NextContinuationToken"],
)
else:
break
except Exception:
current_app.logger.exception(
"#delete-old-s3-objects An error occurred while cleaning up old s3 objects",
) )
@@ -109,7 +142,7 @@ def get_s3_files():
JOBS[job_id] = object JOBS[job_id] = object
except LookupError: except LookupError:
# perhaps our key is not formatted as we expected. If so skip it. # perhaps our key is not formatted as we expected. If so skip it.
current_app.logger.error("LookupError #notify-admin-1200", exc_info=True) current_app.logger.exception("LookupError #notify-admin-1200")
current_app.logger.info( current_app.logger.info(
f"JOBS cache length after regen: {len(JOBS)} #notify-admin-1200" f"JOBS cache length after regen: {len(JOBS)} #notify-admin-1200"
@@ -131,13 +164,13 @@ def download_from_s3(
result = s3.download_file(bucket_name, s3_key, local_filename) result = s3.download_file(bucket_name, s3_key, local_filename)
current_app.logger.info(f"File downloaded successfully to {local_filename}") current_app.logger.info(f"File downloaded successfully to {local_filename}")
except botocore.exceptions.NoCredentialsError as nce: except botocore.exceptions.NoCredentialsError as nce:
current_app.logger.error("Credentials not found", exc_info=True) current_app.logger.exception("Credentials not found")
raise Exception(nce) raise Exception(nce)
except botocore.exceptions.PartialCredentialsError as pce: except botocore.exceptions.PartialCredentialsError as pce:
current_app.logger.error("Incomplete credentials provided", exc_info=True) current_app.logger.exception("Incomplete credentials provided")
raise Exception(pce) raise Exception(pce)
except Exception: except Exception:
current_app.logger.error("An error occurred", exc_info=True) current_app.logger.exception("An error occurred")
text = f"EXCEPTION local_filename {local_filename}" text = f"EXCEPTION local_filename {local_filename}"
raise Exception(text) raise Exception(text)
return result return result
@@ -149,8 +182,8 @@ def get_s3_object(bucket_name, file_location, access_key, secret_key, region):
try: try:
return s3.Object(bucket_name, file_location) return s3.Object(bucket_name, file_location)
except botocore.exceptions.ClientError: except botocore.exceptions.ClientError:
current_app.logger.error( current_app.logger.exception(
f"Can't retrieve S3 Object from {file_location}", exc_info=True f"Can't retrieve S3 Object from {file_location}",
) )
@@ -224,9 +257,8 @@ def get_job_from_s3(service_id, job_id):
"RequestTimeout", "RequestTimeout",
"SlowDown", "SlowDown",
]: ]:
current_app.logger.error( current_app.logger.exception(
f"Retrying job fetch {FILE_LOCATION_STRUCTURE.format(service_id, job_id)} retry_count={retries}", f"Retrying job fetch {FILE_LOCATION_STRUCTURE.format(service_id, job_id)} retry_count={retries}",
exc_info=True,
) )
retries += 1 retries += 1
sleep_time = backoff_factor * (2**retries) # Exponential backoff sleep_time = backoff_factor * (2**retries) # Exponential backoff
@@ -234,22 +266,19 @@ def get_job_from_s3(service_id, job_id):
continue continue
else: else:
# Typically this is "NoSuchKey" # Typically this is "NoSuchKey"
current_app.logger.error( current_app.logger.exception(
f"Failed to get job {FILE_LOCATION_STRUCTURE.format(service_id, job_id)}", f"Failed to get job {FILE_LOCATION_STRUCTURE.format(service_id, job_id)}",
exc_info=True,
) )
return None return None
except Exception: except Exception:
current_app.logger.error( current_app.logger.exception(
f"Failed to get job {FILE_LOCATION_STRUCTURE.format(service_id, job_id)} retry_count={retries}", f"Failed to get job {FILE_LOCATION_STRUCTURE.format(service_id, job_id)} retry_count={retries}",
exc_info=True,
) )
return None return None
current_app.logger.error( current_app.logger.error(
f"Never retrieved job {FILE_LOCATION_STRUCTURE.format(service_id, job_id)}", f"Never retrieved job {FILE_LOCATION_STRUCTURE.format(service_id, job_id)}",
exc_info=True,
) )
return None return None
@@ -289,7 +318,6 @@ def extract_phones(job):
phones[job_row] = "Unavailable" phones[job_row] = "Unavailable"
current_app.logger.error( current_app.logger.error(
"Corrupt csv file, missing columns or possibly a byte order mark in the file", "Corrupt csv file, missing columns or possibly a byte order mark in the file",
exc_info=True,
) )
else: else:
@@ -352,7 +380,7 @@ def get_phone_number_from_s3(service_id, job_id, job_row_number):
return "Unavailable" return "Unavailable"
else: else:
current_app.logger.error( current_app.logger.error(
f"Was unable to construct lookup dictionary for job {job_id}", exc_info=True f"Was unable to construct lookup dictionary for job {job_id}"
) )
return "Unavailable" return "Unavailable"
@@ -399,7 +427,7 @@ def get_personalisation_from_s3(service_id, job_id, job_row_number):
return {} return {}
else: else:
current_app.logger.error( current_app.logger.error(
f"Was unable to construct lookup dictionary for job {job_id}", exc_info=True f"Was unable to construct lookup dictionary for job {job_id}"
) )
return {} return {}
+2 -5
View File
@@ -54,9 +54,8 @@ def cleanup_unfinished_jobs():
try: try:
acceptable_finish_time = job.processing_started + timedelta(minutes=5) acceptable_finish_time = job.processing_started + timedelta(minutes=5)
except TypeError: except TypeError:
current_app.logger.error( current_app.logger.exception(
f"Job ID {job.id} processing_started is {job.processing_started}.", f"Job ID {job.id} processing_started is {job.processing_started}.",
exc_info=True,
) )
raise raise
if now > acceptable_finish_time: if now > acceptable_finish_time:
@@ -194,9 +193,7 @@ def delete_inbound_sms():
) )
) )
except SQLAlchemyError: except SQLAlchemyError:
current_app.logger.exception( current_app.logger.exception("Failed to delete inbound sms notifications")
"Failed to delete inbound sms notifications", exc_info=True
)
raise raise
+2 -2
View File
@@ -114,7 +114,7 @@ def process_ses_results(self, response):
raise raise
except Exception: except Exception:
current_app.logger.exception("Error processing SES results", exc_info=True) current_app.logger.exception("Error processing SES results")
self.retry(queue=QueueNames.RETRY) self.retry(queue=QueueNames.RETRY)
@@ -206,7 +206,7 @@ def handle_complaint(ses_message):
reference = ses_message["mail"]["messageId"] reference = ses_message["mail"]["messageId"]
except KeyError: except KeyError:
current_app.logger.exception( current_app.logger.exception(
"Complaint from SES failed to get reference from message", exc_info=True "Complaint from SES failed to get reference from message"
) )
return return
notification = dao_get_notification_history_by_reference(reference) notification = dao_get_notification_history_by_reference(reference)
+2 -6
View File
@@ -144,12 +144,10 @@ def deliver_sms(self, notification_id):
if isinstance(e, SmsClientResponseException): if isinstance(e, SmsClientResponseException):
current_app.logger.warning( current_app.logger.warning(
"SMS notification delivery for id: {} failed".format(notification_id), "SMS notification delivery for id: {} failed".format(notification_id),
exc_info=True,
) )
else: else:
current_app.logger.exception( current_app.logger.exception(
"SMS notification delivery for id: {} failed".format(notification_id), "SMS notification delivery for id: {} failed".format(notification_id),
exc_info=True,
) )
try: try:
@@ -188,9 +186,7 @@ def deliver_email(self, notification_id):
notification.personalisation = json.loads(personalisation) notification.personalisation = json.loads(personalisation)
send_to_providers.send_email_to_provider(notification) send_to_providers.send_email_to_provider(notification)
except EmailClientNonRetryableException: except EmailClientNonRetryableException:
current_app.logger.exception( current_app.logger.exception(f"Email notification {notification_id} failed")
f"Email notification {notification_id} failed", exc_info=True
)
update_notification_status_by_id(notification_id, "technical-failure") update_notification_status_by_id(notification_id, "technical-failure")
except Exception as e: except Exception as e:
try: try:
@@ -200,7 +196,7 @@ def deliver_email(self, notification_id):
) )
else: else:
current_app.logger.exception( current_app.logger.exception(
f"RETRY: Email notification {notification_id} failed", exc_info=True f"RETRY: Email notification {notification_id} failed"
) )
self.retry(queue=QueueNames.RETRY) self.retry(queue=QueueNames.RETRY)
+4 -4
View File
@@ -46,7 +46,7 @@ def run_scheduled_jobs():
"Job ID {} added to process job queue".format(job.id) "Job ID {} added to process job queue".format(job.id)
) )
except SQLAlchemyError: except SQLAlchemyError:
current_app.logger.exception("Failed to run scheduled jobs", exc_info=True) current_app.logger.exception("Failed to run scheduled jobs")
raise raise
@@ -61,7 +61,7 @@ def delete_verify_codes():
) )
) )
except SQLAlchemyError: except SQLAlchemyError:
current_app.logger.exception("Failed to delete verify codes", exc_info=True) current_app.logger.exception("Failed to delete verify codes")
raise raise
@@ -74,7 +74,7 @@ def expire_or_delete_invitations():
f"Expire job started {start} finished {utc_now()} expired {expired_invites} invitations" f"Expire job started {start} finished {utc_now()} expired {expired_invites} invitations"
) )
except SQLAlchemyError: except SQLAlchemyError:
current_app.logger.exception("Failed to expire invitations", exc_info=True) current_app.logger.exception("Failed to expire invitations")
raise raise
try: try:
@@ -84,7 +84,7 @@ def expire_or_delete_invitations():
f"Delete job started {start} finished {utc_now()} deleted {deleted_invites} invitations" f"Delete job started {start} finished {utc_now()} deleted {deleted_invites} invitations"
) )
except SQLAlchemyError: except SQLAlchemyError:
current_app.logger.exception("Failed to delete invitations", exc_info=True) current_app.logger.exception("Failed to delete invitations")
raise raise
+11 -9
View File
@@ -158,11 +158,10 @@ def __total_sending_limits_for_job_exceeded(service, job, job_id):
job.job_status = "sending limits exceeded" job.job_status = "sending limits exceeded"
job.processing_finished = utc_now() job.processing_finished = utc_now()
dao_update_job(job) dao_update_job(job)
current_app.logger.error( current_app.logger.exception(
"Job {} size {} error. Total sending limits {} exceeded".format( "Job {} size {} error. Total sending limits {} exceeded".format(
job_id, job.notification_count, service.message_limit job_id, job.notification_count, service.message_limit
), ),
exc_info=True,
) )
return True return True
@@ -361,9 +360,8 @@ def save_api_email_or_sms(self, encrypted_notification):
try: try:
self.retry(queue=QueueNames.RETRY) self.retry(queue=QueueNames.RETRY)
except self.MaxRetriesExceededError: except self.MaxRetriesExceededError:
current_app.logger.error( current_app.logger.exception(
f"Max retry failed Failed to persist notification {notification['id']}", f"Max retry failed Failed to persist notification {notification['id']}",
exc_info=True,
) )
@@ -379,11 +377,11 @@ def handle_exception(task, notification, notification_id, exc):
# SQLAlchemy is throwing a FlushError. So we check if the notification id already exists then do not # SQLAlchemy is throwing a FlushError. So we check if the notification id already exists then do not
# send to the retry queue. # send to the retry queue.
# This probably (hopefully) is not an issue with Redis as the celery backing store # This probably (hopefully) is not an issue with Redis as the celery backing store
current_app.logger.exception("Retry" + retry_msg, exc_info=True) current_app.logger.exception("Retry" + retry_msg)
try: try:
task.retry(queue=QueueNames.RETRY, exc=exc) task.retry(queue=QueueNames.RETRY, exc=exc)
except task.MaxRetriesExceededError: except task.MaxRetriesExceededError:
current_app.logger.error("Max retry failed" + retry_msg, exc_info=True) current_app.logger.exception("Max retry failed" + retry_msg)
@notify_celery.task( @notify_celery.task(
@@ -432,10 +430,9 @@ def send_inbound_sms_to_service(self, inbound_sms_id, service_id):
try: try:
self.retry(queue=QueueNames.RETRY) self.retry(queue=QueueNames.RETRY)
except self.MaxRetriesExceededError: except self.MaxRetriesExceededError:
current_app.logger.error( current_app.logger.exception(
"Retry: send_inbound_sms_to_service has retried the max number of" "Retry: send_inbound_sms_to_service has retried the max number of"
+ f"times for service: {service_id} and inbound_sms {inbound_sms_id}", + f"times for service: {service_id} and inbound_sms {inbound_sms_id}"
exc_info=True,
) )
else: else:
current_app.logger.warning( current_app.logger.warning(
@@ -449,6 +446,11 @@ def regenerate_job_cache():
s3.get_s3_files() s3.get_s3_files()
@notify_celery.task(name="delete-old-s3-objects")
def delete_old_s3_objects():
s3.cleanup_old_s3_objects()
@notify_celery.task(name="process-incomplete-jobs") @notify_celery.task(name="process-incomplete-jobs")
def process_incomplete_jobs(job_ids): def process_incomplete_jobs(job_ids):
jobs = [dao_get_job_by_id(job_id) for job_id in job_ids] jobs = [dao_get_job_by_id(job_id) for job_id in job_ids]
@@ -41,8 +41,7 @@ class PerformancePlatformClient:
current_app.logger.error( current_app.logger.error(
"Performance platform update request failed for payload with response details: {} '{}'".format( "Performance platform update request failed for payload with response details: {} '{}'".format(
json.dumps(payload), resp.status_code json.dumps(payload), resp.status_code
), )
exc_info=True,
) )
resp.raise_for_status() resp.raise_for_status()
+2 -6
View File
@@ -80,14 +80,10 @@ class AwsSnsClient(SmsClient):
PhoneNumber=to, Message=content, MessageAttributes=attributes PhoneNumber=to, Message=content, MessageAttributes=attributes
) )
except botocore.exceptions.ClientError as e: except botocore.exceptions.ClientError as e:
self.current_app.logger.error( self.current_app.logger.exception("An error occurred sending sms")
"An error occurred sending sms", exc_info=True
)
raise str(e) raise str(e)
except Exception as e: except Exception as e:
self.current_app.logger.error( self.current_app.logger.exception("An error occurred sending sms")
"An error occurred sending sms", exc_info=True
)
raise str(e) raise str(e)
finally: finally:
elapsed_time = monotonic() - start_time elapsed_time = monotonic() - start_time
+16 -24
View File
@@ -127,7 +127,7 @@ def purge_functional_test_data(user_email_prefix):
users, services, etc. Give an email prefix. Probably "notify-tests-preview". users, services, etc. Give an email prefix. Probably "notify-tests-preview".
""" """
if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]: if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]:
current_app.logger.error("Can only be run in development", exc_info=True) current_app.logger.error("Can only be run in development")
return return
users = User.query.filter(User.email_address.like(f"{user_email_prefix}%")).all() users = User.query.filter(User.email_address.like(f"{user_email_prefix}%")).all()
@@ -302,9 +302,8 @@ def bulk_invite_user_to_service(file_name, service_id, user_id, auth_type, permi
) )
current_app.logger.info(response[0].get_data(as_text=True)) current_app.logger.info(response[0].get_data(as_text=True))
except Exception: except Exception:
current_app.logger.error( current_app.logger.exception(
f"*** ERROR occurred for email address: {email_address.strip()}.", f"*** ERROR occurred for email address: {email_address.strip()}.",
exc_info=True,
) )
file.close() file.close()
@@ -404,9 +403,7 @@ def populate_organizations_from_file(file_name):
db.session.add(org) db.session.add(org)
db.session.commit() db.session.commit()
except IntegrityError: except IntegrityError:
current_app.logger.error( current_app.logger.exception(f"Error duplicate org {org.name}")
f"Error duplicate org {org.name}", exc_info=True
)
db.session.rollback() db.session.rollback()
domains = columns[4].split(",") domains = columns[4].split(",")
for d in domains: for d in domains:
@@ -416,9 +413,8 @@ def populate_organizations_from_file(file_name):
db.session.add(domain) db.session.add(domain)
db.session.commit() db.session.commit()
except IntegrityError: except IntegrityError:
current_app.logger.error( current_app.logger.exception(
f"Integrity error duplicate domain {d.strip()}", f"Integrity error duplicate domain {d.strip()}",
exc_info=True,
) )
db.session.rollback() db.session.rollback()
@@ -530,15 +526,13 @@ def populate_go_live(file_name):
else: else:
go_live_user = None go_live_user = None
except NoResultFound: except NoResultFound:
current_app.logger.error( current_app.logger.exception("No user found for email address")
"No user found for email address", exc_info=True
)
continue continue
try: try:
service = dao_fetch_service_by_id(service_id) service = dao_fetch_service_by_id(service_id)
except NoResultFound: except NoResultFound:
current_app.logger.error( current_app.logger.exception(
f"No service found for service: {service_id}", exc_info=True f"No service found for service: {service_id}"
) )
continue continue
service.go_live_user = go_live_user service.go_live_user = go_live_user
@@ -786,9 +780,7 @@ def validate_mobile(ctx, param, value): # noqa
@click.option("-d", "--admin", default=False, type=bool) @click.option("-d", "--admin", default=False, type=bool)
def create_test_user(name, email, mobile_number, password, auth_type, state, admin): def create_test_user(name, email, mobile_number, password, auth_type, state, admin):
if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test", "staging"]: if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test", "staging"]:
current_app.logger.error( current_app.logger.error("Can only be run in development, test, staging")
"Can only be run in development, test, staging", exc_info=True
)
return return
data = { data = {
@@ -805,14 +797,14 @@ def create_test_user(name, email, mobile_number, password, auth_type, state, adm
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
except IntegrityError: except IntegrityError:
current_app.logger.error("Integrity error duplicate user", exc_info=True) current_app.logger.exception("Integrity error duplicate user")
db.session.rollback() db.session.rollback()
@notify_command(name="create-admin-jwt") @notify_command(name="create-admin-jwt")
def create_admin_jwt(): def create_admin_jwt():
if getenv("NOTIFY_ENVIRONMENT", "") != "development": if getenv("NOTIFY_ENVIRONMENT", "") != "development":
current_app.logger.error("Can only be run in development", exc_info=True) current_app.logger.error("Can only be run in development")
return return
current_app.logger.info( current_app.logger.info(
create_jwt_token( create_jwt_token(
@@ -825,7 +817,7 @@ def create_admin_jwt():
@click.option("-t", "--token", required=True, prompt=False) @click.option("-t", "--token", required=True, prompt=False)
def create_user_jwt(token): def create_user_jwt(token):
if getenv("NOTIFY_ENVIRONMENT", "") != "development": if getenv("NOTIFY_ENVIRONMENT", "") != "development":
current_app.logger.error("Can only be run in development", exc_info=True) current_app.logger.error("Can only be run in development")
return return
service_id = token[-73:-37] service_id = token[-73:-37]
api_key = token[-36:] api_key = token[-36:]
@@ -941,7 +933,7 @@ where possible to enable better maintainability.
@click.option("-g", "--generate", required=True, prompt=True, default=1) @click.option("-g", "--generate", required=True, prompt=True, default=1)
def add_test_organizations_to_db(generate): def add_test_organizations_to_db(generate):
if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]: if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]:
current_app.logger.error("Can only be run in development", exc_info=True) current_app.logger.error("Can only be run in development")
return return
def generate_gov_agency(): def generate_gov_agency():
@@ -1003,7 +995,7 @@ def add_test_organizations_to_db(generate):
@click.option("-g", "--generate", required=True, prompt=True, default=1) @click.option("-g", "--generate", required=True, prompt=True, default=1)
def add_test_services_to_db(generate): def add_test_services_to_db(generate):
if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]: if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]:
current_app.logger.error("Can only be run in development", exc_info=True) current_app.logger.error("Can only be run in development")
return return
for num in range(1, int(generate) + 1): for num in range(1, int(generate) + 1):
@@ -1017,7 +1009,7 @@ def add_test_services_to_db(generate):
@click.option("-g", "--generate", required=True, prompt=True, default=1) @click.option("-g", "--generate", required=True, prompt=True, default=1)
def add_test_jobs_to_db(generate): def add_test_jobs_to_db(generate):
if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]: if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]:
current_app.logger.error("Can only be run in development", exc_info=True) current_app.logger.error("Can only be run in development")
return return
for num in range(1, int(generate) + 1): for num in range(1, int(generate) + 1):
@@ -1032,7 +1024,7 @@ def add_test_jobs_to_db(generate):
@click.option("-g", "--generate", required=True, prompt=True, default=1) @click.option("-g", "--generate", required=True, prompt=True, default=1)
def add_test_notifications_to_db(generate): def add_test_notifications_to_db(generate):
if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]: if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]:
current_app.logger.error("Can only be run in development", exc_info=True) current_app.logger.error("Can only be run in development")
return return
for num in range(1, int(generate) + 1): for num in range(1, int(generate) + 1):
@@ -1053,7 +1045,7 @@ def add_test_notifications_to_db(generate):
@click.option("-d", "--admin", default=False, type=bool) @click.option("-d", "--admin", default=False, type=bool)
def add_test_users_to_db(generate, state, admin): def add_test_users_to_db(generate, state, admin):
if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]: if getenv("NOTIFY_ENVIRONMENT", "") not in ["development", "test"]:
current_app.logger.error("Can only be run in development", exc_info=True) current_app.logger.error("Can only be run in development")
return return
for num in range(1, int(generate) + 1): # noqa for num in range(1, int(generate) + 1): # noqa
+5
View File
@@ -249,6 +249,11 @@ class Config(object):
"schedule": crontab(hour=6, minute=0), "schedule": crontab(hour=6, minute=0),
"options": {"queue": QueueNames.PERIODIC}, "options": {"queue": QueueNames.PERIODIC},
}, },
"delete_old_s3_objects": {
"task": "delete-old-s3-objects",
"schedule": crontab(minute="*/5"),
"options": {"queue": QueueNames.PERIODIC},
},
"regenerate-job-cache": { "regenerate-job-cache": {
"task": "regenerate-job-cache", "task": "regenerate-job-cache",
"schedule": crontab(minute="*/30"), "schedule": crontab(minute="*/30"),
+1 -2
View File
@@ -19,8 +19,7 @@ def cronitor(task_name):
current_app.logger.error( current_app.logger.error(
"Cronitor enabled but task_name {} not found in environment".format( "Cronitor enabled but task_name {} not found in environment".format(
task_name task_name
), )
exc_info=True,
) )
return return
-1
View File
@@ -165,7 +165,6 @@ def update_notification_status_by_reference(reference, status):
"notification not found for reference {} (update to {})".format( "notification not found for reference {} (update to {})".format(
reference, status reference, status
), ),
exc_info=True,
) )
return None return None
+1 -1
View File
@@ -49,7 +49,7 @@ def get_login_gov_user(login_uuid, email_address):
# address in login.gov. # address in login.gov.
# But if we cannot change the email address, at least we don't # But if we cannot change the email address, at least we don't
# want to fail here, otherwise the user will be locked out. # want to fail here, otherwise the user will be locked out.
current_app.logger.error("Error getting login.gov user", exc_info=True) current_app.logger.exception("Error getting login.gov user")
db.session.rollback() db.session.rollback()
return user return user
+1 -1
View File
@@ -112,7 +112,7 @@ def send_sms_to_provider(notification):
except Exception as e: except Exception as e:
n = notification n = notification
msg = f"FAILED send to sms, job_id: {n.job_id} row_number {n.job_row_number} message_id {message_id}" msg = f"FAILED send to sms, job_id: {n.job_id} row_number {n.job_row_number} message_id {message_id}"
current_app.logger.error(hilite(msg), exc_info=True) current_app.logger.exception(hilite(msg))
notification.billable_units = template.fragment_count notification.billable_units = template.fragment_count
dao_update_notification(notification) dao_update_notification(notification)
+3 -3
View File
@@ -72,7 +72,7 @@ def register_errors(blueprint):
@blueprint.errorhandler(400) @blueprint.errorhandler(400)
def bad_request(e): def bad_request(e):
msg = e.description or "Invalid request parameters" msg = e.description or "Invalid request parameters"
current_app.logger.exception(msg, exc_info=True) current_app.logger.exception(msg)
return jsonify(result="error", message=str(msg)), 400 return jsonify(result="error", message=str(msg)), 400
@blueprint.errorhandler(401) @blueprint.errorhandler(401)
@@ -91,7 +91,7 @@ def register_errors(blueprint):
@blueprint.errorhandler(429) @blueprint.errorhandler(429)
def limit_exceeded(e): def limit_exceeded(e):
current_app.logger.exception(e, exc_info=True) current_app.logger.exception(e)
return jsonify(result="error", message=str(e.description)), 429 return jsonify(result="error", message=str(e.description)), 429
@blueprint.errorhandler(NoResultFound) @blueprint.errorhandler(NoResultFound)
@@ -107,7 +107,7 @@ def register_errors(blueprint):
# if e is a werkzeug InternalServerError then it may wrap the original exception. For more details see: # if e is a werkzeug InternalServerError then it may wrap the original exception. For more details see:
# https://flask.palletsprojects.com/en/1.1.x/errorhandling/?highlight=internalservererror#unhandled-exceptions # https://flask.palletsprojects.com/en/1.1.x/errorhandling/?highlight=internalservererror#unhandled-exceptions
e = getattr(e, "original_exception", e) e = getattr(e, "original_exception", e)
current_app.logger.exception(e, exc_info=True) current_app.logger.exception(e)
return jsonify(result="error", message="Internal server error"), 500 return jsonify(result="error", message="Internal server error"), 500
+1 -2
View File
@@ -1565,9 +1565,8 @@ class Notification(db.Model):
try: try:
return encryption.decrypt(self._personalisation) return encryption.decrypt(self._personalisation)
except EncryptionError: except EncryptionError:
current_app.logger.error( current_app.logger.exception(
"Error decrypting notification.personalisation, returning empty dict", "Error decrypting notification.personalisation, returning empty dict",
exc_info=True,
) )
return {} return {}
+1 -1
View File
@@ -117,7 +117,7 @@ def fetch_potential_service(inbound_number, provider_name):
if not has_inbound_sms_permissions(service.permissions): if not has_inbound_sms_permissions(service.permissions):
current_app.logger.error( current_app.logger.error(
'Service "{}" does not allow inbound SMS'.format(service.id), exc_info=True 'Service "{}" does not allow inbound SMS'.format(service.id)
) )
return False return False
+3 -4
View File
@@ -31,7 +31,7 @@ def sns_notification_handler(data, headers):
verify_message_type(message_type) verify_message_type(message_type)
except InvalidMessageTypeException: except InvalidMessageTypeException:
current_app.logger.exception( current_app.logger.exception(
f"Response headers: {headers}\nResponse data: {data}", exc_info=True f"Response headers: {headers}\nResponse data: {data}"
) )
raise InvalidRequest("SES-SNS callback failed: invalid message type", 400) raise InvalidRequest("SES-SNS callback failed: invalid message type", 400)
@@ -39,7 +39,7 @@ def sns_notification_handler(data, headers):
message = json.loads(data.decode("utf-8")) message = json.loads(data.decode("utf-8"))
except decoder.JSONDecodeError: except decoder.JSONDecodeError:
current_app.logger.exception( current_app.logger.exception(
f"Response headers: {headers}\nResponse data: {data}", exc_info=True f"Response headers: {headers}\nResponse data: {data}"
) )
raise InvalidRequest("SES-SNS callback failed: invalid JSON given", 400) raise InvalidRequest("SES-SNS callback failed: invalid JSON given", 400)
@@ -47,8 +47,7 @@ def sns_notification_handler(data, headers):
validate_sns_cert(message) validate_sns_cert(message)
except Exception: except Exception:
current_app.logger.error( current_app.logger.error(
"SES-SNS callback failed: validation failed with error: Signature validation failed", "SES-SNS callback failed: validation failed with error: Signature validation failed"
exc_info=True,
) )
raise InvalidRequest("SES-SNS callback failed: validation failed", 400) raise InvalidRequest("SES-SNS callback failed: validation failed", 400)
+1 -1
View File
@@ -46,7 +46,7 @@ def handle_integrity_error(exc):
""" """
Handle integrity errors caused by the unique constraint on ix_organization_name Handle integrity errors caused by the unique constraint on ix_organization_name
""" """
current_app.logger.exception("Handling integrity error", exc_info=True) current_app.logger.exception("Handling integrity error")
if "ix_organization_name" in str(exc): if "ix_organization_name" in str(exc):
return jsonify(result="error", message="Organization name already exists"), 400 return jsonify(result="error", message="Organization name already exists"), 400
if 'duplicate key value violates unique constraint "domain_pkey"' in str(exc): if 'duplicate key value violates unique constraint "domain_pkey"' in str(exc):
+2 -2
View File
@@ -136,7 +136,7 @@ def handle_integrity_error(exc):
), ),
400, 400,
) )
current_app.logger.exception(exc, exc_info=True) current_app.logger.exception(exc)
return jsonify(result="error", message="Internal server error"), 500 return jsonify(result="error", message="Internal server error"), 500
@@ -824,7 +824,7 @@ def update_guest_list(service_id):
try: try:
guest_list_objects = get_guest_list_objects(service_id, request.get_json()) guest_list_objects = get_guest_list_objects(service_id, request.get_json())
except ValueError as e: except ValueError as e:
current_app.logger.exception(e, exc_info=True) current_app.logger.exception(e)
dao_rollback() dao_rollback()
msg = "{} is not a valid email address or phone number".format(str(e)) msg = "{} is not a valid email address or phone number".format(str(e))
raise InvalidRequest(msg, 400) raise InvalidRequest(msg, 400)
+2 -8
View File
@@ -1,5 +1,4 @@
import json import json
import os
import uuid import uuid
from urllib.parse import urlencode from urllib.parse import urlencode
@@ -54,7 +53,7 @@ from app.user.users_schema import (
post_verify_code_schema, post_verify_code_schema,
post_verify_webauthn_schema, post_verify_webauthn_schema,
) )
from app.utils import hilite, url_with_token, utc_now from app.utils import debug_not_production, hilite, url_with_token, utc_now
from notifications_utils.recipients import is_us_phone_number, use_numeric_sender from notifications_utils.recipients import is_us_phone_number, use_numeric_sender
user_blueprint = Blueprint("user", __name__) user_blueprint = Blueprint("user", __name__)
@@ -69,7 +68,7 @@ def handle_integrity_error(exc):
if "ck_user_has_mobile_or_other_auth" in str(exc): if "ck_user_has_mobile_or_other_auth" in str(exc):
# we don't expect this to trip, so still log error # we don't expect this to trip, so still log error
current_app.logger.exception( current_app.logger.exception(
"Check constraint ck_user_has_mobile_or_other_auth triggered", exc_info=True "Check constraint ck_user_has_mobile_or_other_auth triggered"
) )
return ( return (
jsonify( jsonify(
@@ -589,11 +588,6 @@ def get_user_login_gov_user():
return jsonify(data=result) return jsonify(data=result)
def debug_not_production(msg):
if os.getenv("NOTIFY_ENVIRONMENT") not in ["production"]:
current_app.logger.info(msg)
@user_blueprint.route("/email", methods=["POST"]) @user_blueprint.route("/email", methods=["POST"])
def fetch_user_by_email(): def fetch_user_by_email():
try: try:
+7 -1
View File
@@ -1,6 +1,7 @@
import os
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from flask import url_for from flask import current_app, url_for
from sqlalchemy import func from sqlalchemy import func
from notifications_utils.template import HTMLEmailTemplate, SMSMessageTemplate from notifications_utils.template import HTMLEmailTemplate, SMSMessageTemplate
@@ -125,3 +126,8 @@ def naive_utcnow():
def utc_now(): def utc_now():
return naive_utcnow() return naive_utcnow()
def debug_not_production(msg):
if os.getenv("NOTIFY_ENVIRONMENT") not in ["production"]:
current_app.logger.info(msg)
@@ -0,0 +1,31 @@
# Adopting BackstopJS for Enhanced QA in Admin Project
Status: Accepted
Date: September 5th, 2024
### Context
We're looking to integrate BackstopJS, a visual regression testing tool, into our Admin UI project to improve QA and keep our UI consistent. This tool will help catch visual bugs early and make sure our design stays on track. We considered several options: deferring the integration, minimal integration with our current tools, full integration using Docker, and an optional testing setup. The goal is to find a balance between ease of use for developers and thorough testing while making sure the integration fits well with our current CI/CD pipeline.
### Decision
We decided to integrate BackstopJS as an optional part of our workflow. This means developers can run visual regression tests when they think it's needed, using specific Gulp commands. By doing this, we keep the process flexible and minimize friction for those who are new to the tool. We'll also provide clear documentation and training to help everyone get up to speed.
Once this is working well for folks locally, we'll begin incorporating these steps as an additional part of our CI/CD process and add them as a new separate job, similar to how end-to-end tests were added. We'll first add this in as an informational only run that simply reports the results but doesn't prevent any work from going through.
After we've had a bit of time to test the workflow and make sure everything is working as expected, we'll change the workflow to make it required. This will cause a PR, merge, or deploy to fail or not proceed if any regressions are detected, at which point someone will have to investigate and see if something was missed or a fix is needed for the test(s)/check(s) based on intentional changes.
### Consequences
With this decision, we make it easier for developers to start using BackstopJS without introducing a complicated library to them. This should help us catch more visual bugs and keep our UI consistent over time. The downside is that not everyone may run the tests regularly, which could lead to some missed issues. To counter this, documentation will be created to help developers understand how to best use BackstopJS. The initial setup will take some time, but since it matches the tools we already use, it shouldnt be too much of a hassle. Were also thinking about integrating BackstopJS into our CI/CD pipeline more fully in the future, so we wont have to rely on local environments as much.
### Author
@alexjanousekGSA
### Stakeholders
@ccostino
@stvnrlly
### Next Steps
- Start setting up BackstopJS with Gulp.
- Create documentation and training materials.
- Hold training sessions to introduce developers to BackstopJS.
- Keep an eye on how well the integration is working and get feedback from the team.
- Make adjustments as needed based on what we learn and begin implementing into CI/CD process.
+9 -8
View File
@@ -178,11 +178,12 @@ our ADRs in reverse chronological order so we have a convenient index of them.
This is the log of all of our ADRs in reverse chronological order (newest is up This is the log of all of our ADRs in reverse chronological order (newest is up
top!). top!).
| ADR | TITLE | CURRENT STATUS | IMPLEMENTED | LAST MODIFIED | | ADR | TITLE | CURRENT STATUS | IMPLEMENTED | LAST MODIFIED |
| :---: | :---: | :---: | :---: | :---: | |:------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------:|:--------------:|:-----------:|:-------------:|
| [ADR-0006](./0006-use-for-dependency-management.md) | [Use `poetry` for Dependency Management](./0006-use-for-dependency-management.md) | Accepted | Yes | 09/08/2023 | | [ADR-0009](./0009-adr-implement-backstopjs-to-improve-qa.md) | [Use backstopJS for QA Improvement within Admin Project](./0006-use-for-dependency-management.md) | Accepted | No | 08/27/2024 |
| [ADR-0005](./0005-agreement-data-model.md) | [Agreement info in data model](./0005-agreement-data-model.md) | Accepted | No | 07/05/2023 | | [ADR-0006](./0006-use-for-dependency-management.md) | [Use `poetry` for Dependency Management](./0006-use-for-dependency-management.md) | Accepted | Yes | 09/08/2023 |
| [ADR-0004](./0004-designing-pilot-content-visibility.md) | [Designing Pilot Content Visibility](./0004-designing-pilot-content-visibility.md) | Proposed | No | 06/20/2023 | | [ADR-0005](./0005-agreement-data-model.md) | [Agreement info in data model](./0005-agreement-data-model.md) | Accepted | No | 07/05/2023 |
| [ADR-0003](./0003-implementing-invite-expirations.md) | [Implementing User Invite Expirations](./0003-implementing-invite-expirations.md) | Accepted | No | 09/15/2023 | | [ADR-0004](./0004-designing-pilot-content-visibility.md) | [Designing Pilot Content Visibility](./0004-designing-pilot-content-visibility.md) | Proposed | No | 06/20/2023 |
| [ADR-0002](./0002-how-to-handle-timezones.md) | [Determine How to Handle Timezones in US Notify](./0002-how-to-handle-timezones.md) | Accepted | Yes | 06/15/2023 | | [ADR-0003](./0003-implementing-invite-expirations.md) | [Implementing User Invite Expirations](./0003-implementing-invite-expirations.md) | Accepted | No | 09/15/2023 |
| [ADR-0001](./0001-establishing-adrs-for-us-notify.md) | [Establishing ADRs for US Notify](./0001-establishing-adrs-for-us-notify.md) | Accepted | Yes | 06/15/2023 | | [ADR-0002](./0002-how-to-handle-timezones.md) | [Determine How to Handle Timezones in US Notify](./0002-how-to-handle-timezones.md) | Accepted | Yes | 06/15/2023 |
| [ADR-0001](./0001-establishing-adrs-for-us-notify.md) | [Establishing ADRs for US Notify](./0001-establishing-adrs-for-us-notify.md) | Accepted | Yes | 06/15/2023 |
@@ -166,7 +166,7 @@ class RedisClient:
def __handle_exception(self, e, raise_exception, operation, key_name): def __handle_exception(self, e, raise_exception, operation, key_name):
current_app.logger.exception( current_app.logger.exception(
"Redis error performing {} on {}".format(operation, key_name), exc_info=True "Redis error performing {} on {}".format(operation, key_name)
) )
if raise_exception: if raise_exception:
raise e raise e
+3 -3
View File
@@ -81,11 +81,11 @@ class ResponseHeaderMiddleware(object):
return self._app(environ, rewrite_response_headers) return self._app(environ, rewrite_response_headers)
except BaseException as be: # noqa except BaseException as be: # noqa
if "AuthError" in str(be): # notify-api-1135 if "AuthError" in str(be): # notify-api-1135
current_app.logger.error("AuthError", exc_info=True) current_app.logger.exception("AuthError")
elif "AttributeError" in str(be): # notify-api-1394 elif "AttributeError" in str(be): # notify-api-1394
current_app.logger.error("AttributeError", exc_info=True) current_app.logger.exception("AttributeError")
elif "MethodNotAllowed" in str(be): # notify-admin-1392 elif "MethodNotAllowed" in str(be): # notify-admin-1392
current_app.logger.error("MethodNotAllowed", exc_info=True) current_app.logger.exception("MethodNotAllowed")
else: else:
raise be raise be
+1 -1
View File
@@ -57,7 +57,7 @@ def s3upload(
try: try:
key.put(**put_args) key.put(**put_args)
except botocore.exceptions.ClientError as e: except botocore.exceptions.ClientError as e:
current_app.logger.error("Unable to upload file to S3 bucket", exc_info=True) current_app.logger.exception("Unable to upload file to S3 bucket")
raise e raise e
Generated
+34 -31
View File
@@ -986,38 +986,38 @@ files = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "43.0.0" version = "43.0.1"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"},
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"},
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"},
{file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"},
{file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"},
{file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"},
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"},
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"},
{file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"},
{file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"},
{file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"},
{file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"},
{file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"},
] ]
[package.dependencies] [package.dependencies]
@@ -1030,7 +1030,7 @@ nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"] pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"] sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"] ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"] test-randomorder = ["pytest-randomly"]
[[package]] [[package]]
@@ -2126,9 +2126,13 @@ files = [
{file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"}, {file = "lxml-5.2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:edcfa83e03370032a489430215c1e7783128808fd3e2e0a3225deee278585196"},
{file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"}, {file = "lxml-5.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:28bf95177400066596cdbcfc933312493799382879da504633d16cf60bba735b"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"}, {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a745cc98d504d5bd2c19b10c79c61c7c3df9222629f1b6210c0368177589fb8"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"}, {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b336b0416828022bfd5a2e3083e7f5ba54b96242159f83c7e3eebaec752f1716"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c"},
{file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"}, {file = "lxml-5.2.2-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:4bc6cb140a7a0ad1f7bc37e018d0ed690b7b6520ade518285dc3171f7a117905"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"}, {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:57f0a0bbc9868e10ebe874e9f129d2917750adf008fe7b9c1598c0fbbfdde6a6"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f"},
{file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"}, {file = "lxml-5.2.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:60499fe961b21264e17a471ec296dcbf4365fbea611bf9e303ab69db7159ce61"},
{file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"}, {file = "lxml-5.2.2-cp37-cp37m-win32.whl", hash = "sha256:d9b342c76003c6b9336a80efcc766748a333573abf9350f4094ee46b006ec18f"},
{file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"}, {file = "lxml-5.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b16db2770517b8799c79aa80f4053cd6f8b716f21f8aca962725a9565ce3ee40"},
@@ -2517,7 +2521,6 @@ files = [
{file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"},
{file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"},
{file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"},
{file = "msgpack-1.0.8-py3-none-any.whl", hash = "sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca"},
{file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"},
] ]
@@ -4800,4 +4803,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.12.2" python-versions = "^3.12.2"
content-hash = "213689af42ea6eb91a6a4baf3c3f41a8a69e75a022827ef18fe564645dd90762" content-hash = "42172a923e16c5b0965ab06f717d41e8491ee35f7be674091b38014c48b7a89e"
+1 -1
View File
@@ -62,7 +62,7 @@ shapely = "^2.0.5"
smartypants = "^2.0.1" smartypants = "^2.0.1"
mistune = "0.8.4" mistune = "0.8.4"
blinker = "^1.8.2" blinker = "^1.8.2"
cryptography = "^43.0.0" cryptography = "^43.0.1"
idna = "^3.7" idna = "^3.7"
jmespath = "^1.0.1" jmespath = "^1.0.1"
markupsafe = "^2.1.5" markupsafe = "^2.1.5"
+14
View File
@@ -5,6 +5,7 @@ import pytest
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from app.aws.s3 import ( from app.aws.s3 import (
cleanup_old_s3_objects,
file_exists, file_exists,
get_job_from_s3, get_job_from_s3,
get_personalisation_from_s3, get_personalisation_from_s3,
@@ -14,6 +15,7 @@ from app.aws.s3 import (
remove_s3_object, remove_s3_object,
) )
from app.utils import utc_now from app.utils import utc_now
from notifications_utils import aware_utcnow
default_access_key = getenv("CSV_AWS_ACCESS_KEY_ID") default_access_key = getenv("CSV_AWS_ACCESS_KEY_ID")
default_secret_key = getenv("CSV_AWS_SECRET_ACCESS_KEY") default_secret_key = getenv("CSV_AWS_SECRET_ACCESS_KEY")
@@ -28,6 +30,18 @@ def single_s3_object_stub(key="foo", last_modified=None):
} }
def test_cleanup_old_s3_objects(mocker):
mocker.patch("app.aws.s3.get_bucket_name", return_value="Bucket")
mock_s3_client = mocker.Mock()
mocker.patch("app.aws.s3.get_s3_client", return_value=mock_s3_client)
mock_s3_client.list_objects_v2.return_value = {
"Contents": [{"Key": "A", "LastModified": aware_utcnow()}]
}
cleanup_old_s3_objects()
mock_s3_client.list_objects_v2.assert_called_with(Bucket="Bucket")
def test_get_s3_file_makes_correct_call(notify_api, mocker): def test_get_s3_file_makes_correct_call(notify_api, mocker):
get_s3_mock = mocker.patch("app.aws.s3.get_s3_object") get_s3_mock = mocker.patch("app.aws.s3.get_s3_object")
get_s3_file( get_s3_file(
+2 -2
View File
@@ -81,8 +81,8 @@ def test_cronitor_does_nothing_if_name_not_recognised(notify_api, rmock, mocker)
): ):
assert successful_task() == 1 assert successful_task() == 1
mock_logger.error.assert_called_with( mock_logger.exception.assert_called_with(
"Cronitor enabled but task_name hello not found in environment", exc_info=True "Cronitor enabled but task_name hello not found in environment"
) )
assert rmock.called is False assert rmock.called is False