mirror of
https://github.com/GSA/notifications-api.git
synced 2026-08-22 23:36:08 -04:00
Compare commits
10 Commits
05-22-2025
...
add-second
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b742aec2b7 | ||
|
|
2457c13ef7 | ||
|
|
225d6fc090 | ||
|
|
f9e7885032 | ||
|
|
ef6b741dcb | ||
|
|
7bbb9c4552 | ||
|
|
1f56a37d56 | ||
|
|
02b7d662b0 | ||
|
|
a00b727c40 | ||
|
|
a2bce3c470 |
256
.ds.baseline
256
.ds.baseline
@@ -127,16 +127,262 @@
|
||||
}
|
||||
],
|
||||
"results": {
|
||||
".github/actions/deploy-proxy/action.yml": [
|
||||
".github/workflows/checks.yml": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": ".github/workflows/checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 28,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Basic Auth Credentials",
|
||||
"filename": ".github/workflows/checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 45,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
".github/workflows/daily_checks.yml": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": ".github/workflows/daily_checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 61,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Basic Auth Credentials",
|
||||
"filename": ".github/workflows/daily_checks.yml",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 77,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/enums.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "app/enums.py",
|
||||
"hashed_secret": "12322e07b94ee3c7cd65a2952ece441538b53eb3",
|
||||
"is_verified": false,
|
||||
"line_number": 123,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"app/notifications/receive_notifications.py": [
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "app/notifications/receive_notifications.py",
|
||||
"hashed_secret": "d70eab08607a4d05faa2d0d6647206599e9abc65",
|
||||
"is_verified": false,
|
||||
"line_number": 29,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"deploy-config/sandbox.yml": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "deploy-config/sandbox.yml",
|
||||
"hashed_secret": "113151dd10316fcb0d5507b6215d78e2f3fe9e54",
|
||||
"is_verified": false,
|
||||
"line_number": 11,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"sample.env": [
|
||||
{
|
||||
"type": "Basic Auth Credentials",
|
||||
"filename": "sample.env",
|
||||
"hashed_secret": "5b98cf4c3d794c8af1fcd7991e89cd4e52fb42a4",
|
||||
"is_verified": false,
|
||||
"line_number": 16,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/aws/test_s3.py": [
|
||||
{
|
||||
"type": "Hex High Entropy String",
|
||||
"filename": ".github/actions/deploy-proxy/action.yml",
|
||||
"hashed_secret": "a6c13f5da3788e8d654cd24001dc79a238723248",
|
||||
"filename": "tests/app/aws/test_s3.py",
|
||||
"hashed_secret": "67a74306b06d0c01624fe0d0249a570f4d093747",
|
||||
"is_verified": false,
|
||||
"line_number": 18,
|
||||
"line_number": 42,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/clients/test_document_download.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/clients/test_document_download.py",
|
||||
"hashed_secret": "3acfb2c2b433c0ea7ff107e33df91b18e52f960f",
|
||||
"is_verified": false,
|
||||
"line_number": 14,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/clients/test_performance_platform.py": [
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "tests/app/clients/test_performance_platform.py",
|
||||
"hashed_secret": "76bb66c38ac4046bf73cd4a2c35a2b0af94aeb61",
|
||||
"is_verified": false,
|
||||
"line_number": 84,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/dao/test_services_dao.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/dao/test_services_dao.py",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 289,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/dao/test_users_dao.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/dao/test_users_dao.py",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 69,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/dao/test_users_dao.py",
|
||||
"hashed_secret": "f2c57870308dc87f432e5912d4de6f8e322721ba",
|
||||
"is_verified": false,
|
||||
"line_number": 199,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/db.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/db.py",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 90,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/notifications/test_receive_notification.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/notifications/test_receive_notification.py",
|
||||
"hashed_secret": "913a73b565c8e2c8ed94497580f619397709b8b6",
|
||||
"is_verified": false,
|
||||
"line_number": 27,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "tests/app/notifications/test_receive_notification.py",
|
||||
"hashed_secret": "d70eab08607a4d05faa2d0d6647206599e9abc65",
|
||||
"is_verified": false,
|
||||
"line_number": 57,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/notifications/test_validators.py": [
|
||||
{
|
||||
"type": "Base64 High Entropy String",
|
||||
"filename": "tests/app/notifications/test_validators.py",
|
||||
"hashed_secret": "6c1a8443963d02d13ffe575a71abe19ea731fb66",
|
||||
"is_verified": false,
|
||||
"line_number": 672,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/service/test_rest.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/service/test_rest.py",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 1285,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/test_cloudfoundry_config.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/test_cloudfoundry_config.py",
|
||||
"hashed_secret": "e5e178db7317356946d13e5d2da037d39ac61c71",
|
||||
"is_verified": false,
|
||||
"line_number": 12,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Basic Auth Credentials",
|
||||
"filename": "tests/app/test_cloudfoundry_config.py",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 14,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/test_cloudfoundry_config.py",
|
||||
"hashed_secret": "cfd48edeb81ba7d48cbddcf1eeede25ba67057e8",
|
||||
"is_verified": false,
|
||||
"line_number": 33,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/app/user/test_rest.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/user/test_rest.py",
|
||||
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
|
||||
"is_verified": false,
|
||||
"line_number": 110,
|
||||
"is_secret": false
|
||||
},
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/app/user/test_rest.py",
|
||||
"hashed_secret": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
|
||||
"is_verified": false,
|
||||
"line_number": 864,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/antivirus/test_antivirus_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/antivirus/test_antivirus_client.py",
|
||||
"hashed_secret": "932b25270abe1301c22c709a19082dff07d469ff",
|
||||
"is_verified": false,
|
||||
"line_number": 16,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/encryption/test_encryption_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/encryption/test_encryption_client.py",
|
||||
"hashed_secret": "f1e923a9667de11be6a210849a8651c1bfd81605",
|
||||
"is_verified": false,
|
||||
"line_number": 13,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
"tests/notifications_utils/clients/zendesk/test_zendesk_client.py": [
|
||||
{
|
||||
"type": "Secret Keyword",
|
||||
"filename": "tests/notifications_utils/clients/zendesk/test_zendesk_client.py",
|
||||
"hashed_secret": "913a73b565c8e2c8ed94497580f619397709b8b6",
|
||||
"is_verified": false,
|
||||
"line_number": 16,
|
||||
"is_secret": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2025-05-12T16:45:34Z"
|
||||
"generated_at": "2025-02-27T21:09:45Z"
|
||||
}
|
||||
|
||||
2
.github/actions/deploy-proxy/action.yml
vendored
2
.github/actions/deploy-proxy/action.yml
vendored
@@ -15,7 +15,7 @@ inputs:
|
||||
default: https://github.com/GSA-TTS/cg-egress-proxy.git
|
||||
proxy_version:
|
||||
description: git ref to be deployed
|
||||
default: 1500c67157c1a7a6fbbda7a2de172b3d0a67e703
|
||||
default: main
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
|
||||
3
.github/actions/setup-project/action.yml
vendored
3
.github/actions/setup-project/action.yml
vendored
@@ -16,6 +16,3 @@ runs:
|
||||
- name: Install poetry
|
||||
shell: bash
|
||||
run: pip install poetry==1.8.5
|
||||
- name: Downgrade virtualenv to compatible version
|
||||
shell: bash
|
||||
run: pip install "virtualenv<20.30"
|
||||
|
||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -11,7 +11,3 @@ updates:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- "dependabot" # Custom label to identify Dependabot PRs
|
||||
assignees:
|
||||
- "alexjanousekGSA"
|
||||
reviewers:
|
||||
- "alexjanousekGSA"
|
||||
|
||||
6
.github/workflows/checks.yml
vendored
6
.github/workflows/checks.yml
vendored
@@ -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:
|
||||
|
||||
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.profile
2
.profile
@@ -6,4 +6,4 @@
|
||||
export http_proxy=$egress_proxy
|
||||
export https_proxy=$egress_proxy
|
||||
export NEW_RELIC_PROXY_HOST=$egress_proxy
|
||||
export no_proxy="apps.internal,s3-fips.us-gov-west-1.amazonaws.com"
|
||||
export no_proxy="apps.internal"
|
||||
|
||||
18
Makefile
18
Makefile
@@ -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
|
||||
|
||||
@@ -16,6 +15,7 @@ GIT_HOOKS_PATH ?= $(shell git config --global core.hooksPath || echo "")
|
||||
.PHONY: bootstrap
|
||||
bootstrap: ## Set up everything to run the app
|
||||
make generate-version-file
|
||||
poetry self add poetry-dotenv-plugin
|
||||
poetry lock --no-update
|
||||
poetry install --sync --no-root
|
||||
poetry run pre-commit install
|
||||
@@ -23,18 +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 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 .
|
||||
@@ -62,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
|
||||
|
||||
@@ -221,7 +221,7 @@ If you don't have a line for your `$PATH` environment variable, add it in like
|
||||
this, which will include the PostgreSQL binaries:
|
||||
|
||||
```
|
||||
export PATH="/opt/homebrew/opt/postgresql@15/bin:$PATH"
|
||||
export PATH="/opt/homebrew/opt/postgresql@15/bin:$PATH
|
||||
```
|
||||
|
||||
_NOTE: You don't want to overwrite your existing `$PATH` environment variable! Hence the reason why it is included on the end like this; paths are separated by a colon._
|
||||
@@ -507,10 +507,8 @@ instructions above for more details.
|
||||
- [Deploying to Production](./docs/all.md#-deploying-to-production)
|
||||
- [Smoke-testing the App](./docs/all.md#-smoke-testing-the-app)
|
||||
- [Configuration Management](./docs/all.md#-configuration-management)
|
||||
- [DNS and Domain Changes](./docs/all.md#-dns-and-domain-changes)
|
||||
- [Exporting daily scan results for compliance monitoring](./docs/all.md#exporting-daily-scan-results-for-compliance-monitoring)
|
||||
- [Reviewing daily scan results for compliance](./docs/all.md#reviewing-daily-scan-results-for-compliance)
|
||||
- [Rotating environment variable secrets](./docs/all.md#rotating-environment-variable-secrets)
|
||||
- [DNS Changes](./docs/all.md#-dns-changes)
|
||||
- [Exporting test results for compliance monitoring](./docs/all.md#exporting-test-results-for-compliance-monitoring)
|
||||
- [Known Gotchas](./docs/all.md#-known-gotchas)
|
||||
- [User Account Management](./docs/all.md#-user-account-management)
|
||||
- [SMS Phone Number Management](./docs/all.md#-sms-phone-number-management)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,10 +2,10 @@ import csv
|
||||
import datetime
|
||||
import re
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from io import StringIO
|
||||
|
||||
import botocore
|
||||
import eventlet
|
||||
from boto3 import Session
|
||||
from flask import current_app
|
||||
|
||||
@@ -24,15 +24,8 @@ s3_client = None
|
||||
s3_resource = None
|
||||
|
||||
|
||||
def get_service_id_from_key(key):
|
||||
key = key.replace("service-", "")
|
||||
key = key.split("/")
|
||||
key = key[0].replace("-notify", "")
|
||||
return key
|
||||
|
||||
|
||||
def set_job_cache(key, value):
|
||||
current_app.logger.debug(f"Setting {key} in the job_cache to {value}.")
|
||||
current_app.logger.debug(f"Setting {key} in the job_cache.")
|
||||
job_cache = current_app.config["job_cache"]
|
||||
job_cache[key] = (value, time.time() + 8 * 24 * 60 * 60)
|
||||
|
||||
@@ -43,7 +36,7 @@ def get_job_cache(key):
|
||||
if ret is None:
|
||||
current_app.logger.warning(f"Could not find {key} in the job_cache.")
|
||||
else:
|
||||
current_app.logger.debug(f"Got {key} from job_cache with value {ret}.")
|
||||
current_app.logger.debug(f"Got {key} from job_cache.")
|
||||
return ret
|
||||
|
||||
|
||||
@@ -80,7 +73,7 @@ def get_s3_client():
|
||||
aws_secret_access_key=secret_key,
|
||||
region_name=region,
|
||||
)
|
||||
s3_client = session.client("s3", config=AWS_CLIENT_CONFIG)
|
||||
s3_client = session.client("s3")
|
||||
return s3_client
|
||||
|
||||
|
||||
@@ -123,9 +116,9 @@ def list_s3_objects():
|
||||
)
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
current_app.logger.exception(
|
||||
f"An error occurred while regenerating cache #notify-debug-admin-1200: {str(e)}",
|
||||
"An error occurred while regenerating cache #notify-debug-admin-1200",
|
||||
)
|
||||
|
||||
|
||||
@@ -191,20 +184,18 @@ def read_s3_file(bucket_name, object_key, s3res):
|
||||
"""
|
||||
try:
|
||||
job_id = get_job_id_from_s3_object_key(object_key)
|
||||
service_id = get_service_id_from_key(object_key)
|
||||
|
||||
if get_job_cache(job_id) is None:
|
||||
job = (
|
||||
object = (
|
||||
s3res.Object(bucket_name, object_key)
|
||||
.get()["Body"]
|
||||
.read()
|
||||
.decode("utf-8")
|
||||
)
|
||||
set_job_cache(job_id, job)
|
||||
set_job_cache(f"{job_id}_phones", extract_phones(job, service_id, job_id))
|
||||
set_job_cache(job_id, object)
|
||||
set_job_cache(f"{job_id}_phones", extract_phones(object))
|
||||
set_job_cache(
|
||||
f"{job_id}_personalisation",
|
||||
extract_personalisation(job),
|
||||
extract_personalisation(object),
|
||||
)
|
||||
|
||||
except LookupError:
|
||||
@@ -224,21 +215,11 @@ def get_s3_files():
|
||||
current_app.logger.info(
|
||||
f"job_cache length before regen: {len_job_cache()} #notify-debug-admin-1200"
|
||||
)
|
||||
count = 0
|
||||
try:
|
||||
for object_key in object_keys:
|
||||
read_s3_file(bucket_name, object_key, s3res)
|
||||
count = count + 1
|
||||
eventlet.sleep(0.2)
|
||||
with ThreadPoolExecutor() as executor:
|
||||
executor.map(lambda key: read_s3_file(bucket_name, key, s3res), object_keys)
|
||||
except Exception:
|
||||
current_app.logger.exception(
|
||||
f"Trouble reading {object_key} which is # {count} during cache regeneration"
|
||||
)
|
||||
except OSError as e:
|
||||
current_app.logger.exception(
|
||||
f"Egress proxy issue reading {object_key} which is # {count}"
|
||||
)
|
||||
raise e
|
||||
current_app.logger.exception("Connection pool issue")
|
||||
|
||||
current_app.logger.info(
|
||||
f"job_cache length after regen: {len_job_cache()} #notify-debug-admin-1200"
|
||||
@@ -394,7 +375,7 @@ def get_job_from_s3(service_id, job_id):
|
||||
)
|
||||
retries += 1
|
||||
sleep_time = backoff_factor * (2**retries) # Exponential backoff
|
||||
eventlet.sleep(sleep_time)
|
||||
time.sleep(sleep_time)
|
||||
continue
|
||||
else:
|
||||
# Typically this is "NoSuchKey"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
from time import sleep
|
||||
|
||||
import eventlet
|
||||
from celery.signals import task_postrun
|
||||
from flask import current_app
|
||||
from requests import HTTPError, RequestException, request
|
||||
@@ -83,7 +83,7 @@ def process_job(job_id, sender_id=None):
|
||||
process_row(row, template, job, service, sender_id=sender_id)
|
||||
count = count + 1
|
||||
if count % 3 == 0:
|
||||
eventlet.sleep(1)
|
||||
sleep(1)
|
||||
|
||||
# End point/Exit point for message send flow.
|
||||
job_complete(job, start=start)
|
||||
|
||||
@@ -151,7 +151,7 @@ class AwsCloudwatchClient(Client):
|
||||
# result = temp_client.get_query_results(queryId=query_id)
|
||||
# if result['status'] == 'Complete':
|
||||
# break
|
||||
# eventlet.sleep(1)
|
||||
# time.sleep(1)
|
||||
|
||||
# delivery_receipts = []
|
||||
# for log in result['results']:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}")
|
||||
15
app/utils.py
15
app/utils.py
@@ -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}",
|
||||
)
|
||||
|
||||
165
docs/all.md
165
docs/all.md
@@ -53,10 +53,8 @@
|
||||
- [Smoke-testing the App](#-smoke-testing-the-app)
|
||||
- [Simulated bulk send testing](#-simulated-bulk-send-testing)
|
||||
- [Configuration Management](#-configuration-management)
|
||||
- [DNS and Domain Changes](#-dns-and-domain-changes)
|
||||
- [Exporting daily scan results for compliance monitoring](#exporting-daily-scan-results-for-compliance-monitoring)
|
||||
- [Reviewing daily scan results for compliance](#reviewing-daily-scan-results-for-compliance)
|
||||
- [Rotating environment variable secrets](#rotating-environment-variable-secrets)
|
||||
- [DNS Changes](#-dns-changes)
|
||||
- [Exporting test results for compliance monitoring](#exporting-test-results-for-compliance-monitoring)
|
||||
- [Known Gotchas](#-known-gotchas)
|
||||
- [User Account Management](#-user-account-management)
|
||||
- [SMS Phone Number Management](#-sms-phone-number-management)
|
||||
@@ -1070,7 +1068,7 @@ that the security of the system is maintained.
|
||||
1. [Smoke-testing the App](#smoke-testing)
|
||||
1. [Simulated bulk send testing](#simulated-bulk-send-testing)
|
||||
1. [Configuration Management](#cm)
|
||||
1. [DNS and Domain Changes](#dns)
|
||||
1. [DNS Changes](#dns)
|
||||
1. [Known Gotchas](#gotcha)
|
||||
1. [User Account Management](#ac)
|
||||
1. [SMS Phone Number Management](#phone-numbers)
|
||||
@@ -1241,43 +1239,17 @@ US_Notify Administrators are responsible for ensuring that remediations for vuln
|
||||
- Low - 180 days
|
||||
- Informational - 365 days (depending on the analysis of the issue)
|
||||
|
||||
## <a name="dns"></a> DNS and Domain Changes
|
||||
## <a name="dns"></a> DNS Changes
|
||||
|
||||
Notify.gov DNS records are maintained within [the GSA-TTS/dns repository](https://github.com/GSA-TTS/dns/blob/main/terraform/notify.gov.tf), and the domains and routes are managed directly in our Cloud.gov production space.
|
||||
Notify.gov DNS records are maintained within [the 18f/dns repository](https://github.com/18F/dns/blob/main/terraform/notify.gov.tf). To create new DNS records for notify.gov or any subdomains:
|
||||
|
||||
**Step 1: Make changes to the DNS records**
|
||||
1. Update the `notify.gov.tf` terraform to update oƒr create the new records within Route53 and push the branch to the 18f/dns repository.
|
||||
1. Open a PR.
|
||||
1. Verify that the plan output within circleci creates the records that you expect.
|
||||
1. Request a PR review from the 18F/tts-tech-portfolio team
|
||||
1. Once the PR is approved and merged, verify that the apply step happened correctly within [CircleCI](https://app.circleci.com/pipelines/github/18F/dns)
|
||||
|
||||
1. If you haven't already, clone a local copy of [the GSA-TTS/dns repository](https://github.com/GSA-TTS/dns).
|
||||
1. Create a new branch and update the [`notify.gov.tf`]((https://github.com/GSA-TTS/dns/blob/main/terraform/notify.gov.tf)) Terraform file to update, create, or remove DNS records within AWS Route 53.
|
||||
1. Open a PR in the repository and verify that the plan output within CircleCI makes the changes that you expect.
|
||||
1. Request a PR review from the `@tts-tech-operations` team within the GSA-TTS GitHub org.
|
||||
1. Once the PR is approved and merged, verify that the apply step happened correctly within [CircleCI](https://app.circleci.com/pipelines/github/GSA-TTS/dns).
|
||||
|
||||
**Step 2: Make changes to the domains and routes in Cloud.gov**
|
||||
|
||||
The domains and routes are managed via the [external domain service](https://www.cloud.gov/docs/services/external-domain-service/) within Cloud.gov.
|
||||
|
||||
If you're creating new domains:
|
||||
|
||||
1. Sign in to the `cf` CLI in your terminal and target the `notify-production` space.
|
||||
1. Create the new domain(s) with [`cf create-private-domain`](https://docs.cloudfoundry.org/devguide/deploy-apps/routes-domains.html#private-domains).
|
||||
1. Map the routes needed to the new domain(s) with [`cf map-route`](https://docs.cloudfoundry.org/devguide/deploy-apps/routes-domains.html#map-route).
|
||||
1. Update the service to account for the new domain(s): `cf update-service notify-admin-domain-production -c '{"domains": "example.gov,www.example.gov,..."}'` (make sure to list *all* domains that need to be accounted for, including any existing ones that you want to keep!).
|
||||
|
||||
If you're removing existing domains:
|
||||
|
||||
1. Sign in to the `cf` CLI in your terminal and target the `notify-production` space.
|
||||
1. Unmap the routes to the existing domain(s) with [`cf unmap-route`](https://docs.cloudfoundry.org/devguide/deploy-apps/routes-domains.html#unmap-route).
|
||||
1. Delete the existing domain(s) with [`cf delete-private-domain`](https://docs.cloudfoundry.org/devguide/deploy-apps/routes-domains.html#private-domains).
|
||||
1. Update the service to account for the deleted domain(s): `cf update-service notify-admin-domain-production -c '{"domains": "example.gov,www.example.gov,..."}'` (make sure to list *all* domains that need to be accounted for, including any existing ones that you want to keep!).
|
||||
|
||||
**Step 3: Redeploy or restage the Admin app:**
|
||||
|
||||
Restage or redeploy the `notify-admin-production` app. To restage, you can trigger the action in GitHub or run the command directly: `cf restage notify-admin-production --strategy rolling`.
|
||||
|
||||
Test that the changes took effect properly by going to the domain(s) that were adjusted and seeing if they resolve correctly and/or no longer resolve as expected. Note that this may take up to 72 hours, depending on how long it takes for the DNS changes to propogate.
|
||||
|
||||
## Exporting daily scan results for compliance monitoring
|
||||
## Exporting test results for compliance monitoring
|
||||
|
||||
- Head to https://github.com/GSA/notifications-api/actions/workflows/daily_checks.yml
|
||||
- Open the most recent scan (it should be today's)
|
||||
@@ -1289,115 +1261,16 @@ Test that the changes took effect properly by going to the domain(s) that were a
|
||||
- Rename to `api_static_scan_DATE.zip` and add it to 🔒 https://drive.google.com/drive/folders/1dSe9H7Ag_hLfi5hmQDB2ktWaDwWSf4_R
|
||||
- Repeat for https://github.com/GSA/notifications-admin/actions/workflows/daily_checks.yml
|
||||
|
||||
## Reviewing daily scan results for compliance
|
||||
## Rotating the DANGEROUS_SALT
|
||||
|
||||
To review the daily scan results and check for any new reported findings that need to be remediated, perform the following steps.
|
||||
|
||||
**For the API**
|
||||
|
||||
1. Go to the daily scan page: https://github.com/GSA/notifications-api/actions/workflows/daily_checks.yml
|
||||
1. Click on the latest scan (it should have run on the current day and be at the time)
|
||||
1. Scroll to the bottom and download the two artifacts: `bandit-report` and `zap_scan` - these are zip files that contain the full scan reports
|
||||
1. Click on the `pip-audit` job in the menu on the left of the screen
|
||||
1. Click on the `Run pypa/gh-action-pip-audit` step (the version number may change over time as it gets updated)
|
||||
1. Check that the output of the step doesn't show any new audit findings (the step and job will have failed if it did)
|
||||
1. Click on the `static-scan` job in the menu on the left of the screen
|
||||
1. Click on the `Run scan` step
|
||||
1. Check that the output of the step doesn't show any new scan findings (note: the step and job may still show as successful even if something was found)
|
||||
1. Click on the `dynamic-scan` job in the menu on the left of the screen
|
||||
1. Click on the `Run OWASP API Scan` step
|
||||
1. Check that the output of the step doesn't show any new scan findings (note: the step and job may still show as successful even if something was found)
|
||||
|
||||
Once you're done performing the steps above to gather all of the information, make a note of any new findings that need to be accounted for and remediated and create issues to track the work.
|
||||
|
||||
**For the Admin**
|
||||
|
||||
1. Go to the daily scan page: https://github.com/GSA/notifications-admin/actions/workflows/daily_checks.yml
|
||||
1. Click on the latest scan (it should have run on the current day and be at the time)
|
||||
1. Scroll to the bottom and download the artifact: `zap_scan` - this is a zip file that contains the full scan reports
|
||||
1. Click on the `dependency-audits` job in the menu on the left of the screen
|
||||
1. Click on the `Run pypa/gh-action-pip-audit` step (the version number may change over time as it gets updated)
|
||||
1. Check that the output of the step doesn't show any new audit findings (the step and job will have failed if it did)
|
||||
1. Click on the `Run npm audit` step
|
||||
1. Check that the output of the step doesn't show any new audit findings (the step and job will have failed if it did)
|
||||
1. Click on the `static-scan` job in the menu on the left of the screen
|
||||
1. Click on the `Run scan` step
|
||||
1. Check that the output of the step doesn't show any new scan findings (note: the step and job may still show as successful even if something was found)
|
||||
1. Click on the `dynamic-scan` job in the menu on the left of the screen
|
||||
1. Click on the `Run OWASP Full Scan` step
|
||||
1. Check that the output of the step doesn't show any new scan findings (note: the step and job may still show as successful even if something was found)
|
||||
|
||||
Once you're done performing the steps above to gather all of the information, make a note of any new findings that need to be accounted for and remediated and create issues to track the work.
|
||||
|
||||
|
||||
## Rotating environment variable secrets
|
||||
|
||||
There are a few different ways to handle rotating environment variable secrets, depending on what the secret is.
|
||||
|
||||
### Secret environment variables (set directly)
|
||||
|
||||
The `ADMIN_CLIENT_SECRET`, `DANGEROUS_SALT`, and `SECRET_KEY` environment variables are all generated random strings of characters. To make a new value for any of these environment variables, perform the following steps:
|
||||
|
||||
1. Start the API locally with the command `make run-procfile`
|
||||
1. In a separate terminal tab, navigate to the API project and run `poetry run flask command generate-salt` (this command is found in the [`app/commands.py` file](https://github.com/GSA/notifications-api/blob/main/app/commands.py#L1030-L1037))
|
||||
1. A random secret will appear in the tab, which you will use to update the value(s) in GitHub
|
||||
|
||||
Next, you'll need to go into GitHub for either the [API repo environment settings](https://github.com/GSA/notifications-api/settings/environments) or [Admin repo environment settings](https://github.com/GSA/notifications-admin/settings/environments). Once there you'll see a list of all of the environments; click into the one that you're looking to update and then find the corresponding environment that you need to update. Click on the pencil icon to the right of the environment variable name to edit the value, then paste in the value you generated with the previous steps.
|
||||
|
||||
**NOTE:** These values must match between the API and Admin environment variables per environment (meaning, if you change the Admin repo value for any of these values in any environment, the same variable for the API in the same environment must be changed to match it!).
|
||||
|
||||
The important thing is to use the same secret for Admin and API on each tier -- i.e. you only generate three secrets per environment.
|
||||
|
||||
**NOTE:** You may also have to update these values for Dependabot as well! To do this, go into GitHub and the navigate through `Settings -> Secrets and variables -> Dependabot`, which will take you to a special page to manage environment variables specifically for Dependabot. This is more necessary in the Admin repo because of the E2E tests.
|
||||
|
||||
### E2E environment variables (set directly)
|
||||
|
||||
See the [end-to-end testing section](#end-to-end-testing).
|
||||
|
||||
### Service bindings for Cloud.gov-managed services
|
||||
|
||||
For any Cloud.gov service instance that you need to rotate credentials for, you need to run the following commands:
|
||||
|
||||
1. `cf unbind-service <APP NAME> <SERVICE NAME>`
|
||||
1. `cf bind-service <APP NAME> <SERVICE NAME>`
|
||||
|
||||
Once you are done unbinding and re-binding all services you're looking to rotate credentials for, you need to restage or redeploy the application(s) for the changes to take effect. You can restage directly in the command line: `cf restage <APP NAME> --strategy rolling`
|
||||
|
||||
### Rotating New Relic API keys and licenses
|
||||
|
||||
To rotate New Relic API key, license key, and other credentials, you need access to New Relic. If you have access, sign in and then click on your name in the lower left. Click on `API keys` and you'll be taken to the management screen for all of the API keys. From there, perform these steps:
|
||||
|
||||
1. Create new versions of whichever key(s) you would like to rotate
|
||||
1. Update the corresponding environment variable(s) in GitHub for both the [API repo environment settings](https://github.com/GSA/notifications-api/settings/environments) and the [Admin repo environment settings](https://github.com/GSA/notifications-admin/settings/environments)
|
||||
1. Restage or redeploy the applications
|
||||
1. Once you confirm the new key(s) in New Relic are working, delete the old keys on the API Key management screen
|
||||
|
||||
### Terraform state bucket key rotation
|
||||
|
||||
To rotate the Terraform state bucket key, run these commands in the `api/terraform/bootstrap` directory of the API repo:
|
||||
|
||||
```sh
|
||||
# comment out prevent_destroy in terraform/bootstrap/main.tf
|
||||
# update username to create in run.sh and teardown-creds.sh
|
||||
$ ./run.sh plan -replace=cloudfoundry_service_key.bucket_creds
|
||||
$ ./run.sh apply -replace=cloudfoundry_service_key.bucket_creds
|
||||
```
|
||||
|
||||
Once that's done, copy the key generating to the staging, demo, and production environments of both the API and the Admin.
|
||||
|
||||
### Refreshing/rotating the Login.gov certificate
|
||||
|
||||
1. generate certificate: `openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.crt -nodes`
|
||||
1. 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**.
|
||||
1. use the same certificate for staging, demo, and production
|
||||
1. login to the login.gov partner app (https://portal.int.identitysandbox.gov)
|
||||
1. add the new certificate to the production version of Notify in the partner app (our partner app account has sandbox and production)
|
||||
1. Make a Zendesk support request for login.gov to push the new version of Notify (https://zendesk.login.gov)
|
||||
1. Do not delete the old certificate, because you need things to keep working until you complete the transition.
|
||||
1. When you receive an email from login.gov that the app has been pushed successfully, restage notify on the staging tier
|
||||
1. If staging works, you can restage demo and production
|
||||
1. 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.
|
||||
1. Start API locally `make run-procfile`
|
||||
2. In a separate terminal tab, navigate to the API project and run `poetry run flask command generate-salt`
|
||||
3. A random secret will appear in the tab
|
||||
4. Go to github->settings->secrets and variables->actions in the admin project and find the DANGEROUS_SALT secret for the admin project for staging. Open it and paste the result of #3 into the secret and save. Repeat for the API project, for staging.
|
||||
5. Repeat #3 and #4 but do it for demo
|
||||
6. Repeat #3 and #4 but do it for production
|
||||
|
||||
The important thing is to use the same secret for Admin and API on each tier--i.e. you only generate three secrets.
|
||||
|
||||
## <a name="gotcha"></a> Known Gotchas
|
||||
|
||||
|
||||
63
migrations/versions/0417_add_second_e2e_test_user.py
Normal file
63
migrations/versions/0417_add_second_e2e_test_user.py
Normal 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()
|
||||
@@ -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
120
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"socket.io-client": "^4.8.1"
|
||||
}
|
||||
}
|
||||
4737
poetry.lock
generated
4737
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,17 +8,17 @@ readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12.2"
|
||||
alembic = "==1.16.1"
|
||||
alembic = "==1.13.2"
|
||||
amqp = "==5.3.1"
|
||||
beautifulsoup4 = "==4.13.4"
|
||||
beautifulsoup4 = "==4.12.3"
|
||||
boto3 = "^1.34.150"
|
||||
botocore = "^1.34.159"
|
||||
cachetools = "==5.4.0"
|
||||
celery = {version = "==5.5.2", extras = ["redis"]}
|
||||
celery = {version = "==5.4.0", extras = ["redis"]}
|
||||
certifi = ">=2022.12.7"
|
||||
cffi = "==1.17.1"
|
||||
charset-normalizer = "^3.4.2"
|
||||
click = "==8.2.1"
|
||||
charset-normalizer = "^3.1.0"
|
||||
click = "==8.1.7"
|
||||
click-datetime = "==0.4.0"
|
||||
click-didyoumean = "==0.3.1"
|
||||
click-plugins = "==1.1.1"
|
||||
@@ -26,45 +26,45 @@ click-repl = "==0.3.0"
|
||||
deprecated = "==1.2.14"
|
||||
eventlet = "==0.36.1"
|
||||
expiringdict = "==1.2.2"
|
||||
flask = "~=3.1"
|
||||
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"]}
|
||||
iso8601 = "==2.1.0"
|
||||
jsonschema = {version = "==4.23.0", extras = ["format"]}
|
||||
lxml = "==5.4.0"
|
||||
lxml = "==5.3.1"
|
||||
marshmallow = "==3.26.1"
|
||||
marshmallow-sqlalchemy = "==1.0.0"
|
||||
newrelic = "*"
|
||||
notifications-python-client = "==10.0.1"
|
||||
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"
|
||||
python-dotenv = "==1.1.0"
|
||||
sqlalchemy = "==2.0.41"
|
||||
python-dotenv = "==1.0.1"
|
||||
sqlalchemy = "==2.0.31"
|
||||
werkzeug = "^3.0.6"
|
||||
faker = "^37.3.0"
|
||||
faker = "^26.0.0"
|
||||
async-timeout = "^5.0.1"
|
||||
bleach = "^6.1.0"
|
||||
geojson = "^3.2.0"
|
||||
numpy = "^2.2.6"
|
||||
numpy = "^2.2.3"
|
||||
ordered-set = "^4.1.0"
|
||||
phonenumbers = "^9.0.5"
|
||||
python-json-logger = "^3.3.0"
|
||||
regex = "^2024.11.6"
|
||||
shapely = "^2.1.1"
|
||||
phonenumbers = "^8.13.42"
|
||||
python-json-logger = "^2.0.7"
|
||||
regex = "^2024.7.24"
|
||||
shapely = "^2.0.5"
|
||||
smartypants = "^2.0.1"
|
||||
mistune = "^3.1.3"
|
||||
blinker = "^1.9.0"
|
||||
cryptography = "^45.0.2"
|
||||
cryptography = "^44.0.1"
|
||||
idna = "^3.7"
|
||||
jmespath = "^1.0.1"
|
||||
markupsafe = "^3.0.2"
|
||||
markupsafe = "^2.1.5"
|
||||
pycparser = "^2.22"
|
||||
python-dateutil = "^2.9.0.post0"
|
||||
pyyaml = "^6.0.2"
|
||||
@@ -76,8 +76,6 @@ itsdangerous = "^2.2.0"
|
||||
jinja2 = "^3.1.6"
|
||||
redis = "^5.0.8"
|
||||
requests = "^2.32.3"
|
||||
flask-socketio = "^5.5.1"
|
||||
virtualenv = "<20.30"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
@@ -85,14 +83,14 @@ awscli = "^1.33.32"
|
||||
bandit = "*"
|
||||
black = "^25.1.0"
|
||||
cloudfoundry-client = "*"
|
||||
exceptiongroup = "==1.3.0"
|
||||
flake8 = "^7.2.0"
|
||||
flake8-bugbear = "^24.12.12"
|
||||
exceptiongroup = "==1.2.2"
|
||||
flake8 = "^7.1.1"
|
||||
flake8-bugbear = "^24.1.17"
|
||||
freezegun = "^1.5.1"
|
||||
honcho = "*"
|
||||
isort = "^6.0.1"
|
||||
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"
|
||||
@@ -102,16 +100,12 @@ pytest-cov = "^6.1.1"
|
||||
pytest-xdist = "^3.5.0"
|
||||
radon = "^6.0.1"
|
||||
requests-mock = "^1.11.0"
|
||||
setuptools = "^80.8.0"
|
||||
setuptools = "^75.8.0"
|
||||
sqlalchemy-utils = "^0.41.2"
|
||||
vulture = "^2.10"
|
||||
detect-secrets = "^1.5.0"
|
||||
poetry-dotenv-plugin = "^0.2.0"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.plugins."poetry.application.plugin"]
|
||||
dotenv = "poetry_dotenv_plugin.plugin:PoetryDotenvPlugin"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from os import getenv
|
||||
from unittest.mock import ANY, MagicMock, Mock, call, patch
|
||||
from unittest.mock import MagicMock, Mock, call, patch
|
||||
|
||||
import botocore
|
||||
import pytest
|
||||
@@ -39,7 +39,7 @@ default_region = getenv("CSV_AWS_REGION")
|
||||
|
||||
def single_s3_object_stub(key="foo", last_modified=None):
|
||||
return {
|
||||
"ETag": '"d"',
|
||||
"ETag": '"d41d8cd98f00b204e9800998ecf8427e"',
|
||||
"Key": key,
|
||||
"LastModified": last_modified or utc_now(),
|
||||
}
|
||||
@@ -420,17 +420,29 @@ def test_get_s3_files_success(client, mocker):
|
||||
"CSV_UPLOAD_BUCKET": {"bucket": "test-bucket"},
|
||||
"job_cache": {},
|
||||
}
|
||||
mock_thread_pool_executor = mocker.patch("app.aws.s3.ThreadPoolExecutor")
|
||||
mock_read_s3_file = mocker.patch("app.aws.s3.read_s3_file")
|
||||
mock_list_s3_objects = mocker.patch("app.aws.s3.list_s3_objects")
|
||||
mock_get_s3_resource = mocker.patch("app.aws.s3.get_s3_resource")
|
||||
mock_list_s3_objects.return_value = ["file1.csv", "file2.csv"]
|
||||
mock_s3_resource = MagicMock()
|
||||
mock_get_s3_resource.return_value = mock_s3_resource
|
||||
mock_executor = MagicMock()
|
||||
|
||||
def mock_map(func, iterable):
|
||||
for item in iterable:
|
||||
func(item)
|
||||
|
||||
mock_executor.map.side_effect = mock_map
|
||||
mock_thread_pool_executor.return_value.__enter__.return_value = mock_executor
|
||||
|
||||
get_s3_files()
|
||||
|
||||
# mock_current_app.config.__getitem__.assert_called_once_with("CSV_UPLOAD_BUCKET")
|
||||
mock_list_s3_objects.assert_called_once()
|
||||
mock_thread_pool_executor.assert_called_once()
|
||||
|
||||
mock_executor.map.assert_called_once()
|
||||
|
||||
calls = [
|
||||
(("test-bucket", "file1.csv", mock_s3_resource),),
|
||||
@@ -461,7 +473,7 @@ def test_get_s3_client(mocker):
|
||||
mock_session.return_value.client.return_value = mock_s3_client
|
||||
result = get_s3_client()
|
||||
|
||||
mock_session.return_value.client.assert_called_once_with("s3", config=ANY)
|
||||
mock_session.return_value.client.assert_called_once_with("s3")
|
||||
assert result == mock_s3_client
|
||||
|
||||
|
||||
@@ -603,6 +615,15 @@ def test_get_s3_files_handles_exception(mocker):
|
||||
mock_read_s3_file = mocker.patch(
|
||||
"app.aws.s3.read_s3_file", side_effect=[None, Exception("exception here")]
|
||||
)
|
||||
|
||||
mock_thread_pool_executor = mocker.patch("app.aws.s3.ThreadPoolExecutor")
|
||||
mock_executor = mock_thread_pool_executor.return_value.__enter__.return_value
|
||||
|
||||
def mock_map(func, iterable):
|
||||
for item in iterable:
|
||||
func(item)
|
||||
|
||||
mock_executor.map.side_effect = mock_map
|
||||
get_s3_files()
|
||||
|
||||
calls = [
|
||||
@@ -611,6 +632,4 @@ def test_get_s3_files_handles_exception(mocker):
|
||||
]
|
||||
mock_read_s3_file.assert_has_calls(calls, any_order=True)
|
||||
|
||||
mock_current_app.logger.exception.assert_called_with(
|
||||
"Trouble reading file2.csv which is # 1 during cache regeneration"
|
||||
)
|
||||
mock_current_app.logger.exception.assert_called_with("Connection pool issue")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -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"),
|
||||
],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user