Compare commits

..

10 Commits

Author SHA1 Message Date
Andrew Shumway
b742aec2b7 Pull main 2025-04-09 10:36:41 -06:00
Andrew Shumway
2457c13ef7 Merge branch 'main' of https://github.com/GSA/notifications-api into add-second-e2e-user 2025-04-09 10:36:22 -06:00
Andrew Shumway
225d6fc090 Add more e2e variables to staging deploy workflow 2025-04-09 10:22:33 -06:00
Andrew Shumway
f9e7885032 Add second e2e variables to checks.yml 2025-04-09 10:15:34 -06:00
Andrew Shumway
ef6b741dcb Adjust migration and pull in main fix 2025-04-08 09:21:27 -06:00
Andrew Shumway
7bbb9c4552 Merge branch 'main' of https://github.com/GSA/notifications-api into add-second-e2e-user 2025-04-08 09:16:40 -06:00
Andrew Shumway
1f56a37d56 Pull in main and adjust name of migration 2025-03-19 13:41:12 -06:00
Andrew Shumway
02b7d662b0 Merge branch 'main' of https://github.com/GSA/notifications-api into add-second-e2e-user 2025-03-19 13:38:07 -06:00
Andrew Shumway
a00b727c40 Add auth type 2025-02-26 13:13:36 -07:00
Andrew Shumway
a2bce3c470 add second e2e test user 2025-02-26 11:48:15 -07:00
21 changed files with 1838 additions and 2573 deletions

View File

@@ -47,6 +47,8 @@ jobs:
NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD }}
NOTIFY_E2E_TEST_HTTP_AUTH_USER: ${{ secrets.NOTIFY_E2E_TEST_HTTP_AUTH_USER }}
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
NOTIFY_E2E_TEST_EMAIL_TWO: ${{ secrets.NOTIFY_E2E_TEST_EMAIL_TWO }}
NOTIFY_E2E_TEST_PASSWORD_TWO: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD_TWO }}
- name: Check imports alphabetized
run: poetry run isort --check-only ./app ./tests
@@ -62,6 +64,8 @@ jobs:
NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD }}
NOTIFY_E2E_TEST_HTTP_AUTH_USER: ${{ secrets.NOTIFY_E2E_TEST_HTTP_AUTH_USER }}
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
NOTIFY_E2E_TEST_EMAIL_TWO: ${{ secrets.NOTIFY_E2E_TEST_EMAIL_TWO }}
NOTIFY_E2E_TEST_PASSWORD_TWO: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD_TWO }}
- name: Check coverage threshold
# TODO get this back up to 95
run: poetry run coverage report -m --fail-under=93
@@ -132,6 +136,8 @@ jobs:
NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_HTTP_AUTH_PASSWORD }}
NOTIFY_E2E_TEST_HTTP_AUTH_USER: ${{ secrets.NOTIFY_E2E_TEST_HTTP_AUTH_USER }}
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
NOTIFY_E2E_TEST_EMAIL_TWO: ${{ secrets.NOTIFY_E2E_TEST_EMAIL_TWO }}
NOTIFY_E2E_TEST_PASSWORD_TWO: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD_TWO }}
- name: Run server
run: make run-flask &
env:

View File

@@ -61,6 +61,8 @@ jobs:
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
NOTIFY_E2E_TEST_EMAIL_TWO: ${{ secrets.NOTIFY_E2E_TEST_EMAIL_TWO }}
NOTIFY_E2E_TEST_PASSWORD_TWO: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD_TWO }}
LOGIN_DOT_GOV_REGISTRATION_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/set-up-your-profile&response_type=code&scope=openid+email&state=STATE"
with:
@@ -77,6 +79,8 @@ jobs:
--var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY"
--var NOTIFY_E2E_TEST_EMAIL="$NOTIFY_E2E_TEST_EMAIL"
--var NOTIFY_E2E_TEST_PASSWORD="$NOTIFY_E2E_TEST_PASSWORD"
--var NOTIFY_E2E_TEST_EMAIL_TWO: "$NOTIFY_E2E_TEST_EMAIL_TWO"
--var NOTIFY_E2E_TEST_PASSWORD_TWO: "$NOTIFY_E2E_TEST_PASSWORD_TWO"
--var LOGIN_DOT_GOV_REGISTRATION_URL="$LOGIN_DOT_GOV_REGISTRATION_URL"
--strategy rolling
@@ -89,6 +93,8 @@ jobs:
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
NOTIFY_E2E_TEST_EMAIL: ${{ secrets.NOTIFY_E2E_TEST_EMAIL }}
NOTIFY_E2E_TEST_PASSWORD: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD }}
NOTIFY_E2E_TEST_EMAIL_TWO: ${{ secrets.NOTIFY_E2E_TEST_EMAIL_TWO }}
NOTIFY_E2E_TEST_PASSWORD_TWO: ${{ secrets.NOTIFY_E2E_TEST_PASSWORD_TWO }}
LOGIN_DOT_GOV_REGISTRATION_URL: "https://secure.login.gov/openid_connect/authorize?acr_values=http%3A%2F%2Fidmanagement.gov%2Fns%2Fassurance%2Fial%2F1&client_id=urn:gov:gsa:openidconnect.profiles:sp:sso:gsa:notify-gov&nonce=NONCE&prompt=select_account&redirect_uri=https://notify-staging.app.cloud.gov/set-up-your-profile&response_type=code&scope=openid+email&state=STATE"
with:
cf_username: ${{ secrets.CLOUDGOV_USERNAME }}

View File

@@ -5,8 +5,7 @@ DATE = $(shell date +%Y-%m-%d:%H:%M:%S)
APP_VERSION_FILE = app/version.py
GIT_BRANCH ?= $(shell git symbolic-ref --short HEAD 2> /dev/null || echo "detached")
GIT_COMMIT ?= $(shell git rev-parse HEAD 2> /dev/null || echo "")
GIT_HOOKS_PATH ?= $(shell git config --global core.hooksPath || echo "")
GIT_COMMIT ?= $(shell git rev-parse HEAD)
## DEVELOPMENT
@@ -24,19 +23,6 @@ bootstrap: ## Set up everything to run the app
createdb test_notification_api || true
(poetry run flask db upgrade) || true
.PHONY: bootstrap-with-git-hooks
bootstrap-with-git-hooks: ## Sets everything up and accounts for pre-existing git hooks
make generate-version-file
poetry self add poetry-dotenv-plugin
poetry lock --no-update
poetry install --sync --no-root
git config --global --unset-all core.hooksPath
poetry run pre-commit install
git config --global core.hookspath "${GIT_HOOKS_PATH}"
createdb notification_api || true
createdb test_notification_api || true
(poetry run flask db upgrade) || true
.PHONY: bootstrap-with-docker
bootstrap-with-docker: ## Build the image to run the app in Docker
docker build -f docker/Dockerfile -t notifications-api .
@@ -64,7 +50,7 @@ too-complex:
poetry run radon cc ./app -a -nc
.PHONY: run-flask
run-flask:
run-flask: ## Run flask
poetry run newrelic-admin run-program flask run -p 6011 --host=0.0.0.0
.PHONY: run-celery

View File

@@ -13,7 +13,6 @@ from flask import current_app, g, has_request_context, jsonify, make_response, r
from flask.ctx import has_app_context
from flask_marshmallow import Marshmallow
from flask_migrate import Migrate
from flask_socketio import SocketIO
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy
from sqlalchemy import event
from werkzeug.exceptions import HTTPException as WerkzeugHTTPException
@@ -95,14 +94,6 @@ zendesk_client = ZendeskClient()
redis_store = RedisClient()
document_download_client = DocumentDownloadClient()
socketio = SocketIO(
cors_allowed_origins=[
config.Config.ADMIN_BASE_URL,
],
message_queue=config.Config.REDIS_URL,
logger=True,
engineio_logger=True,
)
notification_provider_clients = NotificationProviderClients()
@@ -120,11 +111,6 @@ def create_app(application):
application.config["NOTIFY_APP_NAME"] = application.name
init_app(application)
socketio.init_app(application)
from app.socket_handlers import register_socket_handlers
register_socket_handlers(socketio)
request_helper.init_app(application)
db.init_app(application)
migrate.init_app(application, db=db)

View File

@@ -48,10 +48,7 @@ class AwsSnsClient(SmsClient):
def send_sms(self, to, content, reference, sender=None, international=False):
matched = False
if "+" not in to:
to = f"+{to}"
for match in phonenumbers.PhoneNumberMatcher(to, None):
for match in phonenumbers.PhoneNumberMatcher(to, "US"):
matched = True
to = phonenumbers.format_number(
match.number, phonenumbers.PhoneNumberFormat.E164

View File

@@ -179,7 +179,6 @@ class Config(object):
S3_RESOURCE = session.resource("s3", config=AWS_CLIENT_CONFIG)
CELERY = {
"broker_connection_retry_on_startup": True,
"worker_max_tasks_per_child": 500,
"task_ignore_result": True,
"result_persistent": False,

View File

@@ -26,11 +26,9 @@ from werkzeug.datastructures import MultiDict
from app import create_uuid, db
from app.dao.dao_utils import autocommit
from app.dao.inbound_sms_dao import Pagination
from app.dao.jobs_dao import dao_get_job_by_id
from app.enums import KeyType, NotificationStatus, NotificationType
from app.models import FactNotificationStatus, Notification, NotificationHistory
from app.utils import (
emit_job_update_summary,
escape_special_characters,
get_midnight_in_utc,
midnight_n_days_ago,
@@ -897,19 +895,6 @@ def dao_update_delivery_receipts(receipts, delivered):
f"#loadtestperformance batch update query time: \
updated {len(receipts)} notification in {elapsed_time} ms"
)
job_ids = (
db.session.execute(
select(Notification.job_id).where(
Notification.message_id.in_(id_to_carrier.keys())
)
)
.scalars()
.all()
)
for job_id in set(job_ids):
job = dao_get_job_by_id(job_id)
emit_job_update_summary(job)
def dao_close_out_delivery_receipts():

View File

@@ -105,17 +105,6 @@ def send_sms_to_provider(notification):
# The future home of the validation is TBD
_experimentally_validate_phone_numbers(recipient)
# TODO current we allow US phone numbers to be uploaded without the country code (1)
# This will break certain international phone numbers (Norway, Denmark, East Timor)
# When we officially announce support for international numbers, US numbers must contain
# their country code.
recipient = str(recipient)
if len(recipient) == 10:
if os.getenv("NOTIFY_ENVIRONMENT") not in [
"test"
]: # we want to test intl support
recipient = f"1{recipient}"
sender_numbers = get_sender_numbers(notification)
if notification.reply_to_text not in sender_numbers:
raise ValueError(

View File

@@ -1,16 +0,0 @@
from flask import current_app, request
from flask_socketio import join_room, leave_room
def register_socket_handlers(socketio):
@socketio.on("join")
def on_join(data): # noqa: F401
room = data.get("room")
join_room(room)
current_app.logger.info(f"Socket {request.sid} joined room {room}")
@socketio.on("leave")
def on_leave(data): # noqa: F401
room = data.get("room")
leave_room(room)
current_app.logger.info(f"Socket {request.sid} left room {room}")

View File

@@ -131,18 +131,3 @@ def utc_now():
def debug_not_production(msg):
if os.getenv("NOTIFY_ENVIRONMENT") not in ["production"]:
current_app.logger.info(msg)
def emit_job_update_summary(job):
from app import socketio
current_app.logger.info(f"Emitting summary for job {job.id}")
socketio.emit(
"job_updated",
{
"job_id": str(job.id),
"job_status": job.job_status,
"notification_count": job.notification_count,
},
room=f"job-{job.id}",
)

View File

@@ -1507,19 +1507,3 @@ Note: better to search on space 'notify-production' rather than specifically for
#notify-admin-1505 (general login issues)
#notify-admin-1701 (wrong sender phone number)
#notify-admin-1859 (job is created with created_at being the wrong time)
### refreshing the login.gov certificate
1. generate certificate: `openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.crt -nodes`
2. update the github secrets for staging, demo, production (contents of key.pem go in LOGIN_PEM and contents of cert.crt in LOGIN_PUB).
DO NOT RESTAGE YET.
3. use the same certificate for staging, demo, and production
4. login to the login.gov partner app (https://portal.int.identitysandbox.gov)
5. add the new certificate to the production version of Notify in the partner app (our partner app account has sandbox and production)
6. Make a Zendesk support request for login.gov to push the new version of Notify (https://zendesk.login.gov)
7. Do not delete the old certificate, because you need things to keep working until you complete the transition.
8. When you receive an email from login.gov that the app has been pushed successfully, restage notify on the staging tier
9. If staging works, you can restage demo and production
10. Delete the old certificate in the partner app, send another zendesk request to push again. This is best practice but a lower
priority, because certificates eventually expire anyway and we have changed the certificate in github secrets, so the old cert is
no longer relevant.

View File

@@ -0,0 +1,63 @@
import datetime
import os
import uuid
import sqlalchemy as sa
from alembic import op
from app import db
from app.dao.users_dao import get_user_by_email
from app.models import User
from app.utils import utc_now
from app.enums import AuthType
revision = "0417_add_second_e2e_test_user"
down_revision = "0416_readd_e2e_test_user"
def upgrade():
email_address = os.getenv("NOTIFY_E2E_TEST_EMAIL_TWO")
password = os.getenv("NOTIFY_E2E_TEST_PASSWORD_TWO")
if not email_address or not password:
raise ValueError(
"Required variables [NOTIFY_E2E_TEST_EMAIL_TWO] and [NOTIFY_E2E_TEST_PASSWORD_TWO] missing!"
)
name = f"e2e_test_user_{uuid.uuid4()}"
data = {
"id": uuid.uuid4(),
"name": name,
"email_address": email_address,
"password": password,
"mobile_number": "+12025555555",
"state": "active",
"created_at": utc_now(),
"password_changed_at": utc_now(),
"failed_login_count": 0,
"platform_admin": "f",
"email_access_validated_at": utc_now(),
"auth_type": AuthType.SMS,
}
conn = op.get_bind()
# delete the old user because
delete_sql = f"""
delete from users where email_address='{email_address}'
"""
insert_sql = """
insert into users (id, name, email_address, _password, mobile_number, state, created_at, password_changed_at, failed_login_count, platform_admin, email_access_validated_at, auth_type)
values (:id, :name, :email_address, :password, :mobile_number, :state, :created_at, :password_changed_at, :failed_login_count, :platform_admin, :email_access_validated_at, :auth_type)
"""
conn.execute(sa.text(delete_sql))
conn.execute(sa.text(insert_sql), data)
def downgrade():
email_address = os.getenv("NOTIFY_E2E_TEST_EMAIL_TWO")
user_to_delete = get_user_by_email(email_address)
if not user_to_delete:
return
db.session.remove(user_to_delete)
db.session.commit()

View File

@@ -628,6 +628,8 @@ def validate_phone_number(number, international=False):
try:
parsed = phonenumbers.parse(number, None)
if parsed.country_code != 1:
raise InvalidPhoneError("Invalid country code")
number = f"{parsed.country_code}{parsed.national_number}"
if len(number) < 8:
raise InvalidPhoneError("Not enough digits")

120
package-lock.json generated
View File

@@ -1,120 +0,0 @@
{
"name": "notifications-api",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"socket.io-client": "^4.8.1"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
}
}
}

View File

@@ -1,5 +0,0 @@
{
"dependencies": {
"socket.io-client": "^4.8.1"
}
}

3873
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,19 +17,19 @@ cachetools = "==5.4.0"
celery = {version = "==5.4.0", extras = ["redis"]}
certifi = ">=2022.12.7"
cffi = "==1.17.1"
charset-normalizer = "^3.4.1"
click = "==8.1.8"
charset-normalizer = "^3.1.0"
click = "==8.1.7"
click-datetime = "==0.4.0"
click-didyoumean = "==0.3.1"
click-plugins = "==1.1.1"
click-repl = "==0.3.0"
deprecated = "==1.2.14"
eventlet = "==0.39.1"
eventlet = "==0.36.1"
expiringdict = "==1.2.2"
flask = "~=3.0"
flask-bcrypt = "==1.0.1"
flask-marshmallow = "==1.2.1"
flask-migrate = "==4.1.0"
flask-migrate = "==4.0.7"
flask-redis = "==0.4.0"
flask-sqlalchemy = "==3.1.1"
gunicorn = {version = "==23.0.0", extras = ["eventlet"]}
@@ -41,7 +41,7 @@ marshmallow-sqlalchemy = "==1.0.0"
newrelic = "*"
notifications-python-client = "==10.0.0"
oscrypto = { git = "https://github.com/wbond/oscrypto.git", rev = "1547f53" }
packaging = "==25.0"
packaging = "==24.2"
poetry-dotenv-plugin = "==0.2.0"
psycopg2-binary = "==2.9.9"
pyjwt = "==2.10.1"
@@ -52,11 +52,11 @@ faker = "^26.0.0"
async-timeout = "^5.0.1"
bleach = "^6.1.0"
geojson = "^3.2.0"
numpy = "^2.2.5"
numpy = "^2.2.3"
ordered-set = "^4.1.0"
phonenumbers = "^8.13.42"
python-json-logger = "^3.3.0"
regex = "^2024.11.6"
python-json-logger = "^2.0.7"
regex = "^2024.7.24"
shapely = "^2.0.5"
smartypants = "^2.0.1"
mistune = "^3.1.3"
@@ -76,7 +76,6 @@ itsdangerous = "^2.2.0"
jinja2 = "^3.1.6"
redis = "^5.0.8"
requests = "^2.32.3"
flask-socketio = "^5.5.1"
[tool.poetry.group.dev.dependencies]
@@ -85,13 +84,13 @@ bandit = "*"
black = "^25.1.0"
cloudfoundry-client = "*"
exceptiongroup = "==1.2.2"
flake8 = "^7.2.0"
flake8-bugbear = "^24.12.12"
flake8 = "^7.1.1"
flake8-bugbear = "^24.1.17"
freezegun = "^1.5.1"
honcho = "*"
isort = "^5.13.2"
jinja2-cli = {version = "==0.8.2", extras = ["yaml"]}
moto = "==5.1.4"
moto = "==5.1.0"
pip-audit = "*"
pre-commit = "^4.2.0"
pytest = "^8.3.2"

View File

@@ -5,7 +5,7 @@ from app import aws_sns_client
def test_send_sms_successful_returns_aws_sns_response(notify_api, mocker):
boto_mock = mocker.patch.object(aws_sns_client, "_client", create=True)
to = "16135555555"
to = "6135555555"
content = reference = "foo"
with notify_api.app_context():
aws_sns_client.send_sms(to, content, reference)

View File

@@ -2029,23 +2029,12 @@ def test_update_delivery_receipts(mocker):
mock_update.where.return_value = mock_where
mock_where.values.return_value = mock_values
FakeJob = type(
"FakeJob",
(object,),
{"id": "job-123", "notification_count": 5, "job_status": "delivered"},
)
fake_result = MagicMock()
fake_result.scalars.return_value.all.return_value = ["job-1", "job-2"]
fake_result.scalars.return_value.one.return_value = FakeJob()
mock_session.execute.side_effect = lambda *args, **kwargs: fake_result
mock_session.execute.return_value = None
with patch("app.dao.notifications_dao.update", return_value=mock_update):
dao_update_delivery_receipts(receipts, delivered)
mock_update.where.assert_called_once()
mock_where.values.assert_called_once()
mock_session.execute.assert_any_call(mock_values)
assert mock_session.execute.call_count == 4
mock_session.execute.assert_called_once_with(mock_values)
mock_session.commit.assert_called_once()
args, kwargs = mock_where.values.call_args

View File

@@ -628,8 +628,6 @@ def test_bad_or_missing_data(
assert recipients.has_errors is True
# TODO, original test for number one had {0, 1, 2}, but it has morphed to {0, 1}
# Is +447900123 legit or not? What changed?
@pytest.mark.parametrize(
("file_contents", "rows_with_bad_recipients"),
[
@@ -640,7 +638,7 @@ def test_bad_or_missing_data(
1234
+447900123
""",
{0, 1},
{0, 1, 2},
),
(
"""
@@ -649,7 +647,7 @@ def test_bad_or_missing_data(
+12022340104, USA
+23051234567, Mauritius
""",
set(),
{2},
),
],
)

View File

@@ -24,16 +24,21 @@ valid_us_phone_numbers = [
"(202) 555-0104",
]
# TODO
# International phone number tests are commented out as a result of issue #943 in notifications-admin. We are
# deliberately eliminating the ability to send to numbers outside of country code 1. These tests should
# be removed at some point when we are sure we are never going to support international numbers
valid_international_phone_numbers = [
"+71234567890", # Russia
"+447123456789", # UK
"+4407123456789", # UK
"+4407123 456789", # UK
"+4407123-456-789", # UK
"+23051234567", # Mauritius,
"+682 12345", # Cook islands
"+3312345678",
"+9-2345-12345-12345", # 15 digits
# "+71234567890", # Russia
# "+447123456789", # UK
# "+4407123456789", # UK
# "+4407123 456789", # UK
# "+4407123-456-789", # UK
# "+23051234567", # Mauritius,
# "+682 12345", # Cook islands
# "+3312345678",
# "+9-2345-12345-12345", # 15 digits
]
@@ -80,7 +85,7 @@ invalid_us_phone_numbers = sum(
invalid_phone_numbers = [
("+80233456789", "Not a valid country prefix"),
("1234567", "Not enough digits"),
("+682 1234", "Not enough digits"), # Cook Islands phone numbers are 5 digits
("+682 1234", "Invalid country code"), # Cook Islands phone numbers can be 5 digits
("+12345 12345 12345 6", "Too many digits"),
]
@@ -151,46 +156,46 @@ def test_detect_us_phone_numbers(phone_number):
@pytest.mark.parametrize(
("phone_number", "expected_info"),
[
(
"+4407900900123",
international_phone_info(
international=True,
country_prefix="44", # UK
billable_units=1,
),
),
(
"+4407700900123",
international_phone_info(
international=True,
country_prefix="44", # Number in TV range
billable_units=1,
),
),
(
"+4407700800123",
international_phone_info(
international=True,
country_prefix="44", # UK Crown dependency, so prefix same as UK
billable_units=1,
),
),
( #
"+20-12-1234-1234",
international_phone_info(
international=True,
country_prefix="20", # Egypt
billable_units=1,
),
),
(
"+201212341234",
international_phone_info(
international=True,
country_prefix="20", # Egypt
billable_units=1,
),
),
# (
# "+4407900900123",
# international_phone_info(
# international=True,
# country_prefix="44", # UK
# billable_units=1,
# ),
# ),
# (
# "+4407700900123",
# international_phone_info(
# international=True,
# country_prefix="44", # Number in TV range
# billable_units=1,
# ),
# ),
# (
# "+4407700800123",
# international_phone_info(
# international=True,
# country_prefix="44", # UK Crown dependency, so prefix same as UK
# billable_units=1,
# ),
# ),
# ( #
# "+20-12-1234-1234",
# international_phone_info(
# international=True,
# country_prefix="20", # Egypt
# billable_units=1,
# ),
# ),
# (
# "+201212341234",
# international_phone_info(
# international=True,
# country_prefix="20", # Egypt
# billable_units=1,
# ),
# ),
(
"+1 664-491-3434",
international_phone_info(
@@ -199,14 +204,14 @@ def test_detect_us_phone_numbers(phone_number):
billable_units=1,
),
),
(
"+71234567890",
international_phone_info(
international=True,
country_prefix="7", # Russia
billable_units=1,
),
),
# (
# "+71234567890",
# international_phone_info(
# international=True,
# country_prefix="7", # Russia
# billable_units=1,
# ),
# ),
(
"1-202-555-0104",
international_phone_info(
@@ -223,14 +228,14 @@ def test_detect_us_phone_numbers(phone_number):
billable_units=1,
),
),
(
"+23051234567",
international_phone_info(
international=True,
country_prefix="230", # Mauritius
billable_units=1,
),
),
# (
# "+23051234567",
# international_phone_info(
# international=True,
# country_prefix="230", # Mauritius
# billable_units=1,
# ),
# ),
],
)
def test_get_international_info(phone_number, expected_info):
@@ -283,11 +288,11 @@ def test_valid_us_phone_number_can_be_formatted_consistently(phone_number):
@pytest.mark.parametrize(
("phone_number", "expected_formatted"),
[
("+44071234567890", "+4471234567890"),
# ("+44071234567890", "+4471234567890"),
("1-202-555-0104", "+12025550104"),
("+12025550104", "+12025550104"),
("12025550104", "+12025550104"),
("+23051234567", "+23051234567"),
# ("+23051234567", "+23051234567"),
],
)
def test_valid_international_phone_number_can_be_formatted_consistently(
@@ -363,17 +368,17 @@ def test_validates_against_guestlist_of_phone_numbers(phone_number):
)
@pytest.mark.parametrize(
"recipient_number, allowlist_number",
[
["+4407123-456-789", "+4407123456789"],
["+4407123456789", "+4407123-456-789"],
],
)
def test_validates_against_guestlist_of_international_phone_numbers(
recipient_number, allowlist_number
):
assert allowed_to_send_to(recipient_number, [allowlist_number])
# @pytest.mark.parametrize(
# "recipient_number, allowlist_number",
# [
# ["+4407123-456-789", "+4407123456789"],
# ["+4407123456789", "+4407123-456-789"],
# ],
# )
# def test_validates_against_guestlist_of_international_phone_numbers(
# recipient_number, allowlist_number
# ):
# assert allowed_to_send_to(recipient_number, [allowlist_number])
@pytest.mark.parametrize("email_address", valid_email_addresses)
@@ -383,21 +388,19 @@ def test_validates_against_guestlist_of_email_addresses(email_address):
)
# TODO something wrong with formatting Egyptian numbers, doesn't seem
# like this would affect sendability need to confirm with AWS simulated numbers.
@pytest.mark.parametrize(
("phone_number", "expected_formatted"),
[
("+4407900900123", "+44 7900 900123"), # UK
("+44(0)7900900123", "+44 7900 900123"), # UK
("+447900900123", "+44 7900 900123"), # UK
# ("+4407900900123", "+44 7900 900123"), # UK
# ("+44(0)7900900123", "+44 7900 900123"), # UK
# ("+447900900123", "+44 7900 900123"), # UK
# ("+20-12-1234-1234", "+20 121 234 1234"), # Egypt
# ("+201212341234", "+20 121 234 1234"), # Egypt
("+1 664 491-3434", "+1 664-491-3434"), # Montserrat
("+7 499 1231212", "+7 499 123-12-12"), # Moscow (Russia)
# ("+7 499 1231212", "+7 499 123-12-12"), # Moscow (Russia)
("1-202-555-0104", "(202) 555-0104"), # Washington DC (USA)
("+23051234567", "+230 5123 4567"), # Mauritius
("+33(0)1 12345678", "+33 1 12 34 56 78"), # Paris (France)
# ("+23051234567", "+230 5123 4567"), # Mauritius
# ("+33(0)1 12345678", "+33 1 12 34 56 78"), # Paris (France)
],
)
def test_format_us_and_international_phone_numbers(phone_number, expected_formatted):
@@ -414,7 +417,7 @@ def test_format_us_and_international_phone_numbers(phone_number, expected_format
(None, ""),
("foo", "foo"),
("TeSt@ExAmPl3.com", "test@exampl3.com"),
("+4407900 900 123", "+447900900123"),
# ("+4407900 900 123", "+447900900123"),
("+1 800 555 5555", "+18005555555"),
],
)