diff --git a/.github/actions/deploy-proxy/action.yml b/.github/actions/deploy-proxy/action.yml index 2c1f4e882..13a773fa0 100644 --- a/.github/actions/deploy-proxy/action.yml +++ b/.github/actions/deploy-proxy/action.yml @@ -9,10 +9,10 @@ inputs: required: true proxy_repo: description: git repo for cg-egress-proxy - default: https://github.com/GSA/cg-egress-proxy.git + default: https://github.com/rahearn/cg-egress-proxy.git proxy_version: description: git ref to be deployed - default: main + default: new-relic-connection runs: using: composite steps: diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9143504a4..c5f2a3d0c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -8,10 +8,11 @@ permissions: env: DEBUG: True NOTIFY_ENVIRONMENT: test + NEW_RELIC_CONFIG_FILE: newrelic.ini + NEW_RELIC_ENVIRONMENT: test FLASK_APP: application.py FLASK_ENV: development WERKZEUG_DEBUG_PIN: off - NOTIFY_EMAIL_DOMAIN: dispostable.com REDIS_ENABLED: 0 AWS_REGION: us-west-2 AWS_PINPOINT_REGION: us-west-2 @@ -53,6 +54,21 @@ jobs: env: SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api + validate-new-relic-config: + runs-on: ubuntu-latest + environment: staging + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup-project + - name: Install pipenv packages + run: pipenv install --dev + - name: Validate NewRelic config + env: + NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }} + # Need to set a NEW_RELIC_ENVIRONMENT with monitor_mode: true + NEW_RELIC_ENVIRONMENT: staging + run: pipenv run newrelic-admin validate-config $NEW_RELIC_CONFIG_FILE + pip-audit: runs-on: ubuntu-latest steps: @@ -60,7 +76,7 @@ jobs: - uses: ./.github/actions/setup-project - name: Create requirements.txt run: pipenv requirements > requirements.txt - - uses: trailofbits/gh-action-pip-audit@v1.0.0 + - uses: pypa/gh-action-pip-audit@v1.0.4 with: inputs: requirements.txt ignore-vulns: PYSEC-2022-237 diff --git a/.github/workflows/daily_checks.yml b/.github/workflows/daily_checks.yml index c0b981b7c..e4968f723 100644 --- a/.github/workflows/daily_checks.yml +++ b/.github/workflows/daily_checks.yml @@ -12,6 +12,8 @@ permissions: env: DEBUG: True NOTIFY_ENVIRONMENT: test + NEW_RELIC_CONFIG_FILE: newrelic.ini + NEW_RELIC_ENVIRONMENT: test FLASK_APP: application.py FLASK_ENV: development WERKZEUG_DEBUG_PIN: off @@ -29,7 +31,7 @@ jobs: - uses: ./.github/actions/setup-project - name: Create requirements.txt run: pipenv requirements > requirements.txt - - uses: trailofbits/gh-action-pip-audit@v1.0.0 + - uses: pypa/gh-action-pip-audit@v1.0.4 with: inputs: requirements.txt ignore-vulns: PYSEC-2022-237 diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index ac800562b..a474ed8be 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -53,6 +53,7 @@ jobs: ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }} with: cf_username: ${{ secrets.CLOUDGOV_USERNAME }} cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} @@ -65,6 +66,7 @@ jobs: --var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET" --var AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" --var AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" + --var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY" - name: Check for changes to egress config id: changed-egress-config diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9349850df..70a1c5c2c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -58,6 +58,7 @@ jobs: ADMIN_CLIENT_SECRET: ${{ secrets.ADMIN_CLIENT_SECRET }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }} with: cf_username: ${{ secrets.CLOUDGOV_USERNAME }} cf_password: ${{ secrets.CLOUDGOV_PASSWORD }} @@ -70,6 +71,7 @@ jobs: --var ADMIN_CLIENT_SECRET="$ADMIN_CLIENT_SECRET" --var AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" --var AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" + --var NEW_RELIC_LICENSE_KEY="$NEW_RELIC_LICENSE_KEY" - name: Check for changes to egress config id: changed-egress-config diff --git a/.profile b/.profile index 9960f33a2..1c1f98b4b 100644 --- a/.profile +++ b/.profile @@ -4,3 +4,4 @@ ## export https_proxy=$egress_proxy +export NEW_RELIC_PROXY_HOST=$egress_proxy diff --git a/Makefile b/Makefile index 9ced0aaa5..e09fba6e9 100644 --- a/Makefile +++ b/Makefile @@ -26,12 +26,12 @@ run-procfile: .PHONY: run-flask run-flask: ## Run flask - pipenv run flask run -p 6011 --host=0.0.0.0 + pipenv run newrelic-admin run-program flask run -p 6011 --host=0.0.0.0 .PHONY: run-celery run-celery: ## Run celery, TODO remove purge for staging/prod pipenv run celery -A run_celery.notify_celery purge -f - pipenv run celery \ + pipenv run newrelic-admin run-program celery \ -A run_celery.notify_celery worker \ --pidfile="/tmp/celery.pid" \ --loglevel=INFO \ @@ -52,6 +52,7 @@ generate-version-file: ## Generates the app version file @echo -e "__git_commit__ = \"${GIT_COMMIT}\"\n__time__ = \"${DATE}\"" > ${APP_VERSION_FILE} .PHONY: test +test: export NEW_RELIC_ENVIRONMENT=test test: ## Run tests pipenv run flake8 . pipenv run isort --check-only ./app ./tests @@ -66,8 +67,8 @@ freeze-requirements: ## Pin all requirements including sub dependencies into req audit: pipenv requirements > requirements.txt pipenv requirements --dev > requirements_for_test.txt - pipenv run pip-audit -r requirements.txt -l --ignore-vuln PYSEC-2022-237 - -pipenv run pip-audit -r requirements_for_test.txt -l + pipenv run pip-audit -r requirements.txt --ignore-vuln PYSEC-2022-237 + -pipenv run pip-audit -r requirements_for_test.txt .PHONY: static-scan static-scan: diff --git a/Pipfile b/Pipfile index dc8437e5f..eee14c122 100644 --- a/Pipfile +++ b/Pipfile @@ -20,7 +20,7 @@ boto3 = "==1.23.8" botocore = "==1.26.8" cachetools = "==5.1.0" celery = {version = "==5.2.7", extras = ["redis"]} -certifi = "==2022.5.18.1" +certifi = ">=2022.12.7" cffi = "==1.15.0" charset-normalizer = "==2.0.12" click = "==8.1.3" @@ -58,7 +58,9 @@ werkzeug = "~=2.1.1" # gds metrics packages prometheus-client = "==0.14.1" gds-metrics = {version = "==0.2.4", ref = "6f1840a57b6fb1ee40b7e84f2f18ec229de8aa72", git = "https://github.com/alphagov/gds_metrics_python.git"} -notifications-utils = {editable = true, ref = "37ae9753c050851453d072994fb03b1415601716", git = "https://github.com/GSA/notifications-utils"} +packaging = "==21.3" +notifications-utils = {editable = true, ref = "main", git = "https://github.com/GSA/notifications-utils.git"} +newrelic = "*" [dev-packages] flake8 = "==4.0.1" diff --git a/Pipfile.lock b/Pipfile.lock index 9a64eced5..004a17eb8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d652255c8ca6f6cb96778b0159228628816a114f867510bd4ce13a9e9692b101" + "sha256": "ad2dc506d24393f641531ca9d6de586ee5593ec895ad674578e48d718897eff0" }, "pipfile-spec": 6, "requires": { @@ -157,11 +157,11 @@ }, "certifi": { "hashes": [ - "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", - "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" ], "index": "pypi", - "version": "==2022.5.18.1" + "version": "==2022.12.7" }, "cffi": { "hashes": [ @@ -275,6 +275,35 @@ "index": "pypi", "version": "==0.4.4" }, + "cryptography": { + "hashes": [ + "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b", + "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f", + "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190", + "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f", + "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f", + "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb", + "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c", + "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773", + "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72", + "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8", + "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717", + "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9", + "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856", + "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96", + "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288", + "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39", + "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e", + "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce", + "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1", + "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de", + "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df", + "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf", + "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458" + ], + "markers": "python_version >= '3.6'", + "version": "==39.0.0" + }, "defusedxml": { "hashes": [ "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", @@ -388,10 +417,11 @@ }, "govuk-bank-holidays": { "hashes": [ - "sha256:6c993f59bff512740066ae5891a9ce3155e94291560d1d5169fea6e634111a46", - "sha256:9e214828802bbed303f01dacee3facc2296a8bcac9c814654ac7300b37381f78" + "sha256:2b91901be492235c4160a64d56d4d1945f6ca7f11c1ea4277395981866c35bef", + "sha256:ffb6ac050701cb850fd2f08eb9c7c91753090f00525e69cebec4f869f6f0de41" ], - "version": "==0.11" + "markers": "python_version >= '3.6'", + "version": "==0.13" }, "greenlet": { "hashes": [ @@ -476,11 +506,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b", - "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313" + "sha256:7efb448ec9a5e313a57655d35aa54cd3e01b7e1fbcf72dce1bf06119420f5bad", + "sha256:e354bedeb60efa6affdcc8ae121b73544a7aa74156d047311948f6d711cd378d" ], "markers": "python_version < '3.10'", - "version": "==5.1.0" + "version": "==6.0.0" }, "iso8601": { "hashes": [ @@ -700,6 +730,27 @@ ], "version": "==0.8.4" }, + "newrelic": { + "hashes": [ + "sha256:05ceedb760a2f76fc8477f969520426656f6949d5b5bb5634a430a9d9d5c18fb", + "sha256:2d12c4c96187c7a51958d3145dcbfebcbb6fe48d9d3219804e558da3769992a3", + "sha256:37821d6d47288605ed9de138b8ec5498bd597dd91bc296af7733e5b864c40cc1", + "sha256:6a13e6956042eeb8cf013a219f936c88293b8d6460234c5d772fd53640264d3a", + "sha256:720929793f4e17838b354fa997caa0d6474710149ac68c3aea23c29ba9123d1c", + "sha256:8810ea27ae22c7621caf61971f3b3ab1356d237317c2cffe80f605b73f5bcb37", + "sha256:9571efebcc330c157cab9a8d0c6d55f6d7ec146370c59423716534e9989d880d", + "sha256:a5650024071525256df7d74a9bc887840889c35d1e7ade51eda8eedb25d85e95", + "sha256:a8aeae34a518ac961e6c972f9d9479a008925faa585c9a89a9307c79927d92e4", + "sha256:b425ec7653bc7e6e73f2a7f55e707cd9341ac90696c0f3dcafc3ac6776aec959", + "sha256:c006ff70fbc87fe1f201cec0d22b5d209a58bbe31a2530fefe457f051ce44b86", + "sha256:d529e272cb433cee50bab89e2f01ec57ac1b8511845f9b777dfadd3eab3842dd", + "sha256:d79df5fb486ca2a0997f4f9cda9a7e8de39ae07df8161b5ab2742f22bcf50974", + "sha256:f3f66de81c4cce20474017397f372ef11cb52a629c97c2e1fe5c445e31c2bf07", + "sha256:fd33ec7e227701ceafb8e3894dcc3dafae5dbffc4a43814ad9e7976fdb418696" + ], + "index": "pypi", + "version": "==8.5.0" + }, "notifications-python-client": { "hashes": [ "sha256:47c803fcc8b4098d069b92547bb52607b558cec25c19e2697a74faab2e5ef4c0" @@ -709,8 +760,42 @@ }, "notifications-utils": { "editable": true, - "git": "https://github.com/GSA/notifications-utils", - "ref": "37ae9753c050851453d072994fb03b1415601716" + "git": "https://github.com/GSA/notifications-utils.git", + "ref": "d88d48026171991de4d8398e5f9f8c2063297885" + }, + "numpy": { + "hashes": [ + "sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9", + "sha256:0e3463e6ac25313462e04aea3fb8a0a30fb906d5d300f58b3bc2c23da6a15398", + "sha256:179a7ef0889ab769cc03573b6217f54c8bd8e16cef80aad369e1e8185f994cd7", + "sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2", + "sha256:26089487086f2648944f17adaa1a97ca6aee57f513ba5f1c0b7ebdabbe2b9954", + "sha256:28bc9750ae1f75264ee0f10561709b1462d450a4808cd97c013046073ae64ab6", + "sha256:28e418681372520c992805bb723e29d69d6b7aa411065f48216d8329d02ba032", + "sha256:442feb5e5bada8408e8fcd43f3360b78683ff12a4444670a7d9e9824c1817d36", + "sha256:6ec0c021cd9fe732e5bab6401adea5a409214ca5592cd92a114f7067febcba0c", + "sha256:7094891dcf79ccc6bc2a1f30428fa5edb1e6fb955411ffff3401fb4ea93780a8", + "sha256:84e789a085aabef2f36c0515f45e459f02f570c4b4c4c108ac1179c34d475ed7", + "sha256:87a118968fba001b248aac90e502c0b13606721b1343cdaddbc6e552e8dfb56f", + "sha256:8e669fbdcdd1e945691079c2cae335f3e3a56554e06bbd45d7609a6cf568c700", + "sha256:ad2925567f43643f51255220424c23d204024ed428afc5aad0f86f3ffc080086", + "sha256:b0677a52f5d896e84414761531947c7a330d1adc07c3a4372262f25d84af7bf7", + "sha256:b07b40f5fb4fa034120a5796288f24c1fe0e0580bbfff99897ba6267af42def2", + "sha256:b09804ff570b907da323b3d762e74432fb07955701b17b08ff1b5ebaa8cfe6a9", + "sha256:b162ac10ca38850510caf8ea33f89edcb7b0bb0dfa5592d59909419986b72407", + "sha256:b31da69ed0c18be8b77bfce48d234e55d040793cebb25398e2a7d84199fbc7e2", + "sha256:caf65a396c0d1f9809596be2e444e3bd4190d86d5c1ce21f5fc4be60a3bc5b36", + "sha256:cfa1161c6ac8f92dea03d625c2d0c05e084668f4a06568b77a25a89111621566", + "sha256:dae46bed2cb79a58d6496ff6d8da1e3b95ba09afeca2e277628171ca99b99db1", + "sha256:ddc7ab52b322eb1e40521eb422c4e0a20716c271a306860979d450decbb51b8e", + "sha256:de92efa737875329b052982e37bd4371d52cabf469f83e7b8be9bb7752d67e51", + "sha256:e274f0f6c7efd0d577744f52032fdd24344f11c5ae668fe8d01aac0422611df1", + "sha256:ed5fb71d79e771ec930566fae9c02626b939e37271ec285e9efaf1b5d4370e7d", + "sha256:ef85cf1f693c88c1fd229ccd1055570cb41cdf4875873b7728b6301f12cd05bf", + "sha256:f1b739841821968798947d3afcefd386fa56da0caf97722a5de53e07c4ccedc7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.24.1" }, "orderedset": { "hashes": [ @@ -731,15 +816,15 @@ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], - "markers": "python_version >= '3.6'", + "index": "pypi", "version": "==21.3" }, "phonenumbers": { "hashes": [ - "sha256:07a95c2f178687fd1c3f722cf792b3d33e3a225ae71577e500c99b28544cd6d0", - "sha256:7cadfe900e833857500b7bafa3e5a7eddc3263eb66b66a767870b33e44665f92" + "sha256:6d63455012fc9431105ffc7739befca61c3efc551b287dca58d2be2e745475a9", + "sha256:a577a46c069ad889c7b7cf4dd978751d059edeab28b97acead4775d2ea1fc70a" ], - "version": "==8.13.1" + "version": "==8.13.4" }, "prometheus-client": { "hashes": [ @@ -751,11 +836,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:535c29c31216c77302877d5120aef6c94ff573748a5b5ca5b1b1f76f5e700c73", - "sha256:ced598b222f6f4029c0800cefaa6a17373fb580cd093223003475ce32805c35b" + "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63", + "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.0.33" + "version": "==3.0.36" }, "psycopg2-binary": { "hashes": [ @@ -865,75 +950,85 @@ }, "pypdf2": { "hashes": [ - "sha256:7074034d199a817f3c0190c533a1a64ed4237cc060cdb1ebc3a6da796a2c71c0", - "sha256:f1b0df8cd686d2ed069088b903ec65fb0f53e9c7837adc8139eccfbb7eb9141f" + "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440", + "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928" ], "markers": "python_version >= '3.6'", - "version": "==2.11.2" + "version": "==3.0.1" }, "pyproj": { "hashes": [ - "sha256:0fff9c3a991508f16027be27d153f6c5583d03799443639d13c681e60f49e2d7", - "sha256:12f62c20656ac9b6076ebb213e9a635d52f4f01fef95310121d337e62e910cb6", - "sha256:14ad113b5753c6057f9b2f3c85a6497cef7fa237c4328f2943c0223e98c1dde6", - "sha256:1f9c100fd0fd80edbc7e4daa303600a8cbef6f0de43d005617acb38276b88dc0", - "sha256:221d8939685e0c43ee594c9f04b6a73a10e8e1cc0e85f28be0b4eb2f1bc8777d", - "sha256:25a36e297f3e0524694d40259e3e895edc1a47492a0e30608268ffc1328e3f5d", - "sha256:2cb8592259ea54e7557523b079d3f2304081680bdb48bfbf0fd879ee6156129c", - "sha256:3b85acf09e5a9e35cd9ee72989793adb7089b4e611be02a43d3d0bda50ad116b", - "sha256:45554f47d1a12a84b0620e4abc08a2a1b5d9f273a4759eaef75e74788ec7162a", - "sha256:4688b4cd62cbd86b5e855f9e27d90fbb53f2b4c2ea1cd394a46919e1a4151b89", - "sha256:47ad53452ae1dc8b0bf1df920a210bb5616989085aa646592f8681f1d741a754", - "sha256:48787962232109bad8b72e27949037a9b03591228a6955f25dbe451233e8648a", - "sha256:4a23d84c5ffc383c7d9f0bde3a06fc1f6697b1b96725597f8f01e7b4bef0a2b5", - "sha256:4e161114bc92701647a83c4bbce79489984f12d980cabb365516e953d1450885", - "sha256:4fd425ee8b6781c249c7adb7daa2e6c41ce573afabe4f380f5eecd913b56a3be", - "sha256:52e54796e2d9554a5eb8f11df4748af1fbbc47f76aa234d6faf09216a84554c5", - "sha256:5816807ca0bdc7256558770c6206a6783a3f02bcf844f94ee245f197bb5f7285", - "sha256:65a0bcdbad95b3c00b419e5d75b1f7e450ec17349b5ea16bf7438ac1d50a12a2", - "sha256:77d5f519f3cdb94b026ecca626f78db4f041afe201cf082079c8c0092a30b087", - "sha256:82200b4569d68b421c079d2973475b58d5959306fe758b43366e79fe96facfe5", - "sha256:954b068136518b3174d0a99448056e97af62b63392a95c420894f7de2229dae6", - "sha256:9a496d9057b2128db9d733e66b206f2d5954bbae6b800d412f562d780561478c", - "sha256:a454a7c4423faa2a14e939d08ef293ee347fa529c9df79022b0585a6e1d8310c", - "sha256:a708445927ace9857f52c3ba67d2915da7b41a8fdcd9b8f99a4c9ed60a75eb33", - "sha256:aa5171f700f174777a9e9ed8f4655583243967c0f9cf2c90e3f54e54ff740134", - "sha256:ccb4b70ad25218027f77e0c8934d10f9b7cdf91d5e64080147743d58fddbc3c0", - "sha256:d94afed99f31673d3d19fe750283621e193e2a53ca9e0443bf9d092c3905833b", - "sha256:e7e609903572a56cca758bbaee5c1663c3e829ddce5eec4f368e68277e37022b", - "sha256:f343725566267a296b09ee7e591894f1fdc90f84f8ad5ec476aeb53bd4479c07", - "sha256:f80adda8c54b84271a93829477a01aa57bc178c834362e9f74e1de1b5033c74c" + "sha256:0189fdd7aa789542a7a623010dfff066c5849b24397f81f860ec3ee085cbf55c", + "sha256:0406f64ff59eb3342efb102c9f31536430aa5cde5ef0bfabd5aaccb73dd8cd5a", + "sha256:0c7b32382ae22a9bf5b690d24c7b4c0fb89ba313c3a91ef1a8c54b50baf10954", + "sha256:129234afa179c8293b010ea4f73655ff7b20b5afdf7fac170f223bcf0ed6defd", + "sha256:19f5de1a7c3b81b676d846350d4bdf2ae6af13b9a450d1881706f088ecad0e2c", + "sha256:231c038c6b65395c41ae3362320f03ce8054cb54dc63556e605695e5d461a27e", + "sha256:25a5425cd2a0b16f5f944d49165196eebaa60b898a08c404a644c29e6a7a04b3", + "sha256:261eb29b1d55b1eb7f336127344d9b31284d950a9446d1e0d1c2411f7dd8e3ac", + "sha256:2bd633f3b8ca6eb09135dfaf06f09e2869deb139985aab26d728e8a60c9938b9", + "sha256:2d1e7f42da205e0534831ae9aa9cee0353ab8c1aab2c369474adbb060294d98a", + "sha256:2f87f16b902c8b2af007295c63a435f043db9e40bd45e6f96962c7b8cd08fdb5", + "sha256:321b82210dc5271558573d0874b9967c5a25872a28d0168049ddabe8bfecffce", + "sha256:3d70ca5933cddbe6f51396006fb9fc78bc2b1f9d28775922453c4b04625a7efb", + "sha256:57ec7d2b7f2773d877927abc72e2229ef8530c09181be0e28217742bae1bc4f5", + "sha256:5a53acbde511a7a9e1873c7f93c68f35b8c3653467b77195fe18e847555dcb7a", + "sha256:5f3f75b030cf811f040c90a8758a20115e8746063e4cad0d0e941a4954d1219b", + "sha256:6bdac3bc1899fcc4021be06d303b342923fb8311fe06f8d862c348a1a0e78b41", + "sha256:7aef19d5a0a3b2d6b17f7dc9a87af722e71139cd1eea7eb82ed062a8a4b0e272", + "sha256:8078c90cea07d53e3406c7c84cbf76a2ac0ffc580c365f13801575486b9d558c", + "sha256:97065fe82e80f7e2740e7897a0e36e8defc0a3614927f0276b4f1d1ea1ef66fa", + "sha256:a30d78e619dae5cd1bb69addae2f1e5f8ee1b4a8ab4f3d954e9eaf41948db506", + "sha256:a32e1d12340ad93232b7ea4dc1a4f4b21fa9fa9efa4b293adad45be7af6b51ec", + "sha256:a5eada965e8ac24e783f2493d1d9bcd11c5c93959bd43558224dd31d9faebd1c", + "sha256:a98fe3e53be428e67ae6a9ee9affff92346622e0e3ea0cbc15dce939b318d395", + "sha256:c240fe6bcb5c325b50fc967d5458d708412633f4f05fefc7fb14c14254ebf421", + "sha256:c60d112d8f1621a606b7f2adb0b1582f80498e663413d2ba9f5df1c93d99f432", + "sha256:cd9f9c409f465834988ce0aa8c1ed496081c6957f2e5ef40ed28de04397d3c0b", + "sha256:ce50126dad7cd4749ab86fc4c8b54ec0898149ce6710ab5c93c76a54a4afa249", + "sha256:da96319b137cfd66f0bae0e300cdc77dd17af4785b9360a9bdddb1d7176a0bbb", + "sha256:e463c687007861a9949909211986850cfc2e72930deda0d06449ef2e315db534", + "sha256:e8c0d1ac9ef5a4d2e6501a4b30136c55f1e1db049d1626cc313855c4f97d196d", + "sha256:e9d82df555cf19001bac40e1de0e40fb762dec785685b77edd6993286c01b7f7", + "sha256:ef76abfee1a0676ef973470abe11e22998750f2bd944afaf76d44ad70b538c06", + "sha256:ef8c30c62fe4e386e523e14e1e83bd460f745bd2c8dfd0d0c327f9460c4d3c0c", + "sha256:f38dea459e22e86326b1c7d47718a3e10c7a27910cf5eb86ea2679b8084d0c4e" ], "markers": "python_version >= '3.8'", - "version": "==3.4.0" + "version": "==3.4.1" }, "pyrsistent": { "hashes": [ - "sha256:055ab45d5911d7cae397dc418808d8802fb95262751872c841c170b0dbf51eed", - "sha256:111156137b2e71f3a9936baf27cb322e8024dac3dc54ec7fb9f0bcf3249e68bb", - "sha256:187d5730b0507d9285a96fca9716310d572e5464cadd19f22b63a6976254d77a", - "sha256:21455e2b16000440e896ab99e8304617151981ed40c29e9507ef1c2e4314ee95", - "sha256:2aede922a488861de0ad00c7630a6e2d57e8023e4be72d9d7147a9fcd2d30712", - "sha256:3ba4134a3ff0fc7ad225b6b457d1309f4698108fb6b35532d015dca8f5abed73", - "sha256:456cb30ca8bff00596519f2c53e42c245c09e1a4543945703acd4312949bfd41", - "sha256:71d332b0320642b3261e9fee47ab9e65872c2bd90260e5d225dabeed93cbd42b", - "sha256:879b4c2f4d41585c42df4d7654ddffff1239dc4065bc88b745f0341828b83e78", - "sha256:9cd3e9978d12b5d99cbdc727a3022da0430ad007dacf33d0bf554b96427f33ab", - "sha256:a178209e2df710e3f142cbd05313ba0c5ebed0a55d78d9945ac7a4e09d923308", - "sha256:b39725209e06759217d1ac5fcdb510e98670af9e37223985f330b611f62e7425", - "sha256:bfa0351be89c9fcbcb8c9879b826f4353be10f58f8a677efab0c017bf7137ec2", - "sha256:bfd880614c6237243ff53a0539f1cb26987a6dc8ac6e66e0c5a40617296a045e", - "sha256:c43bec251bbd10e3cb58ced80609c5c1eb238da9ca78b964aea410fb820d00d6", - "sha256:d690b18ac4b3e3cab73b0b7aa7dbe65978a172ff94970ff98d82f2031f8971c2", - "sha256:d6982b5a0237e1b7d876b60265564648a69b14017f3b5f908c5be2de3f9abb7a", - "sha256:dec3eac7549869365fe263831f576c8457f6c833937c68542d08fde73457d291", - "sha256:e371b844cec09d8dc424d940e54bba8f67a03ebea20ff7b7b0d56f526c71d584", - "sha256:e5d8f84d81e3729c3b506657dddfe46e8ba9c330bf1858ee33108f8bb2adb38a", - "sha256:ea6b79a02a28550c98b6ca9c35b9f492beaa54d7c5c9e9949555893c8a9234d0", - "sha256:f1258f4e6c42ad0b20f9cfcc3ada5bd6b83374516cd01c0960e3cb75fdca6770" + "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8", + "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440", + "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a", + "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c", + "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3", + "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393", + "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9", + "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da", + "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf", + "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64", + "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a", + "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3", + "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98", + "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2", + "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8", + "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf", + "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc", + "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7", + "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28", + "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2", + "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b", + "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a", + "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64", + "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19", + "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1", + "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9", + "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c" ], "markers": "python_version >= '3.7'", - "version": "==0.19.2" + "version": "==0.19.3" }, "python-dateutil": { "hashes": [ @@ -961,10 +1056,10 @@ }, "pytz": { "hashes": [ - "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427", - "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2" + "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0", + "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a" ], - "version": "==2022.6" + "version": "==2022.7.1" }, "pyyaml": { "hashes": [ @@ -1003,18 +1098,18 @@ }, "redis": { "hashes": [ - "sha256:30c07511627a4c5c4d970e060000772f323174f75e745a26938319817ead7a12", - "sha256:46652271dc7525cd5a9667e5b0ca983c848c75b2b8f7425403395bb8379dcf25" + "sha256:a010f6cb7378065040a02839c3f75c7e0fb37a87116fb4a95be82a95552776c7", + "sha256:e6206448e2f8a432871d07d432c13ed6c2abcf6b74edb436c99752b1371be387" ], - "version": "==4.3.5" + "version": "==4.4.2" }, "requests": { "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" ], "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.1" + "version": "==2.28.2" }, "rfc3339-validator": { "hashes": [ @@ -1048,56 +1143,55 @@ }, "setuptools": { "hashes": [ - "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54", - "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75" + "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab", + "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6" ], "markers": "python_version >= '3.7'", - "version": "==65.6.3" + "version": "==66.0.0" }, "shapely": { "hashes": [ - "sha256:02dd5d7dc6e46515d88874134dc8fcdc65826bca93c3eecee59d1910c42c1b17", - "sha256:0b4ee3132ee90f07d63db3aea316c4c065ed7a26231458dda0874414a09d6ba3", - "sha256:0d885cb0cf670c1c834df3f371de8726efdf711f18e2a75da5cfa82843a7ab65", - "sha256:147066da0be41b147a61f8eb805dea3b13709dbc873a431ccd7306e24d712bc0", - "sha256:21776184516a16bf82a0c3d6d6a312b3cd15a4cabafc61ee01cf2714a82e8396", - "sha256:2e0a8c2e55f1be1312b51c92b06462ea89e6bb703fab4b114e7a846d941cfc40", - "sha256:2fd15397638df291c427a53d641d3e6fd60458128029c8c4f487190473a69a91", - "sha256:3480657460e939f45a7d359ef0e172a081f249312557fe9aa78c4fd3a362d993", - "sha256:370b574c78dc5af3a198a6da5d9b3d7c04654bd2ef7e80e80a3a0992dfb2d9cd", - "sha256:38f0fbbcb8ca20c16451c966c1f527cc43968e121c8a048af19ed3e339a921cd", - "sha256:4728666fff8cccc65a07448cae72c75a8773fea061c3f4f139c44adc429b18c3", - "sha256:48dcfffb9e225c0481120f4bdf622131c8c95f342b00b158cdbe220edbbe20b6", - "sha256:532a55ee2a6c52d23d6f7d1567c8f0473635f3b270262c44e1b0c88096827e22", - "sha256:5d7f85c2d35d39ff53c9216bc76b7641c52326f7e09aaad1789a3611a0f812f2", - "sha256:65b21243d8f6bcd421210daf1fabb9de84de2c04353c5b026173b88d17c1a581", - "sha256:66bdac74fbd1d3458fa787191a90fa0ae610f09e2a5ec398c36f968cc0ed743f", - "sha256:6d388c0c1bd878ed1af4583695690aa52234b02ed35f93a1c8486ff52a555838", - "sha256:6fe855e7d45685926b6ba00aaeb5eba5862611f7465775dacd527e081a8ced6d", - "sha256:753ed0e21ab108bd4282405b9b659f2e985e8502b1a72b978eaa51d3496dee19", - "sha256:783bad5f48e2708a0e2f695a34ed382e4162c795cb2f0368b39528ac1d6db7ed", - "sha256:78fb9d929b8ee15cfd424b6c10879ce1907f24e05fb83310fc47d2cd27088e40", - "sha256:84010db15eb364a52b74ea8804ef92a6a930dfc1981d17a369444b6ddec66efd", - "sha256:8d086591f744be483b34628b391d741e46f2645fe37594319e0a673cc2c26bcf", - "sha256:8e59817b0fe63d34baedaabba8c393c0090f061917d18fc0bcc2f621937a8f73", - "sha256:99a2f0da0109e81e0c101a2b4cd8412f73f5f299e7b5b2deaf64cd2a100ac118", - "sha256:99ab0ddc05e44acabdbe657c599fdb9b2d82e86c5493bdae216c0c4018a82dee", - "sha256:a23ef3882d6aa203dd3623a3d55d698f59bfbd9f8a3bfed52c2da05a7f0f8640", - "sha256:a354199219c8d836f280b88f2c5102c81bb044ccea45bd361dc38a79f3873714", - "sha256:a74631e511153366c6dbe3229fa93f877e3c87ea8369cd00f1d38c76b0ed9ace", - "sha256:ab38f7b5196ace05725e407cb8cab9ff66edb8e6f7bb36a398e8f73f52a7aaa2", - "sha256:adcf8a11b98af9375e32bff91de184f33a68dc48b9cb9becad4f132fa25cfa3c", - "sha256:b65f5d530ba91e49ffc7c589255e878d2506a8b96ffce69d3b7c4500a9a9eaf8", - "sha256:be9423d5a3577ac2e92c7e758bd8a2b205f5e51a012177a590bc46fc51eb4834", - "sha256:c2822111ddc5bcfb116e6c663e403579d0fe3f147d2a97426011a191c43a7458", - "sha256:c6a9a4a31cd6e86d0fbe8473ceed83d4fe760b19d949fb557ef668defafea0f6", - "sha256:d048f93e42ba578b82758c15d8ae037d08e69d91d9872bca5a1895b118f4e2b0", - "sha256:e9c30b311de2513555ab02464ebb76115d242842b29c412f5a9aa0cac57be9f6", - "sha256:ec14ceca36f67cb48b34d02d7f65a9acae15cd72b48e303531893ba4a960f3ea", - "sha256:ef3be705c3eac282a28058e6c6e5503419b250f482320df2172abcbea642c831" + "sha256:11f1b1231a6c04213fb1226c6968d1b1b3b369ec42d1e9655066af87631860ea", + "sha256:13a9f978cd287e0fa95f39904a2bb36deddab490e4fab8bf43eba01b7d9eb58f", + "sha256:17d0f89581aa15f7887052a6adf2753f9fe1c3fdbb6116653972e0d43e720e65", + "sha256:21ba32a6c45b7f8ab7d2d8d5cf339704e2d1dfdf3e2fb465b950a0c9bc894a4f", + "sha256:2287d0cb592c1814e9f48065888af7ee3f13e090e6f7fa3e208b06a83fb2f6af", + "sha256:292c22ff7806e3a25bc4324295e9204169c61a09165d4c9ee0a9784c1709c85e", + "sha256:40c397d67ba609a163d38b649eee2b06c5f9bdc86d244a8e4cd09c6e2791cf3c", + "sha256:44198fc188fe4b7dd39ef0fd325395d1d6ab0c29a7bbaa15663a16c362bf6f62", + "sha256:5477be8c11bf3109f7b804bb2d57536538b8d0a6118207f1020d71338f1a827c", + "sha256:550f110940d79931b6a12a17de07f6b158c9586c4b121f885af11458ae5626d7", + "sha256:56c0e70749f8c2956493e9333375d2e2264ce25c838fc49c3a2ececbf2d3ba92", + "sha256:5fe8649aafe6adcb4d90f7f735f06ca8ca02a16da273d901f1dd02afc0d3618e", + "sha256:6c71738702cf5c3fc60b3bbe869c321b053ea754f57addded540a71c78c2612e", + "sha256:7266080d39946395ba4b31fa35b9b7695e0a4e38ccabf0c67e2936caf9f9b054", + "sha256:73771b3f65c2949cce0b310b9b62b8ce069407ceb497a9dd4436f9a4d059f12c", + "sha256:73d605fcefd06ee997ba307ef363448d355f3c3e81b3f56ed332eaf6d506e1b5", + "sha256:7b2c41514ba985ea3772eee9b386d620784cccb7a459a270a072f3ef01fdd807", + "sha256:820bee508e4a0e564db22f8b55bb5e6e7f326d8d7c103639c42f5d3f378f4067", + "sha256:8a7ba97c97d85c1f07c57f9524c45128ef2bf8279061945d78052c78862b357f", + "sha256:8b9f780c3b79b4a6501e0e8833b1877841b7b0e0a243e77b529fda8f1030afc2", + "sha256:91bbca0378eb82f0808f0e59150ac0952086f4caaab87ad8515a5e55e896c21e", + "sha256:99420c89af78f371b96f0e2bad9afdebc6d0707d4275d157101483e4c4049fd6", + "sha256:a391cae931976fb6d8d15a4f4a92006358e93486454a812dde1d64184041a476", + "sha256:a9b6651812f2caa23e4d06bc06a2ed34450f82cb1c110c170a25b01bbb090895", + "sha256:b1def13ec2a74ebda2210d2fc1c53cecce5a079ec90f341101399427874507f1", + "sha256:b3d97f3ce6df47ca68c2d64b8c3cfa5c8ccc0fbc81ef8e15ff6004a6426e71b1", + "sha256:c47a61b1cd0c5b064c6d912bce7dba78c01f319f65ecccd6e61eecd21861a37a", + "sha256:c4b99a3456e06dc55482569669ece969cdab311f2ad2a1d5622fc770f68cf3cd", + "sha256:d28e19791c9be2ba1cb2fddefa86f73364bdf8334e88dbcd78a8e4494c0af66b", + "sha256:d486cab823f0a978964ae97ca10564ea2b2ced93e84a2ef0b7b62cbacec9d3d2", + "sha256:de3722c68e49fbde8cb6859695bbb8fb9a4d48bbdf34fcf38b7994d2bd9772e2", + "sha256:e4ed31658fd0799eaa3569982aab1a5bc8fcf25ec196606bf137ee4fa984be88", + "sha256:e991ad155783cd0830b895ec8f310fde9e79a7b283776b889a751fb1e7c819fc", + "sha256:eab24b60ae96b7375adceb1f120be818c59bd69db0f3540dc89527d8a371d253", + "sha256:eaea9ddee706654026a84aceb9a3156105917bab3de58fcf150343f847478202", + "sha256:ef98fec4a3aca6d33e3b9fdd680fe513cc7d1c6aedc65ada8a3965601d9d4bcf", + "sha256:f69c418f2040c8593e33b1aba8f2acf890804b073b817535b5d291139d152af5", + "sha256:f96b24da0242791cd6042f6caf074e7a4537a66ca2d1b57d423feb98ba901295" ], - "markers": "python_version >= '3.6'", - "version": "==1.8.5.post1" + "markers": "python_version >= '3.7'", + "version": "==2.0.0" }, "six": { "hashes": [ @@ -1187,11 +1281,11 @@ }, "urllib3": { "hashes": [ - "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", - "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" + "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", + "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.13" + "version": "==1.26.14" }, "vine": { "hashes": [ @@ -1203,10 +1297,10 @@ }, "wcwidth": { "hashes": [ - "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", - "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e", + "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0" ], - "version": "==0.2.5" + "version": "==0.2.6" }, "webcolors": { "hashes": [ @@ -1355,11 +1449,11 @@ }, "certifi": { "hashes": [ - "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7", - "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a" + "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3", + "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18" ], "index": "pypi", - "version": "==2022.5.18.1" + "version": "==2022.12.7" }, "cffi": { "hashes": [ @@ -1437,99 +1531,97 @@ "toml" ], "hashes": [ - "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", - "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", - "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", - "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", - "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", - "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", - "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", - "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", - "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", - "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", - "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", - "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", - "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", - "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", - "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", - "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", - "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", - "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", - "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", - "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", - "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", - "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", - "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", - "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", - "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", - "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", - "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", - "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", - "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", - "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", - "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", - "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", - "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", - "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", - "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", - "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", - "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", - "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", - "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", - "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", - "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", - "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", - "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", - "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", - "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", - "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", - "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", - "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", - "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", - "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" + "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45", + "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809", + "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4", + "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b", + "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7", + "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0", + "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0", + "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea", + "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2", + "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a", + "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45", + "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b", + "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209", + "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca", + "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab", + "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095", + "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7", + "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6", + "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af", + "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499", + "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831", + "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637", + "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2", + "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb", + "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029", + "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc", + "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8", + "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f", + "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2", + "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d", + "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289", + "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c", + "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded", + "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96", + "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0", + "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904", + "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21", + "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89", + "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78", + "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad", + "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196", + "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd", + "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0", + "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882", + "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757", + "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16", + "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0", + "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47", + "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40", + "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1", + "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3" ], "markers": "python_version >= '3.7'", - "version": "==6.5.0" + "version": "==7.0.5" }, "cryptography": { "hashes": [ - "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd", - "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db", - "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290", - "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744", - "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb", - "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d", - "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70", - "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b", - "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876", - "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083", - "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6", - "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1", - "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00", - "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b", - "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b", - "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285", - "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9", - "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0", - "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d", - "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2", - "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8", - "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee", - "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b", - "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7", - "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353", - "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c" + "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b", + "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f", + "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190", + "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f", + "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f", + "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb", + "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c", + "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773", + "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72", + "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8", + "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717", + "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9", + "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856", + "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96", + "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288", + "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39", + "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e", + "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce", + "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1", + "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de", + "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df", + "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf", + "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458" ], "markers": "python_version >= '3.6'", - "version": "==38.0.4" + "version": "==39.0.0" }, "cyclonedx-python-lib": { "hashes": [ - "sha256:48ae942a892e8385f4e0193d2e295a338df9ab864652081406c26f58085d2b35", - "sha256:a03b8f79f23aa95d37180b5d7bca81ef393b569e2d29e02f4817cfe4488e1ba2" + "sha256:1ccd482024a30b95c4fffb3fe567a9df97b705f34c1075f8abde8537867600c3", + "sha256:8981ca462fba91469c268d684a03f72c89c7a807674d884f83a28d8c2822a9b6" ], "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==3.1.1" + "version": "==3.1.5" }, "execnet": { "hashes": [ @@ -1573,11 +1665,11 @@ }, "gitpython": { "hashes": [ - "sha256:41eea0deec2deea139b459ac03656f0dd28fc4a3387240ec1d3c259a2c47850f", - "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd" + "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8", + "sha256:cd455b0000615c60e286208ba540271af9fe531fa6a87cc590a7298785ab2882" ], "markers": "python_version >= '3.7'", - "version": "==3.1.29" + "version": "==3.1.30" }, "honcho": { "hashes": [ @@ -1604,10 +1696,11 @@ }, "iniconfig": { "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" ], - "version": "==1.1.1" + "markers": "python_version >= '3.7'", + "version": "==2.0.0" }, "isort": { "hashes": [ @@ -1782,16 +1875,16 @@ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], - "markers": "python_version >= '3.6'", + "index": "pypi", "version": "==21.3" }, "pbr": { "hashes": [ - "sha256:b97bc6695b2aff02144133c2e7399d5885223d42b7912ffaec2ca3898e673bfe", - "sha256:db2317ff07c84c4c63648c9064a79fe9d9f5c7ce85a9099d4b6258b3db83225a" + "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b", + "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3" ], "markers": "python_version >= '2.6'", - "version": "==5.11.0" + "version": "==5.11.1" }, "pip": { "hashes": [ @@ -1811,19 +1904,19 @@ }, "pip-audit": { "hashes": [ - "sha256:a99f825ee431a89b89981c4e9e6eaacff5af3233783f2f5d79fe03306dc378ce", - "sha256:f87b37b6db5317a3f5ecebc202b5d4401958b5e4bd05b39d7b230bdc6f63c34b" + "sha256:e65dfbccf29f290606911d3a49cf62497d3c140e4aac0a17775438d7630588da", + "sha256:eab6aab5a649e93f17309bf8d57bb369a63fa9ef60e96b585bba22acdbb54b97" ], "index": "pypi", - "version": "==2.4.7" + "version": "==2.4.12" }, "pip-requirements-parser": { "hashes": [ - "sha256:22fa213a987913385b2484d5698ecfa1d9cf4154978cdf929085548af55355b0", - "sha256:8c2a6f8e091ac2693824a5ef4e3b250226e34f74a20a91a87b9ab0714b47788f" + "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", + "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3" ], - "markers": "python_version >= '3.6'", - "version": "==31.2.0" + "markers": "python_full_version >= '3.6.0'", + "version": "==32.0.1" }, "pluggy": { "hashes": [ @@ -1866,11 +1959,11 @@ }, "pygments": { "hashes": [ - "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1", - "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42" + "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297", + "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717" ], "markers": "python_version >= '3.6'", - "version": "==2.13.0" + "version": "==2.14.0" }, "pyparsing": { "hashes": [ @@ -1937,10 +2030,10 @@ }, "pytz": { "hashes": [ - "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427", - "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2" + "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0", + "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a" ], - "version": "==2022.6" + "version": "==2022.7.1" }, "pyyaml": { "hashes": [ @@ -1979,11 +2072,11 @@ }, "requests": { "hashes": [ - "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983", - "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349" + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" ], "markers": "python_version >= '3.7' and python_version < '4'", - "version": "==2.28.1" + "version": "==2.28.2" }, "requests-mock": { "hashes": [ @@ -2010,11 +2103,11 @@ }, "rich": { "hashes": [ - "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e", - "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0" + "sha256:81c73a30b144bbcdedc13f4ea0b6ffd7fdc3b0d3cc259a9402309c8e4aee1964", + "sha256:f846bff22a43e8508aebf3f0f2410ce1c6f4cde429098bd58d91fde038c57299" ], - "markers": "python_full_version >= '3.6.3' and python_full_version < '4.0.0'", - "version": "==12.6.0" + "markers": "python_full_version >= '3.7.0'", + "version": "==13.1.0" }, "s3transfer": { "hashes": [ @@ -2026,11 +2119,11 @@ }, "setuptools": { "hashes": [ - "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54", - "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75" + "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab", + "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6" ], "markers": "python_version >= '3.7'", - "version": "==65.6.3" + "version": "==66.0.0" }, "six": { "hashes": [ @@ -2088,11 +2181,11 @@ }, "urllib3": { "hashes": [ - "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc", - "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8" + "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72", + "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.26.13" + "version": "==1.26.14" }, "webencodings": { "hashes": [ diff --git a/app/__init__.py b/app/__init__.py index 489e37db5..fb07f88ea 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -147,9 +147,6 @@ def register_blueprint(application): from app.notifications.notifications_ses_callback import ( ses_callback_blueprint, ) - from app.notifications.notifications_sms_callback import ( - sms_callback_blueprint, - ) from app.notifications.receive_notifications import ( receive_notifications_blueprint, ) @@ -198,11 +195,6 @@ def register_blueprint(application): ses_callback_blueprint.before_request(requires_no_auth) application.register_blueprint(ses_callback_blueprint) - # delivery receipts - # TODO: make sure research mode can still trigger sms callbacks, then re-enable this - sms_callback_blueprint.before_request(requires_no_auth) - application.register_blueprint(sms_callback_blueprint) - # inbound sms receive_notifications_blueprint.before_request(requires_no_auth) application.register_blueprint(receive_notifications_blueprint) diff --git a/app/celery/letters_pdf_tasks.py b/app/celery/letters_pdf_tasks.py new file mode 100644 index 000000000..754501567 --- /dev/null +++ b/app/celery/letters_pdf_tasks.py @@ -0,0 +1,570 @@ +from base64 import urlsafe_b64encode +from datetime import datetime, timedelta +from hashlib import sha512 + +from botocore.exceptions import ClientError as BotoClientError +from flask import current_app +from notifications_utils.letter_timings import LETTER_PROCESSING_DEADLINE +from notifications_utils.postal_address import PostalAddress +from notifications_utils.timezones import convert_utc_to_local_timezone + +from app import encryption, notify_celery +from app.aws import s3 +from app.config import QueueNames, TaskNames +from app.cronitor import cronitor +from app.dao.notifications_dao import ( + dao_get_letters_and_sheets_volume_by_postage, + dao_get_letters_to_be_printed, + dao_get_notification_by_reference, + dao_update_notification, + dao_update_notifications_by_reference, + get_notification_by_id, + update_notification_status_by_id, +) +from app.dao.templates_dao import dao_get_template_by_id +from app.errors import VirusScanError +from app.exceptions import NotificationTechnicalFailureException +from app.letters.utils import ( + LetterPDFNotFound, + ScanErrorType, + find_letter_pdf_in_s3, + generate_letter_pdf_filename, + get_billable_units_for_letter_page_count, + get_file_names_from_error_bucket, + get_folder_name, + get_reference_from_filename, + move_error_pdf_to_scan_bucket, + move_failed_pdf, + move_sanitised_letter_to_test_or_live_pdf_bucket, + move_scan_to_invalid_pdf_bucket, +) +from app.models import ( + INTERNATIONAL_LETTERS, + INTERNATIONAL_POSTAGE_TYPES, + KEY_TYPE_NORMAL, + KEY_TYPE_TEST, + NOTIFICATION_CREATED, + NOTIFICATION_DELIVERED, + NOTIFICATION_PENDING_VIRUS_CHECK, + NOTIFICATION_TECHNICAL_FAILURE, + NOTIFICATION_VALIDATION_FAILED, + NOTIFICATION_VIRUS_SCAN_FAILED, + POSTAGE_TYPES, + RESOLVE_POSTAGE_FOR_FILE_NAME, + Service, +) + + +@notify_celery.task(bind=True, name="get-pdf-for-templated-letter", max_retries=15, default_retry_delay=300) +def get_pdf_for_templated_letter(self, notification_id): + try: + notification = get_notification_by_id(notification_id, _raise=True) + letter_filename = generate_letter_pdf_filename( + reference=notification.reference, + created_at=notification.created_at, + ignore_folder=notification.key_type == KEY_TYPE_TEST, + postage=notification.postage + ) + letter_data = { + 'letter_contact_block': notification.reply_to_text, + 'template': { + "subject": notification.template.subject, + "content": notification.template.content, + "template_type": notification.template.template_type + }, + 'values': notification.personalisation, + 'logo_filename': notification.service.letter_branding and notification.service.letter_branding.filename, + 'letter_filename': letter_filename, + "notification_id": str(notification_id), + 'key_type': notification.key_type + } + + encrypted_data = encryption.sign(letter_data) + + notify_celery.send_task( + name=TaskNames.CREATE_PDF_FOR_TEMPLATED_LETTER, + args=(encrypted_data,), + queue=QueueNames.SANITISE_LETTERS + ) + except Exception as e: + try: + current_app.logger.exception( + f"RETRY: calling create-letter-pdf task for notification {notification_id} failed" + ) + self.retry(exc=e, queue=QueueNames.RETRY) + except self.MaxRetriesExceededError: + message = f"RETRY FAILED: Max retries reached. " \ + f"The task create-letter-pdf failed for notification id {notification_id}. " \ + f"Notification has been updated to technical-failure" + update_notification_status_by_id(notification_id, NOTIFICATION_TECHNICAL_FAILURE) + raise NotificationTechnicalFailureException(message) + + +@notify_celery.task(bind=True, name="update-billable-units-for-letter", max_retries=15, default_retry_delay=300) +def update_billable_units_for_letter(self, notification_id, page_count): + notification = get_notification_by_id(notification_id, _raise=True) + + billable_units = get_billable_units_for_letter_page_count(page_count) + + if notification.key_type != KEY_TYPE_TEST: + notification.billable_units = billable_units + dao_update_notification(notification) + + current_app.logger.info( + f"Letter notification id: {notification_id} reference {notification.reference}: " + f"billable units set to {billable_units}" + ) + + +@notify_celery.task( + bind=True, name="update-validation-failed-for-templated-letter", max_retries=15, default_retry_delay=300 +) +def update_validation_failed_for_templated_letter(self, notification_id, page_count): + notification = get_notification_by_id(notification_id, _raise=True) + notification.status = NOTIFICATION_VALIDATION_FAILED + dao_update_notification(notification) + current_app.logger.info(f"Validation failed: letter is too long {page_count} for letter with id: {notification_id}") + + +@notify_celery.task(name='collate-letter-pdfs-to-be-sent') +@cronitor("collate-letter-pdfs-to-be-sent") +def collate_letter_pdfs_to_be_sent(): + """ + Finds all letters which are still waiting to be sent to DVLA for printing + + This would usually be run at 5.50pm and collect up letters created between before 5:30pm today + that have not yet been sent. + If run after midnight, it will collect up letters created before 5:30pm the day before. + """ + print_run_date = convert_utc_to_local_timezone(datetime.utcnow()) + if print_run_date.time() < LETTER_PROCESSING_DEADLINE: + print_run_date = print_run_date - timedelta(days=1) + + print_run_deadline = print_run_date.replace( + hour=17, minute=30, second=0, microsecond=0 + ) + _get_letters_and_sheets_volumes_and_send_to_dvla(print_run_deadline) + + for postage in POSTAGE_TYPES: + current_app.logger.info(f"starting collate-letter-pdfs-to-be-sent processing for postage class {postage}") + letters_to_print = get_key_and_size_of_letters_to_be_sent_to_print(print_run_deadline, postage) + + for i, letters in enumerate(group_letters(letters_to_print)): + filenames = [letter['Key'] for letter in letters] + + service_id = letters[0]['ServiceId'] + organisation_id = letters[0]['OrganisationId'] + + hash = urlsafe_b64encode(sha512(''.join(filenames).encode()).digest())[:20].decode() + # eg NOTIFY.2018-12-31.001.Wjrui5nAvObjPd-3GEL-.ZIP + dvla_filename = 'NOTIFY.{date}.{postage}.{num:03}.{hash}.{service_id}.{organisation_id}.ZIP'.format( + date=print_run_deadline.strftime("%Y-%m-%d"), + postage=RESOLVE_POSTAGE_FOR_FILE_NAME[postage], + num=i + 1, + hash=hash, + service_id=service_id, + organisation_id=organisation_id + ) + + current_app.logger.info( + 'Calling task zip-and-send-letter-pdfs for {} pdfs to upload {} with total size {:,} bytes'.format( + len(filenames), + dvla_filename, + sum(letter['Size'] for letter in letters) + ) + ) + notify_celery.send_task( + name=TaskNames.ZIP_AND_SEND_LETTER_PDFS, + kwargs={ + 'filenames_to_zip': filenames, + 'upload_filename': dvla_filename + }, + queue=QueueNames.PROCESS_FTP, + compression='zlib' + ) + current_app.logger.info(f"finished collate-letter-pdfs-to-be-sent processing for postage class {postage}") + + current_app.logger.info("finished collate-letter-pdfs-to-be-sent") + + +def _get_letters_and_sheets_volumes_and_send_to_dvla(print_run_deadline): + letters_volumes = dao_get_letters_and_sheets_volume_by_postage(print_run_deadline) + send_letters_volume_email_to_dvla(letters_volumes, print_run_deadline.date()) + + +def send_letters_volume_email_to_dvla(letters_volumes, date): + personalisation = { + 'total_volume': 0, + 'first_class_volume': 0, + 'second_class_volume': 0, + 'international_volume': 0, + 'total_sheets': 0, + 'first_class_sheets': 0, + "second_class_sheets": 0, + 'international_sheets': 0, + 'date': date.strftime("%d %B %Y") + } + for item in letters_volumes: + personalisation['total_volume'] += item.letters_count + personalisation['total_sheets'] += item.sheets_count + if f"{item.postage}_class_volume" in personalisation: + personalisation[f"{item.postage}_class_volume"] = item.letters_count + personalisation[f"{item.postage}_class_sheets"] = item.sheets_count + else: + personalisation["international_volume"] += item.letters_count + personalisation["international_sheets"] += item.sheets_count + + template = dao_get_template_by_id(current_app.config['LETTERS_VOLUME_EMAIL_TEMPLATE_ID']) + recipients = current_app.config['DVLA_EMAIL_ADDRESSES'] + reply_to = template.service.get_default_reply_to_email_address() + service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID']) + + # avoid circular imports: + from app.notifications.process_notifications import ( + persist_notification, + send_notification_to_queue, + ) + for recipient in recipients: + saved_notification = persist_notification( + template_id=template.id, + template_version=template.version, + recipient=recipient, + service=service, + personalisation=personalisation, + notification_type=template.template_type, + api_key_id=None, + key_type=KEY_TYPE_NORMAL, + reply_to_text=reply_to + ) + + send_notification_to_queue(saved_notification, False, queue=QueueNames.NOTIFY) + + +def get_key_and_size_of_letters_to_be_sent_to_print(print_run_deadline, postage): + letters_awaiting_sending = dao_get_letters_to_be_printed(print_run_deadline, postage) + for letter in letters_awaiting_sending: + try: + letter_pdf = find_letter_pdf_in_s3(letter) + yield { + "Key": letter_pdf.key, + "Size": letter_pdf.size, + "ServiceId": str(letter.service_id), + "OrganisationId": str(letter.service.organisation_id) + } + except (BotoClientError, LetterPDFNotFound) as e: + current_app.logger.exception( + f"Error getting letter from bucket for notification: {letter.id} with reference: {letter.reference}", e) + + +def group_letters(letter_pdfs): + """ + Group letters in chunks of MAX_LETTER_PDF_ZIP_FILESIZE. Will add files to lists, never going over that size. + If a single file is (somehow) larger than MAX_LETTER_PDF_ZIP_FILESIZE that'll be in a list on it's own. + If there are no files, will just exit (rather than yielding an empty list). + """ + running_filesize = 0 + list_of_files = [] + service_id = None + for letter in letter_pdfs: + if letter['Key'].lower().endswith('.pdf'): + if not service_id: + service_id = letter['ServiceId'] + if ( + running_filesize + letter['Size'] > current_app.config['MAX_LETTER_PDF_ZIP_FILESIZE'] + or len(list_of_files) >= current_app.config['MAX_LETTER_PDF_COUNT_PER_ZIP'] + or letter['ServiceId'] != service_id + ): + yield list_of_files + running_filesize = 0 + list_of_files = [] + service_id = None + + if not service_id: + service_id = letter['ServiceId'] + running_filesize += letter['Size'] + list_of_files.append(letter) + + if list_of_files: + yield list_of_files + + +@notify_celery.task(bind=True, name='sanitise-letter', max_retries=15, default_retry_delay=300) +def sanitise_letter(self, filename): + try: + reference = get_reference_from_filename(filename) + notification = dao_get_notification_by_reference(reference) + + current_app.logger.info('Notification ID {} Virus scan passed: {}'.format(notification.id, filename)) + + if notification.status != NOTIFICATION_PENDING_VIRUS_CHECK: + current_app.logger.info('Sanitise letter called for notification {} which is in {} state'.format( + notification.id, notification.status)) + return + + notify_celery.send_task( + name=TaskNames.SANITISE_LETTER, + kwargs={ + 'notification_id': str(notification.id), + 'filename': filename, + 'allow_international_letters': notification.service.has_permission( + INTERNATIONAL_LETTERS + ), + }, + queue=QueueNames.SANITISE_LETTERS, + ) + except Exception: + try: + current_app.logger.exception( + "RETRY: calling sanitise_letter task for notification {} failed".format(notification.id) + ) + self.retry(queue=QueueNames.RETRY) + except self.MaxRetriesExceededError: + message = "RETRY FAILED: Max retries reached. " \ + "The task sanitise_letter failed for notification {}. " \ + "Notification has been updated to technical-failure".format(notification.id) + update_notification_status_by_id(notification.id, NOTIFICATION_TECHNICAL_FAILURE) + raise NotificationTechnicalFailureException(message) + + +@notify_celery.task(bind=True, name='process-sanitised-letter', max_retries=15, default_retry_delay=300) +def process_sanitised_letter(self, sanitise_data): + letter_details = encryption.verify_signature(sanitise_data) + + filename = letter_details['filename'] + notification_id = letter_details['notification_id'] + + current_app.logger.info('Processing sanitised letter with id {}'.format(notification_id)) + notification = get_notification_by_id(notification_id, _raise=True) + + if notification.status != NOTIFICATION_PENDING_VIRUS_CHECK: + current_app.logger.info( + 'process-sanitised-letter task called for notification {} which is in {} state'.format( + notification.id, notification.status) + ) + return + + try: + original_pdf_object = s3.get_s3_object(current_app.config['LETTERS_SCAN_BUCKET_NAME'], filename) + + if letter_details['validation_status'] == 'failed': + current_app.logger.info('Processing invalid precompiled pdf with id {} (file {})'.format( + notification_id, filename)) + + _move_invalid_letter_and_update_status( + notification=notification, + filename=filename, + scan_pdf_object=original_pdf_object, + message=letter_details['message'], + invalid_pages=letter_details['invalid_pages'], + page_count=letter_details['page_count'], + ) + return + + current_app.logger.info('Processing valid precompiled pdf with id {} (file {})'.format( + notification_id, filename)) + + billable_units = get_billable_units_for_letter_page_count(letter_details['page_count']) + is_test_key = notification.key_type == KEY_TYPE_TEST + + # Updating the notification needs to happen before the file is moved. This is so that if updating the + # notification fails, the task can retry because the file is in the same place. + update_letter_pdf_status( + reference=notification.reference, + status=NOTIFICATION_DELIVERED if is_test_key else NOTIFICATION_CREATED, + billable_units=billable_units, + recipient_address=letter_details['address'] + ) + + # The original filename could be wrong because we didn't know the postage. + # Now we know if the letter is international, we can check what the filename should be. + upload_file_name = generate_letter_pdf_filename( + reference=notification.reference, + created_at=notification.created_at, + ignore_folder=True, + postage=notification.postage + ) + + move_sanitised_letter_to_test_or_live_pdf_bucket( + filename, + is_test_key, + notification.created_at, + upload_file_name, + ) + # We've moved the sanitised PDF from the sanitise bucket, but still need to delete the original file: + original_pdf_object.delete() + + except BotoClientError: + # Boto exceptions are likely to be caused by the file(s) being in the wrong place, so retrying won't help - + # we'll need to manually investigate + current_app.logger.exception( + f"Boto error when processing sanitised letter for notification {notification.id} (file {filename})" + ) + update_notification_status_by_id(notification.id, NOTIFICATION_TECHNICAL_FAILURE) + raise NotificationTechnicalFailureException + except Exception: + try: + current_app.logger.exception( + "RETRY: calling process_sanitised_letter task for notification {} failed".format(notification.id) + ) + self.retry(queue=QueueNames.RETRY) + except self.MaxRetriesExceededError: + message = "RETRY FAILED: Max retries reached. " \ + "The task process_sanitised_letter failed for notification {}. " \ + "Notification has been updated to technical-failure".format(notification.id) + update_notification_status_by_id(notification.id, NOTIFICATION_TECHNICAL_FAILURE) + raise NotificationTechnicalFailureException(message) + + +def _move_invalid_letter_and_update_status( + *, notification, filename, scan_pdf_object, message=None, invalid_pages=None, page_count=None +): + try: + move_scan_to_invalid_pdf_bucket( + source_filename=filename, + message=message, + invalid_pages=invalid_pages, + page_count=page_count + ) + scan_pdf_object.delete() + + update_letter_pdf_status( + reference=notification.reference, + status=NOTIFICATION_VALIDATION_FAILED, + billable_units=0) + except BotoClientError: + current_app.logger.exception( + "Error when moving letter with id {} to invalid PDF bucket".format(notification.id) + ) + update_notification_status_by_id(notification.id, NOTIFICATION_TECHNICAL_FAILURE) + raise NotificationTechnicalFailureException + + +@notify_celery.task(name='process-virus-scan-failed') +def process_virus_scan_failed(filename): + move_failed_pdf(filename, ScanErrorType.FAILURE) + reference = get_reference_from_filename(filename) + notification = dao_get_notification_by_reference(reference) + updated_count = update_letter_pdf_status(reference, NOTIFICATION_VIRUS_SCAN_FAILED, billable_units=0) + + if updated_count != 1: + raise Exception( + "There should only be one letter notification for each reference. Found {} notifications".format( + updated_count + ) + ) + + error = VirusScanError('notification id {} Virus scan failed: {}'.format(notification.id, filename)) + current_app.logger.exception(error) + raise error + + +@notify_celery.task(name='process-virus-scan-error') +def process_virus_scan_error(filename): + move_failed_pdf(filename, ScanErrorType.ERROR) + reference = get_reference_from_filename(filename) + notification = dao_get_notification_by_reference(reference) + updated_count = update_letter_pdf_status(reference, NOTIFICATION_TECHNICAL_FAILURE, billable_units=0) + + if updated_count != 1: + raise Exception( + "There should only be one letter notification for each reference. Found {} notifications".format( + updated_count + ) + ) + error = VirusScanError('notification id {} Virus scan error: {}'.format(notification.id, filename)) + current_app.logger.exception(error) + raise error + + +def update_letter_pdf_status(reference, status, billable_units, recipient_address=None): + postage = None + if recipient_address: + # fix allow_international_letters + postage = PostalAddress(raw_address=recipient_address.replace(',', '\n'), + allow_international_letters=True + ).postage + postage = postage if postage in INTERNATIONAL_POSTAGE_TYPES else None + update_dict = {'status': status, 'billable_units': billable_units, 'updated_at': datetime.utcnow()} + if postage: + update_dict.update({'postage': postage, 'international': True}) + if recipient_address: + update_dict['to'] = recipient_address + update_dict['normalised_to'] = ''.join(recipient_address.split()).lower() + return dao_update_notifications_by_reference( + references=[reference], + update_dict=update_dict)[0] + + +def replay_letters_in_error(filename=None): + # This method can be used to replay letters that end up in the ERROR directory. + # We had an incident where clamAV was not processing the virus scan. + if filename: + move_error_pdf_to_scan_bucket(filename) + # call task to add the filename to anti virus queue + current_app.logger.info("Calling scan_file for: {}".format(filename)) + + if current_app.config['ANTIVIRUS_ENABLED']: + notify_celery.send_task( + name=TaskNames.SCAN_FILE, + kwargs={'filename': filename}, + queue=QueueNames.ANTIVIRUS, + ) + else: + # stub out antivirus in dev + sanitise_letter.apply_async( + [filename], + queue=QueueNames.LETTERS + ) + else: + error_files = get_file_names_from_error_bucket() + for item in error_files: + moved_file_name = item.key.split('/')[1] + current_app.logger.info("Calling scan_file for: {}".format(moved_file_name)) + move_error_pdf_to_scan_bucket(moved_file_name) + # call task to add the filename to anti virus queue + if current_app.config['ANTIVIRUS_ENABLED']: + notify_celery.send_task( + name=TaskNames.SCAN_FILE, + kwargs={'filename': moved_file_name}, + queue=QueueNames.ANTIVIRUS, + ) + else: + # stub out antivirus in dev + sanitise_letter.apply_async( + [filename], + queue=QueueNames.LETTERS + ) + + +@notify_celery.task(name='resanitise-pdf') +def resanitise_pdf(notification_id): + """ + `notification_id` is the notification id for a PDF letter which was either uploaded or sent using the API. + + This task calls the `recreate_pdf_for_precompiled_letter` template preview task which recreates the + PDF for a letter which is already sanitised and in the letters-pdf bucket. The new file that is generated + will then overwrite the existing letter in the letters-pdf bucket. + """ + notification = get_notification_by_id(notification_id) + + # folder_name is the folder that the letter is in the letters-pdf bucket e.g. '2021-10-10/' + folder_name = get_folder_name(notification.created_at) + + filename = generate_letter_pdf_filename( + reference=notification.reference, + created_at=notification.created_at, + ignore_folder=True, + postage=notification.postage + ) + + notify_celery.send_task( + name=TaskNames.RECREATE_PDF_FOR_PRECOMPILED_LETTER, + kwargs={ + 'notification_id': str(notification.id), + 'file_location': f'{folder_name}{filename}', + 'allow_international_letters': notification.service.has_permission( + INTERNATIONAL_LETTERS + ), + }, + queue=QueueNames.SANITISE_LETTERS, + ) diff --git a/app/celery/process_sms_client_response_tasks.py b/app/celery/process_sms_client_response_tasks.py deleted file mode 100644 index 8bdf84301..000000000 --- a/app/celery/process_sms_client_response_tasks.py +++ /dev/null @@ -1,89 +0,0 @@ -import uuid -from datetime import datetime - -from flask import current_app -from notifications_utils.template import SMSMessageTemplate - -from app import notify_celery, statsd_client -from app.celery.process_ses_receipts_tasks import check_and_queue_callback_task -from app.clients import ClientException -from app.dao import notifications_dao -from app.dao.templates_dao import dao_get_template_by_id -from app.models import NOTIFICATION_PENDING - -sms_response_mapper = { - # 'MMG': get_mmg_responses, - # 'Firetext': get_firetext_responses, -} - - -# gUpdate with new providers") -@notify_celery.task(bind=True, name="process-sms-client-response", max_retries=5, default_retry_delay=300) -def process_sms_client_response(self, status, provider_reference, client_name, detailed_status_code=None): - # validate reference - try: - uuid.UUID(provider_reference, version=4) - except ValueError as e: - current_app.logger.exception(f'{client_name} callback with invalid reference {provider_reference}') - raise e - - response_parser = sms_response_mapper[client_name] - - # validate status - try: - notification_status, detailed_status = response_parser(status, detailed_status_code) - current_app.logger.info( - f'{client_name} callback returned status of {notification_status}' - f'({status}): {detailed_status}({detailed_status_code}) for reference: {provider_reference}' - ) - except KeyError: - _process_for_status( - notification_status='technical-failure', - client_name=client_name, - provider_reference=provider_reference - ) - raise ClientException(f'{client_name} callback failed: status {status} not found.') - - _process_for_status( - notification_status=notification_status, - client_name=client_name, - provider_reference=provider_reference, - detailed_status_code=detailed_status_code - ) - - -def _process_for_status(notification_status, client_name, provider_reference, detailed_status_code=None): - # record stats - notification = notifications_dao.update_notification_status_by_id( - notification_id=provider_reference, - status=notification_status, - sent_by=client_name.lower(), - detailed_status_code=detailed_status_code - ) - if not notification: - return - - statsd_client.incr('callback.{}.{}'.format(client_name.lower(), notification_status)) - - if notification.sent_at: - statsd_client.timing_with_dates( - f'callback.{client_name.lower()}.{notification_status}.elapsed-time', - datetime.utcnow(), - notification.sent_at - ) - - if notification.billable_units == 0: - service = notification.service - template_model = dao_get_template_by_id(notification.template_id, notification.template_version) - - template = SMSMessageTemplate( - template_model.__dict__, - values=notification.personalisation, - prefix=service.name, - show_prefix=service.prefix_sms, - ) - notification.billable_units = template.fragment_count - notifications_dao.dao_update_notification(notification) - - if notification_status != NOTIFICATION_PENDING: - check_and_queue_callback_task(notification) diff --git a/app/celery/research_mode_tasks.py b/app/celery/research_mode_tasks.py index 6ed35c1fa..62344a41a 100644 --- a/app/celery/research_mode_tasks.py +++ b/app/celery/research_mode_tasks.py @@ -7,9 +7,9 @@ from app.celery.process_ses_receipts_tasks import process_ses_results from app.config import QueueNames from app.models import SMS_TYPE -temp_fail = "7700900003" -perm_fail = "7700900002" -delivered = "7700900001" +temp_fail = "2028675303" +perm_fail = "2028675302" +delivered = "2028675309" delivered_email = "delivered@simulator.notify" perm_fail_email = "perm-fail@simulator.notify" @@ -17,22 +17,8 @@ temp_fail_email = "temp-fail@simulator.notify" def send_sms_response(provider, reference, to): - if provider == "mmg": - body = mmg_callback(reference, to) - headers = {"Content-type": "application/json"} - else: - headers = {"Content-type": "application/x-www-form-urlencoded"} - body = firetext_callback(reference, to) - # to simulate getting a temporary_failure from firetext - # we need to send a pending status updated then a permanent-failure - if body['status'] == '2': # pending status - make_request(SMS_TYPE, provider, body, headers) - # 1 is a declined status for firetext, will result in a temp-failure - body = {'mobile': to, - 'status': "1", - 'time': '2016-03-10 14:17:00', - 'reference': reference - } + body = sns_callback(reference, to) + headers = {"Content-type": "application/json"} make_request(SMS_TYPE, provider, body, headers) @@ -73,44 +59,25 @@ def make_request(notification_type, provider, data, headers): return response.json() -def mmg_callback(notification_id, to): - """ - status: 3 - delivered - status: 4 - expired (temp failure) - status: 5 - rejected (perm failure) - """ +def sns_callback(notification_id, to): + raise Exception("Need to update for SNS callback format along with test_send_to_providers") - if to.strip().endswith(temp_fail): - status = "4" - elif to.strip().endswith(perm_fail): - status = "5" - else: - status = "3" + # example from mmg_callback + # if to.strip().endswith(temp_fail): + # # status: 4 - expired (temp failure) + # status = "4" + # elif to.strip().endswith(perm_fail): + # # status: 5 - rejected (perm failure) + # status = "5" + # else: + # # status: 3 - delivered + # status = "3" - return json.dumps({"reference": "mmg_reference", - "CID": str(notification_id), - "MSISDN": to, - "status": status, - "deliverytime": "2016-04-05 16:01:07"}) - - -def firetext_callback(notification_id, to): - """ - status: 0 - delivered - status: 1 - perm failure - """ - if to.strip().endswith(perm_fail): - status = "1" - elif to.strip().endswith(temp_fail): - status = "2" - else: - status = "0" - return { - 'mobile': to, - 'status': status, - 'time': '2016-03-10 14:17:00', - 'reference': notification_id - } + # return json.dumps({"reference": "mmg_reference", + # "CID": str(notification_id), + # "MSISDN": to, + # "status": status, + # "deliverytime": "2016-04-05 16:01:07"}) def ses_notification_callback(reference): diff --git a/app/commands.py b/app/commands.py index f88e6c0b1..9084db19a 100644 --- a/app/commands.py +++ b/app/commands.py @@ -60,6 +60,8 @@ from app.models import ( Notification, Organisation, Service, + Template, + TemplateHistory, User, ) from app.utils import get_local_midnight_in_utc @@ -142,12 +144,11 @@ def purge_functional_test_data(user_email_prefix): @notify_command(name='insert-inbound-numbers') @click.option('-f', '--file_name', required=True, - help="""Full path of the file to upload, file is a contains inbound numbers, - one number per line. The number must have the format of 07... not 447....""") + help="""Full path of the file to upload, file is a contains inbound numbers, one number per line.""") def insert_inbound_numbers_from_file(file_name): print("Inserting inbound numbers from {}".format(file_name)) with open(file_name) as file: - sql = "insert into inbound_numbers values('{}', '{}', 'mmg', null, True, now(), null);" + sql = "insert into inbound_numbers values('{}', '{}', 'sns', null, True, now(), null);" for line in file: line = line.strip() @@ -733,3 +734,28 @@ def create_user_jwt(token): service_id = token[-73:-37] api_key = token[-36:] print(create_jwt_token(api_key, service_id)) + + +def _update_template(id, name, template_type, content, subject): + + template = Template.query.filter_by(id=id).first() + template.name = name + template.template_type = template_type + template.content = '\n'.join(content) + template.subject = subject + + history = TemplateHistory.query.filter_by(id=id).first() + history.name = name + history.template_type = template_type + history.content = '\n'.join(content) + history.subject = subject + + db.session.commit() + + +@notify_command(name='update-templates') +def update_templates(): + with open(current_app.config['CONFIG_FILES'] + '/templates.json') as f: + data = json.load(f) + for d in data: + _update_template(d['id'], d['name'], d['type'], d['content'], d['subject']) diff --git a/app/config.py b/app/config.py index 491ec24ec..2c1a189a4 100644 --- a/app/config.py +++ b/app/config.py @@ -1,6 +1,6 @@ import json from datetime import timedelta -from os import getenv +from os import getenv, path from celery.schedules import crontab from kombu import Exchange, Queue @@ -108,23 +108,10 @@ class Config(object): VALIDATE_SNS_TOPICS = True VALID_SNS_TOPICS = ['notify_test_bounce', 'notify_test_success', 'notify_test_complaint', 'notify_test_sms_inbound'] - # SMS config to be cleaned up during https://github.com/GSA/notifications-api/issues/7 - # MMG API Key - MMG_API_KEY = getenv('MMG_API_KEY', 'placeholder') - # Firetext API Key - FIRETEXT_API_KEY = getenv("FIRETEXT_API_KEY", "placeholder") - FIRETEXT_INTERNATIONAL_API_KEY = getenv("FIRETEXT_INTERNATIONAL_API_KEY", "placeholder") # these should always add up to 100% SMS_PROVIDER_RESTING_POINTS = { 'sns': 100, - 'mmg': 0, - 'firetext': 0 } - FIRETEXT_INBOUND_SMS_AUTH = json.loads(getenv('FIRETEXT_INBOUND_SMS_AUTH', '[]')) - MMG_INBOUND_SMS_AUTH = json.loads(getenv('MMG_INBOUND_SMS_AUTH', '[]')) - MMG_INBOUND_SMS_USERNAME = json.loads(getenv('MMG_INBOUND_SMS_USERNAME', '[]')) - MMG_URL = getenv("MMG_URL", "https://api.mmg.co.uk/jsonv2a/api.php") - FIRETEXT_URL = getenv("FIRETEXT_URL", "https://www.firetext.co.uk/api/sendsms/json") # Zendesk ZENDESK_API_KEY = getenv('ZENDESK_API_KEY') @@ -154,6 +141,9 @@ class Config(object): MAX_LETTER_PDF_ZIP_FILESIZE = 40 * 1024 * 1024 # 40mb MAX_LETTER_PDF_COUNT_PER_ZIP = 500 + # Default data + CONFIG_FILES = path.dirname(__file__) + '/config_files/' + NOTIFY_SERVICE_ID = 'd6aa2c68-a2d9-4437-ab19-3ae8eb202553' NOTIFY_USER_ID = '6af522d0-2915-4e52-83a3-3690455a5fe6' INVITATION_EMAIL_TEMPLATE_ID = '4f46df42-f795-4cc4-83bb-65ca312f49cc' @@ -285,7 +275,7 @@ class Config(object): 'simulate-delivered-2@notifications.service.gov.uk', 'simulate-delivered-3@notifications.service.gov.uk', ) - SIMULATED_SMS_NUMBERS = ('+447700900000', '+447700900111', '+447700900222') + SIMULATED_SMS_NUMBERS = ('+12028675000', '+12028675111', '+12028675222') FREE_SMS_TIER_FRAGMENT_COUNT = 250000 @@ -317,7 +307,7 @@ class Development(Config): CONTACT_LIST_BUCKET = _default_s3_credentials('local-contact-list') # credential overrides - DANGEROUS_SALT = 'dev-notify-salt' + DANGEROUS_SALT = 'development-notify-salt' SECRET_KEY = 'dev-notify-secret-key' # nosec B105 - this is only used in development INTERNAL_CLIENT_API_KEYS = {Config.ADMIN_CLIENT_ID: ['dev-notify-secret-key']} ALLOW_EXPIRED_API_TOKEN = getenv('ALLOW_EXPIRED_API_TOKEN', '0') == '1' @@ -329,10 +319,6 @@ class Test(Development): ANTIVIRUS_ENABLED = True DVLA_EMAIL_ADDRESSES = ['success@simulator.amazonses.com', 'success+2@simulator.amazonses.com'] - FIRETEXT_INBOUND_SMS_AUTH = ['testkey'] - MMG_INBOUND_SMS_AUTH = ['testkey'] - MMG_INBOUND_SMS_USERNAME = ['username'] - HIGH_VOLUME_SERVICE = [ '941b6f9a-50d7-4742-8d50-f365ca74bf27', '63f95b86-2d19-4497-b8b2-ccf25457df4e', diff --git a/app/config_files/templates.json b/app/config_files/templates.json new file mode 100644 index 000000000..ae208541b --- /dev/null +++ b/app/config_files/templates.json @@ -0,0 +1,274 @@ +[ + { + "id": "42a23d19-504e-49bb-a95e-4976baff4757", + "name": "Example text message template", + "type": "sms", + "subject": "", + "content": ["Hi, I’m trying out U.S. Notify. Today is ((day of week)) and my favorite color is ((color))."] + }, + { + "id": "4f46df42-f795-4cc4-83bb-65ca312f49cc", + "name": "Notify invitation email", + "type": "email", + "subject": "((user_name)) has invited you to collaborate on ((service_name)) on U.S. Notify", + "content": ["((user_name)) has invited you to collaborate on ((service_name)) on U.S. Notify.", + "", + "", + "U.S. Notify makes it easy to keep people updated by helping you send text messages, emails and letters.", + "", + "", + "Click this link to create an account on U.S. Notify:", + "", + "((url))", + "", + "", + "This invitation will stop working at midnight tomorrow. This is to keep ((service_name)) secure."] + }, + { + "id": "36fb0730-6259-4da1-8a80-c8de22ad4246", + "name": "Notify SMS verify code", + "type": "sms", + "subject": "", + "content": ["((verify_code)) is your U.S. Notify authentication code"] + }, + { + "id": "474e9242-823b-4f99-813d-ed392e7f1201", + "name": "Notify password reset email", + "type": "email", + "subject": "Reset your U.S. Notify password", + "content": ["Hi ((user_name)),", + "", + "", + "We received a request to reset your password on U.S. Notify.", + "", + "", + "If you didn’t request this email, you can ignore it – your password has not been changed.", + "", + "", + "To reset your password, click this link:","","","((url))"] + }, + { + "id": "299726d2-dba6-42b8-8209-30e1d66ea164", + "name": "Notify email verify code", + "type": "email", + "subject": "Sign in to U.S. Notify", + "content": ["Hi ((name)),", + "", + "", + "To sign in to U.S. Notify please open this link:", + "", + "", + "((url))"] + }, + { + "id": "ece42649-22a8-4d06-b87f-d52d5d3f0a27", + "name": "Notify email verification code", + "type": "email", + "subject": "Confirm U.S. Notify registration", + "content": ["Hi ((name)),", + "", + "", + "To complete your registration for U.S. Notify please click the link below", + "", + "", + "((url))"] + }, + { + "id": "0880fbb1-a0c6-46f0-9a8e-36c986381ceb", + "name": "Your U.S. Notify account", + "type": "email", + "subject": "Your U.S. Notify account", + "content": ["You already have a U.S. Notify account with this email address.", + "", + "", + "Sign in here: ((signin_url))", + "", + "", + "If you’ve forgotten your password, you can reset it here: ((forgot_password_url))", + "", + "", + "", + "", + "If you didn’t try to register for a U.S. Notify account recently, please let us know here: ((feedback_url))"] + }, + { + "id": "eb4d9930-87ab-4aef-9bce-786762687884", + "name": "Confirm new email address", + "type": "email", + "subject": "Confirm your email address for U.S. Notify", + "content": ["Hi ((name)),","","","Click this link to confirm your new email address:", + "", + "", + "((url))", + "", + "", + "If you didn’t try to change the email address for your U.S. Notify account, let us know here:", + "", + "", + "((feedback_url))"] + }, + { + "id": "618185c6-3636-49cd-b7d2-6f6f5eb3bdde", + "name": "Automated \"You’re now live\" message", + "type": "email", + "subject": "((service name)) is now live on U.S. Notify", + "content": ["Hi ((name)),", + "", + "", + "((service name)) is now live on U.S. Notify.", + "", + "", + "You can send up to ((message limit)) messages per day.", + "", + "", + "As a live service, you’ll need to know who to contact if you have a question, or something goes wrong.", + "", + "", + "If our system status page shows a problem, then we’ve been alerted and are working on it – you don’t need to contact us.", + "", + "", + "#Problems or questions out of hours", + "", + "", + "We offer out of hours support for emergencies.", + "", + "", + "It’s only an emergency if:", + "", + "* no one in your team can log in", + "", + "* a β€˜technical difficulties’ error appears when you try to upload a file", + "", + "* a 500 response code appears when you try to send messages using the API", + "", + "", + "If you have one of these emergencies, email details to:", + "", + "notify-support@gsa.gov", + "", + "", + "^Only use this email address for out of hours emergencies. Don’t share this address with people outside of your team.", + "", + "", + "We’ll get back to you within 30 minutes and give you hourly updates until the problem’s fixed.", + "", + "", + "For non-emergency problems or questions, use our support page and we’ll reply in office hours.", + "", + "", + "Thanks", + "", + "U.S. Notify team"] + }, + { + "id": "203566f0-d835-47c5-aa06-932439c86573", + "name": "Notify organization invitation email", + "type": "email", + "subject": "((user_name)) has invited you to collaborate on ((organisation_name)) on U.S. Notify", + "content": ["((user_name)) has invited you to collaborate on ((organisation_name)) on U.S. Notify.","","","U.S. Notify makes it easy to keep people updated by helping you send text messages, emails and letters.","","","Open this link to create an account on U.S. Notify:","","((url))","","","This invitation will stop working at midnight tomorrow. This is to keep ((organisation_name)) secure."] + }, + { + "id": "c73f1d71-4049-46d5-a647-d013bdeca3f0", + "name": "Email address changed by service manager", + "type": "email", + "subject": "Your U.S. Notify email address has changed", + "content": ["Dear ((name)),","","","((servicemanagername)) changed your Notify account email address to:","","","((email address))","","","You’ll need to use this email address next time you sign in.","","","Thanks","","","U.S. Notify team"] + }, + { + "id": "8a31520f-4751-4789-8ea1-fe54496725eb", + "name": "Phone number changed by service manager", + "type": "sms", + "subject": "", + "content": ["Your mobile number was changed by ((servicemanagername)). Next time you sign in, your U.S. Notify authentication code will be sent to this phone."] + }, + { + "id": "a42f1d17-9404-46d5-a647-d013bdfca3e1", + "name": "Verify email reply-to address for a service", + "type": "email", + "subject": "Your U.S. Notify reply-to email address", + "content": ["Hi,","","","This address has been provided as a reply-to email address for a U.S. Notify account.","","Any replies from users to emails they receive through U.S. Notify will come back to this email address.","","","This is just a quick check to make sure the address is valid.","","","No need to reply.","","","Thanks","","","U.S. Notify team"] + }, + { + "id": "4fd2e43c-309b-4e50-8fb8-1955852d9d71", + "name": "MOU Signed By Receipt", + "type": "email", + "subject": "You’ve accepted the U.S. Notify data sharing and financial agreement", + "content": [ + "Hi ((signed_by_name)),", + "", + "((org_name)) has accepted the U.S. Notify data sharing and financial agreement. ", + "", + "If you need another copy of the agreement you can download it here: ((mou_link))", + "", + "", + "Thanks,", + "U.S. Notify team" + ] + }, + { + "id": "c20206d5-bf03-4002-9a90-37d5032d9e84", + "name": "MOU Signed On Behalf Of Receipt - Signed by", + "type": "email", + "subject": "You’ve accepted the U.S. Notify data sharing and financial agreement", + "content": [ + "Hi ((signed_by_name)),", + "", + "((org_name)) has accepted the U.S. Notify data sharing and financial agreement. We’ve emailed ((on_behalf_of_name)) to let them know too.", + "", + "If you need another copy of the agreement you can download it here: ((mou_link))", + "", + "", + "Thanks,", + "U.S. Notify team" + ] + }, + { + "id": "522b6657-5ca5-4368-a294-6b527703bd0b", + "name": "MOU Signed On Behalf Of Receipt - On Behalf Of", + "type": "email", + "subject": "((org_name)) has accepted the U.S. Notify data sharing and financial agreement", + "content": [ + "Hi ((on_behalf_of_name)),", + "", + "((signed_by_name)) has accepted the U.S. Notify data sharing and financial agreement on your behalf, for ((org_name)).", + "", + "U.S. Notify lets teams in the public sector send emails, text messages and letters. It’s built and run by a team in the TTS Public Benefits Studio (part of GSA).", + "", + "If you need another copy of the agreement you can download it here: ((mou_link))", + "", + "", + "Thanks,", + "U.S. Notify team" + ] + }, + { + "id": "d0e66c4c-0c50-43f0-94f5-f85b613202d4", + "name": "MOU Signed Notify Team Alert", + "type": "email", + "subject": "Someone signed an MOU for an org on Notify", + "content": [ + "What’s up Notifiers,", + "", + "((signed_by_name)) just accepted the data sharing and financial agreement for ((org_name)).", + "", + "See how ((org_name)) is using Notify here: ((org_dashboard_link))" + ] + }, + { + "id": "11fad854-fd38-4a7c-bd17-805fb13dfc12", + "name": "Notify daily letter volumes", + "type": "email", + "subject": "Notify letter volume for ((date)): ((total_volume)) letters, ((total_sheets)) sheets", + "content": [ + "((total_volume)) letters (((total_sheets)) sheets) sent via Notify are coming in today’s batch. These include: ", + "", + "((first_class_volume)) first class letters (((first_class_sheets)) sheets).", + "((second_class_volume)) second class letters (((second_class_sheets)) sheets).", + "((international_volume)) international letters (((international_sheets)) sheets).", + "", + "Thanks", + "", + "U.S. Notify team" + ] + } +] diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index 919eab075..2fee15805 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -101,7 +101,7 @@ def _update_notification_status(notification, status, provider_response=None): @autocommit -def update_notification_status_by_id(notification_id, status, sent_by=None, detailed_status_code=None): +def update_notification_status_by_id(notification_id, status, sent_by=None): notification = Notification.query.with_for_update().filter(Notification.id == notification_id).first() if not notification: @@ -131,8 +131,7 @@ def update_notification_status_by_id(notification_id, status, sent_by=None, deta notification.sent_by = sent_by return _update_notification_status( notification=notification, - status=status, - detailed_status_code=detailed_status_code + status=status ) diff --git a/app/dao/provider_details_dao.py b/app/dao/provider_details_dao.py index 15ed99505..2fc03d424 100644 --- a/app/dao/provider_details_dao.py +++ b/app/dao/provider_details_dao.py @@ -24,10 +24,8 @@ def get_provider_details_by_identifier(identifier): def get_alternative_sms_provider(identifier): - if identifier == 'firetext': - return 'mmg' - elif identifier == 'mmg': - return 'firetext' + if identifier == 'sns': + raise Exception("No alternative SMS providers currently available") raise ValueError('Unrecognised sms provider {}'.format(identifier)) diff --git a/app/delivery/send_to_providers.py b/app/delivery/send_to_providers.py index c1532e83e..84feb52cc 100644 --- a/app/delivery/send_to_providers.py +++ b/app/delivery/send_to_providers.py @@ -111,7 +111,7 @@ def send_email_to_provider(notification): technical_failure(notification=notification) return if notification.status == 'created': - provider = provider_to_use(EMAIL_TYPE) + provider = provider_to_use(EMAIL_TYPE, False) template_dict = SerialisedTemplate.from_id_and_service_id( template_id=notification.template_id, service_id=service.id, version=notification.template_version @@ -175,12 +175,10 @@ provider_cache = TTLCache(maxsize=8, ttl=10) @cached(cache=provider_cache) def provider_to_use(notification_type, international=True): - international = False # TODO: remove or resolve the functionality of this flag - # TODO rip firetext and mmg out of early migrations and clean up the expression below active_providers = [ p for p in get_provider_details_by_notification_type( notification_type, international - ) if p.active and p.identifier not in ['firetext', 'mmg'] + ) if p.active ] if not active_providers: diff --git a/app/models.py b/app/models.py index 098857644..828c283ea 100644 --- a/app/models.py +++ b/app/models.py @@ -3,6 +3,9 @@ import itertools import uuid from flask import current_app, url_for +from notifications_utils.clients.encryption.encryption_client import ( + EncryptionError, +) from notifications_utils.insensitive_dict import InsensitiveDict from notifications_utils.letter_timings import get_letter_timings from notifications_utils.postal_address import ( @@ -33,6 +36,7 @@ from sqlalchemy.dialects.postgresql import JSON, JSONB, UUID from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import validates from sqlalchemy.orm.collections import attribute_mapped_collection from app import db, encryption @@ -137,6 +141,14 @@ class User(db.Model): secondary='user_to_organisation', backref='users') + @validates("mobile_number") + def validate_mobile_number(self, key, number): + try: + if number is not None: + return validate_phone_number(number, international=True) + except InvalidPhoneError as err: + raise ValueError(str(err)) from err + @property def password(self): raise AttributeError("Password not readable") @@ -705,11 +717,9 @@ class ServiceGuestList(db.Model): try: if recipient_type == MOBILE_TYPE: - validate_phone_number(recipient, international=True) - instance.recipient = recipient + instance.recipient = validate_phone_number(recipient, international=True) elif recipient_type == EMAIL_TYPE: - validate_email_address(recipient) - instance.recipient = recipient + instance.recipient = validate_email_address(recipient) else: raise ValueError('Invalid recipient type') except InvalidPhoneError: @@ -1113,12 +1123,10 @@ class TemplateHistory(TemplateBase): ) -MMG_PROVIDER = "mmg" -FIRETEXT_PROVIDER = "firetext" SNS_PROVIDER = 'sns' SES_PROVIDER = 'ses' -SMS_PROVIDERS = [MMG_PROVIDER, FIRETEXT_PROVIDER, SNS_PROVIDER] +SMS_PROVIDERS = [SNS_PROVIDER] EMAIL_PROVIDERS = [SES_PROVIDER] PROVIDERS = SMS_PROVIDERS + EMAIL_PROVIDERS @@ -1510,7 +1518,10 @@ class Notification(db.Model): @property def personalisation(self): if self._personalisation: - return encryption.decrypt(self._personalisation) + try: + return encryption.decrypt(self._personalisation) + except EncryptionError: + current_app.logger.error("Error decrypting notification.personalisation, returning empty dict") return {} @personalisation.setter diff --git a/app/notifications/notifications_sms_callback.py b/app/notifications/notifications_sms_callback.py deleted file mode 100644 index 4448191d2..000000000 --- a/app/notifications/notifications_sms_callback.py +++ /dev/null @@ -1,60 +0,0 @@ -from flask import Blueprint - -from app.errors import register_errors - -sms_callback_blueprint = Blueprint("sms_callback", __name__, url_prefix="/notifications/sms") -register_errors(sms_callback_blueprint) - -# TODO SNS SMS delivery receipts delivered here - -# @sms_callback_blueprint.route('/mmg', methods=['POST']) -# def process_mmg_response(): -# client_name = 'MMG' -# data = json.loads(request.data) -# errors = validate_callback_data(data=data, -# fields=['status', 'CID'], -# client_name=client_name) -# if errors: -# raise InvalidRequest(errors, status_code=400) - -# status = str(data.get('status')) -# detailed_status_code = str(data.get('substatus')) - -# provider_reference = data.get('CID') - -# process_sms_client_response.apply_async( -# [status, provider_reference, client_name, detailed_status_code], -# queue=QueueNames.SMS_CALLBACKS, -# ) - -# return jsonify(result='success'), 200 - - -# @sms_callback_blueprint.route('/firetext', methods=['POST']) -# def process_firetext_response(): -# client_name = 'Firetext' -# errors = validate_callback_data(data=request.form, -# fields=['status', 'reference'], -# client_name=client_name) -# if errors: -# raise InvalidRequest(errors, status_code=400) - -# status = request.form.get('status') -# detailed_status_code = request.form.get('code') -# provider_reference = request.form.get('reference') - -# process_sms_client_response.apply_async( -# [status, provider_reference, client_name, detailed_status_code], -# queue=QueueNames.SMS_CALLBACKS, -# ) - -# return jsonify(result='success'), 200 - - -def validate_callback_data(data, fields, client_name): - errors = [] - for f in fields: - if not str(data.get(f, '')): - error = "{} callback failed: {} missing".format(client_name, f) - errors.append(error) - return errors if len(errors) > 0 else None diff --git a/app/notifications/process_notifications.py b/app/notifications/process_notifications.py index b831de7bb..0e120ed21 100644 --- a/app/notifications/process_notifications.py +++ b/app/notifications/process_notifications.py @@ -104,13 +104,13 @@ def persist_notification( document_download_count=None, updated_at=None ): - current_app.logger.info('Presisting notification') + current_app.logger.info('Persisting notification') notification_created_at = created_at or datetime.utcnow() if not notification_id: notification_id = uuid.uuid4() - current_app.logger.info('Presisting notification with id {}'.format(notification_id)) + current_app.logger.info('Persisting notification with id {}'.format(notification_id)) notification = Notification( id=notification_id, @@ -135,7 +135,7 @@ def persist_notification( updated_at=updated_at ) - current_app.logger.info('Presisting notification with to address: {}'.format(notification.to)) + current_app.logger.info('Persisting notification with to address: {}'.format(notification.to)) if notification_type == SMS_TYPE: formatted_recipient = validate_and_format_phone_number(recipient, international=True) @@ -145,9 +145,9 @@ def persist_notification( notification.phone_prefix = recipient_info.country_prefix notification.rate_multiplier = recipient_info.billable_units elif notification_type == EMAIL_TYPE: - current_app.logger.info('Presisting notification with type: {}'.format(EMAIL_TYPE)) + current_app.logger.info('Persisting notification with type: {}'.format(EMAIL_TYPE)) notification.normalised_to = format_email_address(notification.to) - current_app.logger.info('Presisting notification to formatted email: {}'.format(notification.normalised_to)) + current_app.logger.info('Persisting notification to formatted email: {}'.format(notification.normalised_to)) elif notification_type == LETTER_TYPE: notification.postage = postage notification.international = postage in INTERNATIONAL_POSTAGE_TYPES diff --git a/app/notifications/receive_notifications.py b/app/notifications/receive_notifications.py index 03009a791..ae0997217 100644 --- a/app/notifications/receive_notifications.py +++ b/app/notifications/receive_notifications.py @@ -1,8 +1,4 @@ -from datetime import datetime -from urllib.parse import unquote - -import iso8601 -from flask import Blueprint, abort, current_app, json, jsonify, request +from flask import Blueprint, current_app, json, jsonify, request from gds_metrics.metrics import Counter from notifications_utils.recipients import try_validate_and_format_phone_number @@ -93,113 +89,10 @@ def receive_sns_sms(): ), 200 -@receive_notifications_blueprint.route('/notifications/sms/receive/mmg', methods=['POST']) -def receive_mmg_sms(): - """ - { - 'MSISDN': '447123456789' - 'Number': '40604', - 'Message': 'some+uri+encoded+message%3A', - 'ID': 'SOME-MMG-SPECIFIC-ID', - 'DateRecieved': '2017-05-21+11%3A56%3A11' - } - """ - post_data = request.get_json() - - auth = request.authorization - - if not auth: - current_app.logger.warning("Inbound sms (MMG) no auth header") - abort(401) - elif auth.username not in current_app.config['MMG_INBOUND_SMS_USERNAME'] \ - or auth.password not in current_app.config['MMG_INBOUND_SMS_AUTH']: - current_app.logger.warning("Inbound sms (MMG) incorrect username ({}) or password".format(auth.username)) - abort(403) - - inbound_number = strip_leading_forty_four(post_data['Number']) - - service = fetch_potential_service(inbound_number, 'mmg') - if not service: - # since this is an issue with our service <-> number mapping, or no inbound_sms service permission - # we should still tell MMG that we received it successfully - return 'RECEIVED', 200 - - INBOUND_SMS_COUNTER.labels("mmg").inc() - - inbound = create_inbound_sms_object(service, - content=format_mmg_message(post_data["Message"]), - from_number=post_data['MSISDN'], - provider_ref=post_data["ID"], - date_received=post_data.get('DateRecieved'), - provider_name="mmg") - - tasks.send_inbound_sms_to_service.apply_async([str(inbound.id), str(service.id)], queue=QueueNames.NOTIFY) - - current_app.logger.debug( - '{} received inbound SMS with reference {} from MMG'.format(service.id, inbound.provider_reference)) - return jsonify({ - "status": "ok" - }), 200 - - -@receive_notifications_blueprint.route('/notifications/sms/receive/firetext', methods=['POST']) -def receive_firetext_sms(): - post_data = request.form - - auth = request.authorization - if not auth: - current_app.logger.warning("Inbound sms (Firetext) no auth header") - abort(401) - elif auth.username != 'notify' or auth.password not in current_app.config['FIRETEXT_INBOUND_SMS_AUTH']: - current_app.logger.warning("Inbound sms (Firetext) incorrect username ({}) or password".format(auth.username)) - abort(403) - - inbound_number = strip_leading_forty_four(post_data['destination']) - - service = fetch_potential_service(inbound_number, 'firetext') - if not service: - return jsonify({ - "status": "ok" - }), 200 - - inbound = create_inbound_sms_object(service=service, - content=post_data["message"], - from_number=post_data['source'], - provider_ref=None, - date_received=post_data['time'], - provider_name="firetext") - - INBOUND_SMS_COUNTER.labels("firetext").inc() - - tasks.send_inbound_sms_to_service.apply_async([str(inbound.id), str(service.id)], queue=QueueNames.NOTIFY) - current_app.logger.debug( - '{} received inbound SMS with reference {} from Firetext'.format(service.id, inbound.provider_reference)) - return jsonify({ - "status": "ok" - }), 200 - - -def format_mmg_message(message): - return unescape_string(unquote(message.replace('+', ' '))) - - def unescape_string(string): return string.encode('raw_unicode_escape').decode('unicode_escape') -def format_mmg_datetime(date): - """ - We expect datetimes in format 2017-05-21+11%3A56%3A11 - ie, spaces replaced with pluses, and URI encoded - and in UTC - """ - try: - orig_date = format_mmg_message(date) - parsed_datetime = iso8601.parse_date(orig_date).replace(tzinfo=None) - return parsed_datetime - except iso8601.ParseError: - return datetime.utcnow() - - def create_inbound_sms_object(service, content, from_number, provider_ref, date_received, provider_name): user_number = try_validate_and_format_phone_number( from_number, @@ -208,9 +101,6 @@ def create_inbound_sms_object(service, content, from_number, provider_ref, date_ ) provider_date = date_received - if provider_date: - provider_date = format_mmg_datetime(provider_date) - inbound = InboundSms( service=service, notify_number=service.get_inbound_number(), @@ -244,9 +134,3 @@ def fetch_potential_service(inbound_number, provider_name): def has_inbound_sms_permissions(permissions): str_permissions = [p.permission for p in permissions] return set([INBOUND_SMS_TYPE, SMS_TYPE]).issubset(set(str_permissions)) - - -def strip_leading_forty_four(number): - if number.startswith('44'): - return number.replace('44', '0', 1) - return number diff --git a/app/notifications/validators.py b/app/notifications/validators.py index 03a98b812..7a079df66 100644 --- a/app/notifications/validators.py +++ b/app/notifications/validators.py @@ -151,10 +151,7 @@ def check_if_service_can_send_to_number(service, number): else: permissions = service.permissions - if ( - # if number is international and not a crown dependency - international_phone_info.international and not international_phone_info.crown_dependency - ) and INTERNATIONAL_SMS_TYPE not in permissions: + if international_phone_info.international and INTERNATIONAL_SMS_TYPE not in permissions: raise BadRequestError(message="Cannot send to international mobile numbers") else: return international_phone_info diff --git a/app/user/rest.py b/app/user/rest.py index 3ec2adb9b..6d85ddd98 100644 --- a/app/user/rest.py +++ b/app/user/rest.py @@ -5,7 +5,7 @@ from urllib.parse import urlencode from flask import Blueprint, abort, current_app, jsonify, request from notifications_utils.recipients import ( - is_uk_phone_number, + is_us_phone_number, use_numeric_sender, ) from sqlalchemy.exc import IntegrityError @@ -140,7 +140,7 @@ def update_user_attribute(user_id): def get_sms_reply_to_for_notify_service(recipient, template): - if not is_uk_phone_number(recipient) and use_numeric_sender(recipient): + if not is_us_phone_number(recipient) and use_numeric_sender(recipient): reply_to = current_app.config['NOTIFY_INTERNATIONAL_SMS_SENDER'] else: reply_to = template.service.get_default_sms_sender() diff --git a/deploy-config/egress_proxy/notify-api-demo.allow.acl b/deploy-config/egress_proxy/notify-api-demo.allow.acl index 711a1adfd..fcd95f284 100644 --- a/deploy-config/egress_proxy/notify-api-demo.allow.acl +++ b/deploy-config/egress_proxy/notify-api-demo.allow.acl @@ -1,2 +1,4 @@ email.us-west-2.amazonaws.com sns.us-west-2.amazonaws.com +gov-collector.newrelic.com +egress-proxy-notify-api-demo.apps.internal diff --git a/deploy-config/egress_proxy/notify-api-staging.allow.acl b/deploy-config/egress_proxy/notify-api-staging.allow.acl index 711a1adfd..9f16b1edf 100644 --- a/deploy-config/egress_proxy/notify-api-staging.allow.acl +++ b/deploy-config/egress_proxy/notify-api-staging.allow.acl @@ -1,2 +1,4 @@ email.us-west-2.amazonaws.com sns.us-west-2.amazonaws.com +gov-collector.newrelic.com +egress-proxy-notify-api-staging.apps.internal diff --git a/deploy-config/sandbox.yml b/deploy-config/sandbox.yml index 70e1da125..aafa9b491 100644 --- a/deploy-config/sandbox.yml +++ b/deploy-config/sandbox.yml @@ -6,6 +6,6 @@ worker_memory: 512M scheduler_memory: 256M public_api_route: notify-api-sandbox.app.cloud.gov admin_base_url: https://notify-sandbox.app.cloud.gov -ADMIN_CLIENT_SECRET: dev-notify-secret-key -DANGEROUS_SALT: dev-notify-salt -SECRET_KEY: dev-notify-secret-key +ADMIN_CLIENT_SECRET: sandbox-notify-secret-key +DANGEROUS_SALT: sandbox-notify-salt +SECRET_KEY: sandbox-notify-secret-key diff --git a/docker-compose.devcontainer.yml b/docker-compose.devcontainer.yml index 62bdfad51..d08ca2972 100644 --- a/docker-compose.devcontainer.yml +++ b/docker-compose.devcontainer.yml @@ -48,7 +48,6 @@ services: - redis links: - db - restart: always worker: container_name: worker image: dev-notification-api diff --git a/docs/data-storage.md b/docs/data-storage.md new file mode 100644 index 000000000..1ab7cf3ed --- /dev/null +++ b/docs/data-storage.md @@ -0,0 +1,70 @@ +Data Storage Policies & Procedures +================================== + + +Potential PII Locations +----------------------- + +### Tables + +#### users1 + +* name +* email_address +* mobile_number + +#### invited_users1 + +* email_address + +#### invited_organization_users1 + +* email_address + +#### jobs + +No db data is PII, but each job has a csv file in s3 containing phone numbers and personalization data. + +#### notifications + +* to +* normalized_to +* _personalization2 +* phone_prefix3 + +#### notification_history + +* phone_prefix3 + +#### inbound_sms + +* content2 +* user_number + +#### events + +* data (contains user IP addresses)1 + +### Notes + +#### Note 1. + +Users and invited users are Federal, State, or Local government employees or contractors. Members of the general public are _not_ users of the system + +#### Note 2. + +Field-level encryption is used on these fields. + +Details on encryption schemes and algorithms can be found in [SC-28(1)](https://github.com/GSA/us-notify-compliance/blob/main/dist/system-security-plans/lato/sc-28.1.md) + +#### Note 3. + +Probably not PII, this is the country code of the phone. + + +Data Retention Policy +--------------------- + +Seven (7) days by default. Each service can be set with a custom policy via `ServiceDataRetention` by a Platform Admin. The `ServiceDataRetention` setting applies per-service and per-message type and controls both entries in the `notifications` table as well as `csv` contact files uploaded to s3 + +Data cleanup is controlled by several tasks in the `nightly_tasks.py` file, kicked off by Celery Beat. diff --git a/docs/deploying.md b/docs/deploying.md index 15c41afbc..94d85af36 100644 --- a/docs/deploying.md +++ b/docs/deploying.md @@ -42,7 +42,10 @@ for experimenting with infrastructure changes without going through the full CI/ Rules for use: 1. Ensure that no other developer is using the environment, as there is nothing stopping changes from overwriting each other. -1. Clean up services you create when you are done. `terraform destroy` from within the `terraform/sandbox` directory should do it. +1. Clean up when you are done: + - `terraform destroy` from within the `terraform/sandbox` directory will take care of the provisioned services + - Delete the apps and routes shown in `cf apps` by running `cf delete APP_NAME -r` + - Delete the space deployers still shown in `cf services` by running `terraform/destroy_service_account.sh -s notify-sandbox -u ` ### Deploying to the sandbox diff --git a/docs/infra-overview.md b/docs/infra-overview.md index db017f8f3..301d618d4 100644 --- a/docs/infra-overview.md +++ b/docs/infra-overview.md @@ -22,16 +22,21 @@ In SNS, we have 3 topics for SMS receipts. These are not currently functional, s Through Pinpoint, the API needs at least one number so that the application itself can send SMS for authentication codes. -The API also has access to AWS S3 buckets for storing CSVs of messages and contact lists. It does not access a third S3 bucket that stores agency logos. +The API also has access to AWS S3 buckets for storing CSVs of messages and contact lists. It does not access a third S3 bucket that stores agency logos. We may be able to provision these services through cloud.gov, as well. In addition to [s3 support](https://cloud.gov/docs/services/s3/), there is [an SES brokerpak](https://github.com/GSA-TTS/datagov-brokerpak-smtp) and work on an SNS brokerpak. +## New Relic + +We are using [New Relic](https://one.newrelic.com/nr1-core?account=3389907) for application monitoring and error reporting. When requesting access to New Relic, ask to be added to the Benefits-Studio subaccount. + ## Onboarding - [ ] Join [the GSA GitHub org](https://github.com/GSA/GitHub-Administration#join-the-gsa-organization) - [ ] Get permissions for the repos - [ ] Get access to the cloud.gov org && space - [ ] Get [access to AWS](https://handbook.tts.gsa.gov/launching-software/infrastructure/#cloud-service-provider-csp-sandbox-accounts), if necessary +- [ ] Get [access to New Relic](https://handbook.tts.gsa.gov/tools/new-relic/#how-do-i-get-access-to-new-relic), if necessary - [ ] Pull down creds from cloud.gov and create the local .env file - [ ] Do stuff! @@ -54,4 +59,4 @@ TODO: create env vars for these origin and destination email addresses for the r 6. Go to SNS console for \$AWS_PINPOINT_REGION, look at lefthand sidebar under "Mobile" and go to "Text Messaging (SMS)" 7. Scroll down to "Sandbox destination phone numbers" and tap "Add phone number" then follow the steps to verify (you'll need to be able to retrieve a code sent to each number) -At this point, you _should_ be able to complete both the email and phone verification steps of the Notify user sign up process! πŸŽ‰ \ No newline at end of file +At this point, you _should_ be able to complete both the email and phone verification steps of the Notify user sign up process! πŸŽ‰ diff --git a/docs/openapi.yml b/docs/openapi.yml index abbf8ff67..5cb4717e5 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -570,7 +570,7 @@ paths: reference: type: string example: - phone_number: "5558675309" + phone_number: "2028675309" template_id: "85b58733-7ebf-494e-bee2-a21a4ce17d58" personalisation: variable: "value" diff --git a/docs/system-description.md b/docs/system-description.md new file mode 100644 index 000000000..b4e8f910c --- /dev/null +++ b/docs/system-description.md @@ -0,0 +1,38 @@ +US Notify +========= + +System Description +------------------ + +US Notify is a service being developed by the TTS Public Benefits Studio to increase the availability of +SMS and email notifications to Federal, State, and Local Benefits agencies. + +Agencies that sign up will be able to create and use personalized message templates for sending +notifications to members of the public regarding their benefits. These could include reminders +about upcoming enrollment deadlines and tasks, or information about upcoming appointments, events, +or services. + +The templates are sent by the agency using one of two methods: + +* using the US Notify API to send a message to a given recipient with given personalization values +* using the US Notify website to upload a CSV file of recipients and their personalization values, one row per message + +### Environment + +US Notify is comprised of two applications both running on cloud.gov: + +* Admin, a Flask website running on the python_buildpack which hosts agency user-facing UI +* API, a Flask application running on the python_buildpack hosting the US Notify API + +US Notify utilizes several cloud.gov-provided services: + +* S3 buckets for temporary file storage +* Elasticache (redis) for cacheing data and enqueueing background tasks +* RDS (PostgreSQL) for system data storage + +US Notify also provisions and uses two AWS services via a [supplemental service broker](https://github.com/GSA/datagov-ssb): + +* [SNS](https://aws.amazon.com/sns/) for sending SMS messages +* [SES](https://aws.amazon.com/ses/) for sending email messages + +For further details of the system and how it connects to supporting services, see the [application boundary diagram](https://github.com/GSA/us-notify-compliance/blob/main/diagrams/rendered/apps/application.boundary.png) diff --git a/manifest.yml b/manifest.yml index 3a75d8c29..ae64daf6c 100644 --- a/manifest.yml +++ b/manifest.yml @@ -22,7 +22,7 @@ applications: - type: worker instances: ((worker_instances)) memory: ((worker_memory)) - command: celery -A run_celery.notify_celery worker --loglevel=INFO --concurrency=4 + command: newrelic-admin run-program celery -A run_celery.notify_celery worker --loglevel=INFO --concurrency=4 - type: scheduler instances: 1 memory: ((scheduler_memory)) @@ -32,6 +32,9 @@ applications: NOTIFY_APP_NAME: api FLASK_APP: application.py FLASK_ENV: production + NEW_RELIC_CONFIG_FILE: newrelic.ini + NEW_RELIC_ENVIRONMENT: ((env)) + NEW_RELIC_LICENSE_KEY: ((NEW_RELIC_LICENSE_KEY)) NOTIFY_ENVIRONMENT: ((env)) API_HOST_NAME: https://((public_api_route)) @@ -48,3 +51,4 @@ applications: AWS_US_TOLL_FREE_NUMBER: +18446120782 REQUESTS_CA_BUNDLE: "/etc/ssl/certs/ca-certificates.crt" + NEW_RELIC_CA_BUNDLE_PATH: "/etc/ssl/certs/ca-certificates.crt" diff --git a/migrations/alembic.ini b/migrations/alembic.ini index 8c9d61375..199797e0a 100644 --- a/migrations/alembic.ini +++ b/migrations/alembic.ini @@ -20,12 +20,12 @@ keys = console keys = generic [logger_root] -level = WARN +level = INFO handlers = qualname = [logger_sqlalchemy] -level = WARN +level = INFO handlers = qualname = sqlalchemy.engine diff --git a/migrations/versions/0381_encrypted_column_types.py b/migrations/versions/0381_encrypted_column_types.py new file mode 100644 index 000000000..64761fd8e --- /dev/null +++ b/migrations/versions/0381_encrypted_column_types.py @@ -0,0 +1,21 @@ +""" + +Revision ID: 0381_encrypted_column_types +Revises: 0380_bst_to_local +Create Date: 2022-12-09 10:17:03.358405 + +""" +from alembic import op +import sqlalchemy as sa + + +revision = '0381_encrypted_column_types' +down_revision = '0380_bst_to_local' + + +def upgrade(): + op.alter_column("api_keys", "secret", type_=sa.types.String()) + + +def downgrade(): + op.alter_column("api_keys", "secret", type_=sa.types.String(length=255)) diff --git a/migrations/versions/0382_remove_old_sms_providers_.py b/migrations/versions/0382_remove_old_sms_providers_.py new file mode 100644 index 000000000..f33f1b0d8 --- /dev/null +++ b/migrations/versions/0382_remove_old_sms_providers_.py @@ -0,0 +1,22 @@ +""" + +Revision ID: 0382_remove_old_sms_providers +Revises: 0381_encrypted_column_types +Create Date: 2022-12-16 12:52:14.182717 + +""" +from alembic import op +import sqlalchemy as sa + + +revision = '0382_remove_old_providers' +down_revision = '0381_encrypted_column_types' + + +def upgrade(): + op.execute("DELETE FROM provider_details WHERE identifier IN ('mmg', 'firetext')") + op.execute("DELETE FROM provider_details_history WHERE identifier IN ('mmg', 'firetext')") + + +def downgrade(): + raise Exception("Irreversible migration") diff --git a/migrations/versions/0383_update_default_templates.py b/migrations/versions/0383_update_default_templates.py new file mode 100644 index 000000000..3313abfa4 --- /dev/null +++ b/migrations/versions/0383_update_default_templates.py @@ -0,0 +1,55 @@ +""" + +Revision ID: 0383_update_default_templates.py +Revises: 0382_remove_old_providers +Create Date: 2023-01-10 11:42:25.633265 + +""" +import json +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +from flask import current_app + +revision = '0383_update_default_templates.py' +down_revision = '0382_remove_old_providers' + + +def upgrade(): + update = """ + UPDATE {} SET name = '{}', template_type = '{}', content = '{}', subject = '{}' + WHERE id = '{}' + """ + + with open(current_app.config['CONFIG_FILES'] + '/templates.json') as f: + data = json.load(f) + for d in data: + for table_name in 'templates', 'templates_history': + op.execute( + update.format( + table_name, + d['name'], + d['type'], + '\n'.join(d['content']), + d.get('subject'), + d['id'] + ) + ) + + # op.execute( + # """ + # INSERT INTO template_redacted + # ( + # template_id, + # redact_personalisation, + # updated_at, + # updated_by_id + # ) VALUES ( '{}', false, current_timestamp, '{}' ) + # """.format(d['id'], current_app.config['NOTIFY_USER_ID']) + # ) + + +def downgrade(): + # with associated code changes, edits to templates should no longer be made via migration. + # instead, update the fixture and run the flask command to update. + pass diff --git a/newrelic.ini b/newrelic.ini new file mode 100644 index 000000000..394ae3c1a --- /dev/null +++ b/newrelic.ini @@ -0,0 +1,222 @@ +# --------------------------------------------------------------------------- + +# +# This file configures the New Relic Python Agent. +# +# The path to the configuration file should be supplied to the function +# newrelic.agent.initialize() when the agent is being initialized. +# +# The configuration file follows a structure similar to what you would +# find for Microsoft Windows INI files. For further information on the +# configuration file format see the Python ConfigParser documentation at: +# +# http://docs.python.org/library/configparser.html +# +# For further discussion on the behaviour of the Python agent that can +# be configured via this configuration file see: +# +# http://newrelic.com/docs/python/python-agent-configuration +# + +# --------------------------------------------------------------------------- + +# Here are the settings that are common to all environments. + +[newrelic] + +# You must specify the license key associated with your New +# Relic account. This key binds the Python Agent's data to your +# account in the New Relic service. +# license_key is set via NEW_RELIC_LICENSE_KEY env var + +host = gov-collector.newrelic.com + +# The application name. Set this to be the name of your +# application as you would like it to show up in New Relic UI. +# The UI will then auto-map instances of your application into a +# entry on your home dashboard page. +app_name = us-notify-api (Development) + +# New Relic offers distributed tracing for monitoring and analyzing modern +# distributed systems.Enable distributed tracing. +distributed_tracing.enabled = true + +# When "true", the agent collects performance data about your +# application and reports this data to the New Relic UI at +# newrelic.com. This global switch is normally overridden for +# each environment below. +monitor_mode = true + +# Sets the name of a file to log agent messages to. Useful for +# debugging any issues with the agent. This is not set by +# default as it is not known in advance what user your web +# application processes will run as and where they have +# permission to write to. Whatever you set this to you must +# ensure that the permissions for the containing directory and +# the file itself are correct, and that the user that your web +# application runs as can write to the file. If not able to +# write out a log file, it is also possible to say "stderr" and +# output to standard error output. This would normally result in +# output appearing in your web server log. +#log_file = /tmp/newrelic-python-agent.log + +# Sets the level of detail of messages sent to the log file, if +# a log file location has been provided. Possible values, in +# increasing order of detail, are: "critical", "error", "warning", +# "info" and "debug". When reporting any agent issues to New +# Relic technical support, the most useful setting for the +# support engineers is "debug". However, this can generate a lot +# of information very quickly, so it is best not to keep the +# agent at this level for longer than it takes to reproduce the +# problem you are experiencing. +log_level = info + +# The Python Agent communicates with the New Relic service using +# SSL by default. Note that this does result in an increase in +# CPU overhead, over and above what would occur for a non SSL +# connection, to perform the encryption involved in the SSL +# communication. This work is though done in a distinct thread +# to those handling your web requests, so it should not impact +# response times. You can if you wish revert to using a non SSL +# connection, but this will result in information being sent +# over a plain socket connection and will not be as secure. +ssl = true + +# High Security Mode enforces certain security settings, and +# prevents them from being overridden, so that no sensitive data +# is sent to New Relic. Enabling High Security Mode means that +# SSL is turned on, request parameters are not collected, and SQL +# can not be sent to New Relic in its raw form. To activate High +# Security Mode, it must be set to 'true' in this local .ini +# configuration file AND be set to 'true' in the server-side +# configuration in the New Relic user interface. For details, see +# https://docs.newrelic.com/docs/subscriptions/high-security +high_security = false + +# The Python Agent will attempt to connect directly to the New +# Relic service. If there is an intermediate firewall between +# your host and the New Relic service that requires you to use a +# HTTP proxy, then you should set both the "proxy_host" and +# "proxy_port" settings to the required values for the HTTP +# proxy. The "proxy_user" and "proxy_pass" settings should +# additionally be set if proxy authentication is implemented by +# the HTTP proxy. The "proxy_scheme" setting dictates what +# protocol scheme is used in talking to the HTTP proxy. This +# would normally always be set as "http" which will result in the +# agent then using a SSL tunnel through the HTTP proxy for end to +# end encryption. +# proxy_scheme = http +# proxy_host = hostname +# proxy_port = 8080 +# proxy_user = +# proxy_pass = + +# Capturing request parameters is off by default. To enable the +# capturing of request parameters, first ensure that the setting +# "attributes.enabled" is set to "true" (the default value), and +# then add "request.parameters.*" to the "attributes.include" +# setting. For details about attributes configuration, please +# consult the documentation. +# attributes.include = request.parameters.* + +# The transaction tracer captures deep information about slow +# transactions and sends this to the UI on a periodic basis. The +# transaction tracer is enabled by default. Set this to "false" +# to turn it off. +transaction_tracer.enabled = true + +# Threshold in seconds for when to collect a transaction trace. +# When the response time of a controller action exceeds this +# threshold, a transaction trace will be recorded and sent to +# the UI. Valid values are any positive float value, or (default) +# "apdex_f", which will use the threshold for a dissatisfying +# Apdex controller action - four times the Apdex T value. +transaction_tracer.transaction_threshold = apdex_f + +# When the transaction tracer is on, SQL statements can +# optionally be recorded. The recorder has three modes, "off" +# which sends no SQL, "raw" which sends the SQL statement in its +# original form, and "obfuscated", which strips out numeric and +# string literals. +transaction_tracer.record_sql = obfuscated + +# Threshold in seconds for when to collect stack trace for a SQL +# call. In other words, when SQL statements exceed this +# threshold, then capture and send to the UI the current stack +# trace. This is helpful for pinpointing where long SQL calls +# originate from in an application. +transaction_tracer.stack_trace_threshold = 0.5 + +# Determines whether the agent will capture query plans for slow +# SQL queries. Only supported in MySQL and PostgreSQL. Set this +# to "false" to turn it off. +transaction_tracer.explain_enabled = true + +# Threshold for query execution time below which query plans +# will not not be captured. Relevant only when "explain_enabled" +# is true. +transaction_tracer.explain_threshold = 0.5 + +# Space separated list of function or method names in form +# 'module:function' or 'module:class.function' for which +# additional function timing instrumentation will be added. +transaction_tracer.function_trace = + +# The error collector captures information about uncaught +# exceptions or logged exceptions and sends them to UI for +# viewing. The error collector is enabled by default. Set this +# to "false" to turn it off. +error_collector.enabled = true + +# To stop specific errors from reporting to the UI, set this to +# a space separated list of the Python exception type names to +# ignore. The exception name should be of the form 'module:class'. +error_collector.ignore_classes = + +# Browser monitoring is the Real User Monitoring feature of the UI. +# For those Python web frameworks that are supported, this +# setting enables the auto-insertion of the browser monitoring +# JavaScript fragments. +browser_monitoring.auto_instrument = false + +# A thread profiling session can be scheduled via the UI when +# this option is enabled. The thread profiler will periodically +# capture a snapshot of the call stack for each active thread in +# the application to construct a statistically representative +# call tree. +thread_profiler.enabled = true + +# --------------------------------------------------------------------------- + +# +# The application environments. These are specific settings which +# override the common environment settings. The settings related to a +# specific environment will be used when the environment argument to the +# newrelic.agent.initialize() function has been defined to be either +# "development", "test", "staging" or "production". +# + +[newrelic:development] +app_name = us-notify-api (Development) + +[newrelic:test] +app_name = us-notify-api (Test) +monitor_mode = false + +[newrelic:sandbox] +app_name = us-notify-api (Sandbox) +monitor_mode = true + +[newrelic:staging] +app_name = us-notify-api (Staging) +monitor_mode = true + +[newrelic:demo] +app_name = us-notify-api (Demo) +monitor_mode = true + +[newrelic:production] +app_name = us-notify-api +monitor_mode = true + +# --------------------------------------------------------------------------- diff --git a/pytest.ini b/pytest.ini index 06dd1145f..6578fbca8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,8 +2,6 @@ testpaths = tests env = NOTIFY_ENVIRONMENT=test - MMG_API_KEY=mmg-secret-key - FIRETEXT_API_KEY=Firetext REDIS_ENABLED=0 addopts = -p no:warnings xfail_strict = true diff --git a/sample.env b/sample.env index a38f29e2a..70d1fa779 100644 --- a/sample.env +++ b/sample.env @@ -56,3 +56,9 @@ NOTIFY_APP_NAME=api FLASK_APP=application.py FLASK_ENV=development WERKZEUG_DEBUG_PIN=off + +############################################################# + +# New Relic +NEW_RELIC_CONFIG_FILE=newrelic.ini +NEW_RELIC_LICENSE_KEY="don't write secrets to the sample file" diff --git a/scripts/migrate_and_run_web.sh b/scripts/migrate_and_run_web.sh index 89a4dd5ea..3e39dceb6 100755 --- a/scripts/migrate_and_run_web.sh +++ b/scripts/migrate_and_run_web.sh @@ -4,4 +4,4 @@ if [[ $CF_INSTANCE_INDEX -eq 0 ]]; then flask db upgrade fi -exec gunicorn -c ${HOME}/gunicorn_config.py application +exec newrelic-admin run-program gunicorn -c ${HOME}/gunicorn_config.py application diff --git a/terraform/create_service_account.sh b/terraform/create_service_account.sh index d69a90796..cee07591e 100755 --- a/terraform/create_service_account.sh +++ b/terraform/create_service_account.sh @@ -18,9 +18,16 @@ Options: -o : configure the organization to act on. Default: $org Notes: -OrgManager is required for terraform to create -egress spaces +* OrgManager is required for terraform to create -egress spaces +* Requires cf-cli@8 " +cf_version=`cf --version | cut -d " " -f 3` +if [[ $cf_version != 8.* ]]; then + echo "$usage" + exit 1 +fi + set -e set -o pipefail @@ -67,7 +74,7 @@ cf create-service cloud-gov-service-account $role $service 1>&2 cf create-service-key $service service-account-key 1>&2 # output service key to stdout in secrets.auto.tfvars format -creds=`cf service-key $service service-account-key | tail -n 4` +creds=`cf service-key $service service-account-key | tail -n +2 | jq '.credentials'` username=`echo $creds | jq -r '.username'` password=`echo $creds | jq -r '.password'` diff --git a/terraform/demo/main.tf b/terraform/demo/main.tf index cb703cf50..86ce9ba53 100644 --- a/terraform/demo/main.tf +++ b/terraform/demo/main.tf @@ -7,58 +7,46 @@ locals { } module "database" { - source = "github.com/18f/terraform-cloudgov//database" + source = "github.com/18f/terraform-cloudgov//database?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name - env = local.env - app_name = local.app_name + name = "${local.app_name}-rds-${local.env}" recursive_delete = local.recursive_delete rds_plan_name = "micro-psql" } module "redis" { - source = "github.com/18f/terraform-cloudgov//redis" + source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name - env = local.env - app_name = local.app_name + name = "${local.app_name}-redis-${local.env}" recursive_delete = local.recursive_delete redis_plan_name = "redis-dev" } module "csv_upload_bucket" { - source = "github.com/18f/terraform-cloudgov//s3" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name recursive_delete = local.recursive_delete - s3_service_name = "${local.app_name}-csv-upload-bucket-${local.env}" + name = "${local.app_name}-csv-upload-bucket-${local.env}" } module "contact_list_bucket" { - source = "github.com/18f/terraform-cloudgov//s3" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name recursive_delete = local.recursive_delete - s3_service_name = "${local.app_name}-contact-list-bucket-${local.env}" + name = "${local.app_name}-contact-list-bucket-${local.env}" } module "egress-space" { source = "../shared/egress_space" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_restricted_space_name = local.cf_space_name deployers = [ diff --git a/terraform/demo/providers.tf b/terraform/demo/providers.tf index 11d4c4457..180959761 100644 --- a/terraform/demo/providers.tf +++ b/terraform/demo/providers.tf @@ -15,3 +15,10 @@ terraform { profile = "notify-terraform-backend" } } + +provider "cloudfoundry" { + api_url = "https://api.fr.cloud.gov" + user = var.cf_user + password = var.cf_password + app_logs_max = 30 +} diff --git a/terraform/production/main.tf b/terraform/production/main.tf index e32610a37..2bcae487b 100644 --- a/terraform/production/main.tf +++ b/terraform/production/main.tf @@ -7,58 +7,46 @@ locals { } module "database" { - source = "github.com/18f/terraform-cloudgov//database" + source = "github.com/18f/terraform-cloudgov//database?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name - env = local.env - app_name = local.app_name + name = "${local.app_name}-rds-${local.env}" recursive_delete = local.recursive_delete rds_plan_name = "TKTK-production-rds-plan" } module "redis" { - source = "github.com/18f/terraform-cloudgov//redis" + source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name - env = local.env - app_name = local.app_name + name = "${local.app_name}-redis-${local.env}" recursive_delete = local.recursive_delete redis_plan_name = "TKTK-production-redis-plan" } module "csv_upload_bucket" { - source = "github.com/18f/terraform-cloudgov//s3" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name recursive_delete = local.recursive_delete - s3_service_name = "${local.app_name}-csv-upload-bucket-${local.env}" + name = "${local.app_name}-csv-upload-bucket-${local.env}" } module "contact_list_bucket" { - source = "github.com/18f/terraform-cloudgov//s3" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name recursive_delete = local.recursive_delete - s3_service_name = "${local.app_name}-contact-list-bucket-${local.env}" + name = "${local.app_name}-contact-list-bucket-${local.env}" } module "egress-space" { source = "../shared/egress_space" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_restricted_space_name = local.cf_space_name deployers = [ @@ -74,14 +62,12 @@ module "egress-space" { # `cf create-domain TKTK-org-name TKTK-production-domain-name` ########################################################################### # module "domain" { -# source = "github.com/18f/terraform-cloudgov//domain" +# source = "github.com/18f/terraform-cloudgov//domain?ref=v0.2.0" # -# cf_user = var.cf_user -# cf_password = var.cf_password # cf_org_name = local.cf_org_name # cf_space_name = local.cf_space_name -# env = local.env -# app_name = local.app_name +# app_name_or_id = "${local.app_name}-${local.env}" +# name = "${local.app_name}-domain-${local.env}" # recursive_delete = local.recursive_delete # cdn_plan_name = "domain" # domain_name = "TKTK-production-domain-name" diff --git a/terraform/production/providers.tf b/terraform/production/providers.tf index f4cfe869a..360f648f4 100644 --- a/terraform/production/providers.tf +++ b/terraform/production/providers.tf @@ -15,3 +15,10 @@ terraform { profile = "notify-terraform-backend" } } + +provider "cloudfoundry" { + api_url = "https://api.fr.cloud.gov" + user = var.cf_user + password = var.cf_password + app_logs_max = 30 +} diff --git a/terraform/sandbox/main.tf b/terraform/sandbox/main.tf index 96e0338ab..6e8bb11f2 100644 --- a/terraform/sandbox/main.tf +++ b/terraform/sandbox/main.tf @@ -7,58 +7,46 @@ locals { } module "database" { - source = "github.com/18f/terraform-cloudgov//database" + source = "github.com/18f/terraform-cloudgov//database?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name - env = local.env - app_name = local.app_name + name = "${local.app_name}-rds-${local.env}" recursive_delete = local.recursive_delete rds_plan_name = "micro-psql" } module "redis" { - source = "github.com/18f/terraform-cloudgov//redis" + source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name - env = local.env - app_name = local.app_name + name = "${local.app_name}-redis-${local.env}" recursive_delete = local.recursive_delete redis_plan_name = "redis-dev" } module "csv_upload_bucket" { - source = "github.com/18f/terraform-cloudgov//s3" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name recursive_delete = local.recursive_delete - s3_service_name = "${local.app_name}-csv-upload-bucket-${local.env}" + name = "${local.app_name}-csv-upload-bucket-${local.env}" } module "contact_list_bucket" { - source = "github.com/18f/terraform-cloudgov//s3" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name recursive_delete = local.recursive_delete - s3_service_name = "${local.app_name}-contact-list-bucket-${local.env}" + name = "${local.app_name}-contact-list-bucket-${local.env}" } module "egress-space" { source = "../shared/egress_space" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_restricted_space_name = local.cf_space_name deployers = [ diff --git a/terraform/sandbox/providers.tf b/terraform/sandbox/providers.tf index 0cab42f6f..00ea2888d 100644 --- a/terraform/sandbox/providers.tf +++ b/terraform/sandbox/providers.tf @@ -15,3 +15,10 @@ terraform { profile = "notify-terraform-backend" } } + +provider "cloudfoundry" { + api_url = "https://api.fr.cloud.gov" + user = var.cf_user + password = var.cf_password + app_logs_max = 30 +} diff --git a/terraform/shared/egress_space/main.tf b/terraform/shared/egress_space/main.tf index 4a65b74e9..4b841ad14 100644 --- a/terraform/shared/egress_space/main.tf +++ b/terraform/shared/egress_space/main.tf @@ -12,7 +12,7 @@ data "cloudfoundry_org" "org" { resource "cloudfoundry_space" "public_egress" { name = "${var.cf_restricted_space_name}-egress" - org = data.cloudfoundry_org.org.id + org = data.cloudfoundry_org.org.id } ### @@ -21,8 +21,8 @@ resource "cloudfoundry_space" "public_egress" { data "cloudfoundry_user" "users" { for_each = var.deployers - name = each.key - org_id = data.cloudfoundry_org.org.id + name = each.key + org_id = data.cloudfoundry_org.org.id } locals { @@ -30,7 +30,7 @@ locals { } resource "cloudfoundry_space_users" "deployers" { - space = cloudfoundry_space.public_egress.id - managers = local.user_ids + space = cloudfoundry_space.public_egress.id + managers = local.user_ids developers = local.user_ids } diff --git a/terraform/shared/egress_space/providers.tf b/terraform/shared/egress_space/providers.tf index ad8addecd..8db86ca90 100644 --- a/terraform/shared/egress_space/providers.tf +++ b/terraform/shared/egress_space/providers.tf @@ -2,15 +2,8 @@ terraform { required_version = "~> 1.0" required_providers { cloudfoundry = { - source = "cloudfoundry-community/cloudfoundry" + source = "cloudfoundry-community/cloudfoundry" version = "~> 0.15" } } } - -provider "cloudfoundry" { - api_url = "https://api.fr.cloud.gov" - user = var.cf_user - password = var.cf_password - app_logs_max = 30 -} diff --git a/terraform/shared/egress_space/variables.tf b/terraform/shared/egress_space/variables.tf index 1ab080dea..45bcc717d 100644 --- a/terraform/shared/egress_space/variables.tf +++ b/terraform/shared/egress_space/variables.tf @@ -1,8 +1,3 @@ -variable "cf_password" { - type = string - sensitive = true -} -variable "cf_user" {} variable "cf_org_name" {} variable "cf_restricted_space_name" {} variable "deployers" { diff --git a/terraform/staging/main.tf b/terraform/staging/main.tf index 911e9d8b9..a2ea6d0d8 100644 --- a/terraform/staging/main.tf +++ b/terraform/staging/main.tf @@ -7,58 +7,46 @@ locals { } module "database" { - source = "github.com/18f/terraform-cloudgov//database" + source = "github.com/18f/terraform-cloudgov//database?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name - env = local.env - app_name = local.app_name + name = "${local.app_name}-rds-${local.env}" recursive_delete = local.recursive_delete rds_plan_name = "micro-psql" } module "redis" { - source = "github.com/18f/terraform-cloudgov//redis" + source = "github.com/18f/terraform-cloudgov//redis?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name - env = local.env - app_name = local.app_name + name = "${local.app_name}-redis-${local.env}" recursive_delete = local.recursive_delete redis_plan_name = "redis-dev" } module "csv_upload_bucket" { - source = "github.com/18f/terraform-cloudgov//s3" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name recursive_delete = local.recursive_delete - s3_service_name = "${local.app_name}-csv-upload-bucket-${local.env}" + name = "${local.app_name}-csv-upload-bucket-${local.env}" } module "contact_list_bucket" { - source = "github.com/18f/terraform-cloudgov//s3" + source = "github.com/18f/terraform-cloudgov//s3?ref=v0.2.0" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_space_name = local.cf_space_name recursive_delete = local.recursive_delete - s3_service_name = "${local.app_name}-contact-list-bucket-${local.env}" + name = "${local.app_name}-contact-list-bucket-${local.env}" } module "egress-space" { source = "../shared/egress_space" - cf_user = var.cf_user - cf_password = var.cf_password cf_org_name = local.cf_org_name cf_restricted_space_name = local.cf_space_name deployers = [ diff --git a/terraform/staging/providers.tf b/terraform/staging/providers.tf index 72a7c30f6..181b5fd69 100644 --- a/terraform/staging/providers.tf +++ b/terraform/staging/providers.tf @@ -15,3 +15,10 @@ terraform { profile = "notify-terraform-backend" } } + +provider "cloudfoundry" { + api_url = "https://api.fr.cloud.gov" + user = var.cf_user + password = var.cf_password + app_logs_max = 30 +} diff --git a/tests/app/celery/test_process_ses_receipts_tasks.py b/tests/app/celery/test_process_ses_receipts_tasks.py index a90f9f6f8..1ec8afcea 100644 --- a/tests/app/celery/test_process_ses_receipts_tasks.py +++ b/tests/app/celery/test_process_ses_receipts_tasks.py @@ -1,5 +1,6 @@ import json from datetime import datetime +from unittest.mock import ANY from freezegun import freeze_time @@ -14,11 +15,8 @@ from app.celery.research_mode_tasks import ( ses_notification_callback, ses_soft_bounce_callback, ) -from app.celery.service_callback_tasks import ( - create_delivery_status_callback_data, -) from app.dao.notifications_dao import get_notification_by_id -from app.models import Complaint, Notification +from app.models import Complaint from tests.app.conftest import create_sample_notification from tests.app.db import ( create_notification, @@ -156,7 +154,7 @@ def test_ses_callback_should_update_notification_status( status='sending', sent_at=datetime.utcnow() ) - callback_api = create_service_callback_api( + create_service_callback_api( service=sample_email_template.service, url="https://original_url.com" ) @@ -167,9 +165,9 @@ def test_ses_callback_should_update_notification_status( "callback.ses.elapsed-time", datetime.utcnow(), notification.sent_at ) statsd_client.incr.assert_any_call("callback.ses.delivered") - updated_notification = Notification.query.get(notification.id) - encrypted_data = create_delivery_status_callback_data(updated_notification, callback_api) - send_mock.assert_called_once_with([str(notification.id), encrypted_data], queue="service-callbacks") + send_mock.assert_called_once_with([str(notification.id), ANY], queue="service-callbacks") + # assert second arg is an encrypted string + assert isinstance(send_mock.call_args.args[0][1], str) def test_ses_callback_should_not_update_notification_status_if_already_delivered(sample_email_template, mocker): diff --git a/tests/app/celery/test_process_sms_client_response_tasks.py b/tests/app/celery/test_process_sms_client_response_tasks.py deleted file mode 100644 index 34bd6807c..000000000 --- a/tests/app/celery/test_process_sms_client_response_tasks.py +++ /dev/null @@ -1,179 +0,0 @@ -import uuid -from datetime import datetime - -import pytest -from freezegun import freeze_time - -from app import statsd_client -from app.celery.process_sms_client_response_tasks import ( - process_sms_client_response, -) -from app.clients import ClientException -from app.models import NOTIFICATION_TECHNICAL_FAILURE - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_process_sms_client_response_raises_error_if_reference_is_not_a_valid_uuid(client): - with pytest.raises(ValueError): - process_sms_client_response( - status='000', provider_reference='something-bad', client_name='sms-client') - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@pytest.mark.parametrize('client_name', ('Firetext', 'MMG')) -def test_process_sms_response_raises_client_exception_for_unknown_status( - sample_notification, - mocker, - client_name, -): - with pytest.raises(ClientException) as e: - process_sms_client_response( - status='000', - provider_reference=str(sample_notification.id), - client_name=client_name, - ) - - assert f"{client_name} callback failed: status {'000'} not found." in str(e.value) - assert sample_notification.status == NOTIFICATION_TECHNICAL_FAILURE - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@pytest.mark.parametrize('status, detailed_status_code, sms_provider, expected_notification_status, reason', [ - ('0', None, 'Firetext', 'delivered', None), - ('1', '101', 'Firetext', 'permanent-failure', 'Unknown Subscriber'), - ('2', '102', 'Firetext', 'pending', 'Absent Subscriber'), - ('2', '1', 'MMG', 'permanent-failure', "Number does not exist"), - ('3', '2', 'MMG', 'delivered', "Delivered to operator"), - ('4', '27', 'MMG', 'temporary-failure', "Absent Subscriber"), - ('5', '13', 'MMG', 'permanent-failure', "Sender id blacklisted"), -]) -def test_process_sms_client_response_updates_notification_status( - sample_notification, - mocker, - status, - detailed_status_code, - sms_provider, - expected_notification_status, - reason -): - mock_logger = mocker.patch('app.celery.tasks.current_app.logger.info') - sample_notification.status = 'sending' - - process_sms_client_response(status, str(sample_notification.id), sms_provider, detailed_status_code) - - message = f"{sms_provider} callback returned status of {expected_notification_status}({status}): {reason}({detailed_status_code}) for reference: {sample_notification.id}" # noqa - mock_logger.assert_any_call(message) - assert sample_notification.status == expected_notification_status - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@pytest.mark.parametrize('detailed_status_code, expected_notification_status, reason', [ - ('101', 'permanent-failure', 'Unknown Subscriber'), - ('102', 'temporary-failure', 'Absent Subscriber'), - (None, 'temporary-failure', None), - ('000', 'temporary-failure', 'No error reported') -]) -def test_process_sms_client_response_updates_notification_status_when_called_second_time( - sample_notification, - mocker, - detailed_status_code, - expected_notification_status, - reason -): - mock_logger = mocker.patch('app.celery.tasks.current_app.logger.info') - sample_notification.status = 'sending' - process_sms_client_response('2', str(sample_notification.id), 'Firetext') - - process_sms_client_response('1', str(sample_notification.id), 'Firetext', detailed_status_code) - - if detailed_status_code: - message = f'Updating notification id {sample_notification.id} to status {expected_notification_status}, reason: {reason}' # noqa - mock_logger.assert_called_with(message) - - assert sample_notification.status == expected_notification_status - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@pytest.mark.parametrize('detailed_status_code', ['102', None, '000']) -def test_process_sms_client_response_updates_notification_status_to_pending_with_and_without_failure_code_present( - sample_notification, - mocker, - detailed_status_code -): - sample_notification.status = 'sending' - - process_sms_client_response('2', str(sample_notification.id), 'Firetext', detailed_status_code) - - assert sample_notification.status == 'pending' - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_process_sms_client_response_updates_notification_status_when_detailed_status_code_not_recognised( - sample_notification, - mocker, -): - mock_logger = mocker.patch('app.celery.tasks.current_app.logger.warning') - sample_notification.status = 'sending' - process_sms_client_response('2', str(sample_notification.id), 'Firetext') - - process_sms_client_response('1', str(sample_notification.id), 'Firetext', '789') - - mock_logger.assert_called_once_with('Failure code 789 from Firetext not recognised') - assert sample_notification.status == 'temporary-failure' - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_sms_response_does_not_send_callback_if_notification_is_not_in_the_db(sample_service, mocker): - send_mock = mocker.patch('app.celery.process_sms_client_response_tasks.check_and_queue_callback_task') - reference = str(uuid.uuid4()) - process_sms_client_response(status='3', provider_reference=reference, client_name='MMG') - send_mock.assert_not_called() - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@freeze_time('2001-01-01T12:00:00') -def test_process_sms_client_response_records_statsd_metrics(sample_notification, client, mocker): - mocker.patch('app.statsd_client.incr') - mocker.patch('app.statsd_client.timing_with_dates') - - sample_notification.status = 'sending' - sample_notification.sent_at = datetime.utcnow() - - process_sms_client_response('0', str(sample_notification.id), 'Firetext') - - statsd_client.incr.assert_any_call("callback.firetext.delivered") - statsd_client.timing_with_dates.assert_any_call( - "callback.firetext.delivered.elapsed-time", datetime.utcnow(), sample_notification.sent_at - ) - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_process_sms_updates_billable_units_if_zero(sample_notification): - sample_notification.billable_units = 0 - process_sms_client_response('3', str(sample_notification.id), 'MMG') - - assert sample_notification.billable_units == 1 - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_process_sms_response_does_not_send_service_callback_for_pending_notifications(sample_notification, mocker): - send_mock = mocker.patch('app.celery.process_sms_client_response_tasks.check_and_queue_callback_task') - process_sms_client_response('2', str(sample_notification.id), 'Firetext') - send_mock.assert_not_called() - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_outcome_statistics_called_for_successful_callback(sample_notification, mocker): - send_mock = mocker.patch('app.celery.process_sms_client_response_tasks.check_and_queue_callback_task') - reference = str(sample_notification.id) - - process_sms_client_response('3', reference, 'MMG') - send_mock.assert_called_once_with(sample_notification) - - -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -def test_process_sms_updates_sent_by_with_client_name_if_not_in_noti(sample_notification): - sample_notification.sent_by = None - process_sms_client_response('3', str(sample_notification.id), 'MMG') - - assert sample_notification.sent_by == 'mmg' diff --git a/tests/app/celery/test_reporting_tasks.py b/tests/app/celery/test_reporting_tasks.py index 1bae08a7b..5843132bb 100644 --- a/tests/app/celery/test_reporting_tasks.py +++ b/tests/app/celery/test_reporting_tasks.py @@ -168,7 +168,7 @@ def test_create_nightly_billing_for_day_sms_rate_multiplier( created_at=yesterday, template=sample_template, status='delivered', - sent_by='mmg', + sent_by='sns', international=False, rate_multiplier=1.0, billable_units=1, @@ -177,7 +177,7 @@ def test_create_nightly_billing_for_day_sms_rate_multiplier( created_at=yesterday, template=sample_template, status='delivered', - sent_by='mmg', + sent_by='sns', international=False, rate_multiplier=second_rate, billable_units=1, @@ -212,7 +212,7 @@ def test_create_nightly_billing_for_day_different_templates( created_at=yesterday, template=sample_template, status='delivered', - sent_by='mmg', + sent_by='sns', international=False, rate_multiplier=1.0, billable_units=1, @@ -221,7 +221,7 @@ def test_create_nightly_billing_for_day_different_templates( created_at=yesterday, template=sample_email_template, status='delivered', - sent_by='ses', + sent_by='sns', international=False, rate_multiplier=0, billable_units=0, @@ -260,7 +260,7 @@ def test_create_nightly_billing_for_day_different_sent_by( created_at=yesterday, template=sample_template, status='delivered', - sent_by='mmg', + sent_by='sns', international=False, rate_multiplier=1.0, billable_units=1, @@ -269,7 +269,7 @@ def test_create_nightly_billing_for_day_different_sent_by( created_at=yesterday, template=sample_template, status='delivered', - sent_by='firetext', + sent_by='sns', international=False, rate_multiplier=1.0, billable_units=1, diff --git a/tests/app/celery/test_research_mode_tasks.py b/tests/app/celery/test_research_mode_tasks.py index 6fa9a8d0a..57ad2507d 100644 --- a/tests/app/celery/test_research_mode_tasks.py +++ b/tests/app/celery/test_research_mode_tasks.py @@ -6,11 +6,11 @@ from flask import json from app.celery.research_mode_tasks import ( HTTPError, - firetext_callback, - mmg_callback, + create_fake_letter_response_file, send_email_response, send_sms_response, ses_notification_callback, + sns_callback, ) from app.config import QueueNames from tests.conftest import Matcher @@ -21,22 +21,24 @@ dvla_response_file_matcher = Matcher( ) -def test_make_mmg_callback(notify_api, rmock): - endpoint = "http://localhost:6011/notifications/sms/mmg" +@pytest.mark.skip(reason="Re-enable when SMS receipts exist") +def test_make_sns_callback(notify_api, rmock): + endpoint = "http://localhost:6011/notifications/sms/sns" rmock.request( "POST", endpoint, json={"status": "success"}, status_code=200) - send_sms_response("mmg", "1234", "07700900001") + send_sms_response("sns", "1234", "2028675309") assert rmock.called assert rmock.request_history[0].url == endpoint - assert json.loads(rmock.request_history[0].text)['MSISDN'] == '07700900001' + assert json.loads(rmock.request_history[0].text)['MSISDN'] == '2028675309' +@pytest.mark.skip(reason="Re-enable when SMS receipts exist") def test_callback_logs_on_api_call_failure(notify_api, rmock, mocker): - endpoint = "http://localhost:6011/notifications/sms/mmg" + endpoint = "http://localhost:6011/notifications/sms/sns" rmock.request( "POST", endpoint, @@ -54,23 +56,6 @@ def test_callback_logs_on_api_call_failure(notify_api, rmock, mocker): ) -@pytest.mark.parametrize("phone_number", - ["07700900001", "07700900002", "07700900003", - "07700900236"]) -def test_make_firetext_callback(notify_api, rmock, phone_number): - endpoint = "http://localhost:6011/notifications/sms/firetext" - rmock.request( - "POST", - endpoint, - json="some data", - status_code=200) - send_sms_response("firetext", "1234", phone_number) - - assert rmock.called - assert rmock.request_history[0].url == endpoint - assert 'mobile={}'.format(phone_number) in rmock.request_history[0].text - - def test_make_ses_callback(notify_api, mocker): mock_task = mocker.patch('app.celery.research_mode_tasks.process_ses_results') some_ref = str(uuid.uuid4()) @@ -81,50 +66,31 @@ def test_make_ses_callback(notify_api, mocker): assert mock_task.apply_async.call_args[0][0][0] == ses_notification_callback(some_ref) -@pytest.mark.parametrize("phone_number", ["07700900001", "+447700900001", "7700900001", "+44 7700900001", - "+447700900236"]) -def test_delivered_mmg_callback(phone_number): - data = json.loads(mmg_callback("1234", phone_number)) +@pytest.mark.skip(reason="Re-enable when SNS delivery receipts exist") +def test_delievered_sns_callback(): + phone_number = "2028675309" + data = json.loads(sns_callback("1234", phone_number)) assert data['MSISDN'] == phone_number assert data['status'] == "3" - assert data['reference'] == "mmg_reference" + assert data['reference'] == "sns_reference" assert data['CID'] == "1234" -@pytest.mark.parametrize("phone_number", ["07700900002", "+447700900002", "7700900002", "+44 7700900002"]) -def test_perm_failure_mmg_callback(phone_number): - data = json.loads(mmg_callback("1234", phone_number)) +@pytest.mark.skip(reason="Re-enable when SNS delivery receipts exist") +def test_perm_failure_sns_callback(): + phone_number = "2028675302" + data = json.loads(sns_callback("1234", phone_number)) assert data['MSISDN'] == phone_number assert data['status'] == "5" - assert data['reference'] == "mmg_reference" + assert data['reference'] == "sns_reference" assert data['CID'] == "1234" -@pytest.mark.parametrize("phone_number", ["07700900003", "+447700900003", "7700900003", "+44 7700900003"]) -def test_temp_failure_mmg_callback(phone_number): - data = json.loads(mmg_callback("1234", phone_number)) +@pytest.mark.skip(reason="Re-enable when SNS delivery receipts exist") +def test_temp_failure_sns_callback(): + phone_number = "2028675303" + data = json.loads(sns_callback("1234", phone_number)) assert data['MSISDN'] == phone_number assert data['status'] == "4" - assert data['reference'] == "mmg_reference" + assert data['reference'] == "sns_reference" assert data['CID'] == "1234" - - -@pytest.mark.parametrize("phone_number", ["07700900001", "+447700900001", "7700900001", "+44 7700900001", - "+447700900256"]) -def test_delivered_firetext_callback(phone_number): - assert firetext_callback('1234', phone_number) == { - 'mobile': phone_number, - 'status': '0', - 'time': '2016-03-10 14:17:00', - 'reference': '1234' - } - - -@pytest.mark.parametrize("phone_number", ["07700900002", "+447700900002", "7700900002", "+44 7700900002"]) -def test_failure_firetext_callback(phone_number): - assert firetext_callback('1234', phone_number) == { - 'mobile': phone_number, - 'status': '1', - 'time': '2016-03-10 14:17:00', - 'reference': '1234' - } diff --git a/tests/app/celery/test_tasks.py b/tests/app/celery/test_tasks.py index 53f55b2f4..6cc90c836 100644 --- a/tests/app/celery/test_tasks.py +++ b/tests/app/celery/test_tasks.py @@ -72,9 +72,6 @@ class AnyStringWith(str): return self in other -mmg_error = {'Error': '40', 'Description': 'error'} - - def _notification_json(template, to, personalisation=None, job_id=None, row_number=0): return { "template": str(template.id), @@ -467,7 +464,6 @@ def test_should_send_template_to_correct_sms_task_and_persist(sample_template_wi assert not persisted_notification.sent_by assert not persisted_notification.job_id assert persisted_notification.personalisation == {'name': 'Jo'} - assert persisted_notification._personalisation == encryption.encrypt({"name": "Jo"}) assert persisted_notification.notification_type == 'sms' mocked_deliver_sms.assert_called_once_with( [str(persisted_notification.id)], @@ -500,10 +496,10 @@ def test_should_put_save_sms_task_in_research_mode_queue_if_research_mode_servic def test_should_save_sms_if_restricted_service_and_valid_number(notify_db_session, mocker): - user = create_user(mobile_number="07700 900890") + user = create_user(mobile_number="202-867-5309") service = create_service(user=user, restricted=True) template = create_template(service=service) - notification = _notification_json(template, "+447700900890") # The user’s own number, but in a different format + notification = _notification_json(template, "+12028675309") # The user’s own number, but in a different format mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') @@ -516,7 +512,7 @@ def test_should_save_sms_if_restricted_service_and_valid_number(notify_db_sessio ) persisted_notification = Notification.query.one() - assert persisted_notification.to == '+447700900890' + assert persisted_notification.to == '+12028675309' assert persisted_notification.template_id == template.id assert persisted_notification.template_version == template.version assert persisted_notification.status == 'created' @@ -551,11 +547,11 @@ def test_save_email_should_save_default_email_reply_to_text_on_notification(noti assert persisted_notification.reply_to_text == 'reply_to@digital.gov.uk' -def test_save_sms_should_save_default_smm_sender_notification_reply_to_text_on(notify_db_session, mocker): +def test_save_sms_should_save_default_sms_sender_notification_reply_to_text_on(notify_db_session, mocker): service = create_service_with_defined_sms_sender(sms_sender_value='12345') template = create_template(service=service) - notification = _notification_json(template, to="07700 900205") + notification = _notification_json(template, to="2028675309") mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') notification_id = uuid.uuid4() @@ -570,11 +566,11 @@ def test_save_sms_should_save_default_smm_sender_notification_reply_to_text_on(n def test_should_not_save_sms_if_restricted_service_and_invalid_number(notify_db_session, mocker): - user = create_user(mobile_number="07700 900205") + user = create_user(mobile_number="2028675309") service = create_service(user=user, restricted=True) template = create_template(service=service) - notification = _notification_json(template, "07700 900849") + notification = _notification_json(template, "2028675400") mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') notification_id = uuid.uuid4() @@ -665,14 +661,14 @@ def test_should_save_sms_template_to_and_persist_with_job_id(sample_job, mocker) def test_should_not_save_sms_if_team_key_and_recipient_not_in_team(notify_db_session, mocker): assert Notification.query.count() == 0 - user = create_user(mobile_number="07700 900205") + user = create_user(mobile_number="2028675309") service = create_service(user=user, restricted=True) template = create_template(service=service) team_members = [user.mobile_number for user in service.users] assert "07890 300000" not in team_members - notification = _notification_json(template, "07700 900849") + notification = _notification_json(template, "2028675400") mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') notification_id = uuid.uuid4() @@ -715,7 +711,6 @@ def test_should_use_email_template_and_persist(sample_email_template_with_placeh assert not persisted_notification.sent_by assert persisted_notification.job_row_number == 1 assert persisted_notification.personalisation == {'name': 'Jo'} - assert persisted_notification._personalisation == encryption.encrypt({"name": "Jo"}) assert persisted_notification.api_key_id is None assert persisted_notification.key_type == KEY_TYPE_NORMAL assert persisted_notification.notification_type == 'email' @@ -930,10 +925,10 @@ def test_save_sms_does_not_send_duplicate_and_does_not_put_in_retry_queue(sample def test_save_sms_uses_sms_sender_reply_to_text(mocker, notify_db_session): - service = create_service_with_defined_sms_sender(sms_sender_value='07123123123') + service = create_service_with_defined_sms_sender(sms_sender_value='2028675309') template = create_template(service=service) - notification = _notification_json(template, to="07700 900205") + notification = _notification_json(template, to="2028675301") mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') notification_id = uuid.uuid4() @@ -944,15 +939,15 @@ def test_save_sms_uses_sms_sender_reply_to_text(mocker, notify_db_session): ) persisted_notification = Notification.query.one() - assert persisted_notification.reply_to_text == '447123123123' + assert persisted_notification.reply_to_text == '+12028675309' def test_save_sms_uses_non_default_sms_sender_reply_to_text_if_provided(mocker, notify_db_session): - service = create_service_with_defined_sms_sender(sms_sender_value='07123123123') + service = create_service_with_defined_sms_sender(sms_sender_value='2028675309') template = create_template(service=service) new_sender = service_sms_sender_dao.dao_add_sms_sender_for_service(service.id, 'new-sender', False) - notification = _notification_json(template, to="07700 900205") + notification = _notification_json(template, to="202-867-5301") mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') notification_id = uuid.uuid4() @@ -1472,7 +1467,7 @@ def test_save_api_email_dont_retry_if_notification_already_exists(sample_service ), ( save_sms, 'app.celery.provider_tasks.deliver_sms.apply_async', - '07700 900890', + '202-867-5309', {'template_type': 'sms'} ), )) diff --git a/tests/app/conftest.py b/tests/app/conftest.py index 7cd374b72..ba4a6419b 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -626,8 +626,8 @@ def ses_provider(): @pytest.fixture(scope='function') -def mmg_provider(): - return ProviderDetails.query.filter_by(identifier='mmg').one() +def sns_provider(): + return ProviderDetails.query.filter_by(identifier='sns').one() @pytest.fixture(scope='function') @@ -888,9 +888,8 @@ def sample_service_guest_list(notify_db_session): def sample_inbound_numbers(sample_service): service = create_service(service_name='sample service 2', check_if_service_exists=True) inbound_numbers = list() - inbound_numbers.append(create_inbound_number(number='1', provider='mmg')) - inbound_numbers.append(create_inbound_number(number='2', provider='mmg', active=False, service_id=service.id)) - inbound_numbers.append(create_inbound_number(number='3', provider='firetext', service_id=sample_service.id)) + inbound_numbers.append(create_inbound_number(number='1', provider='sns')) + inbound_numbers.append(create_inbound_number(number='2', provider='sns', active=False, service_id=service.id)) return inbound_numbers diff --git a/tests/app/dao/notification_dao/test_notification_dao.py b/tests/app/dao/notification_dao/test_notification_dao.py index b0dfd8482..8ae304ebf 100644 --- a/tests/app/dao/notification_dao/test_notification_dao.py +++ b/tests/app/dao/notification_dao/test_notification_dao.py @@ -53,7 +53,6 @@ from tests.app.db import ( ) -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") def test_should_by_able_to_update_status_by_reference(sample_email_template, ses_provider): data = _notification_json(sample_email_template, status='sending') @@ -69,8 +68,7 @@ def test_should_by_able_to_update_status_by_reference(sample_email_template, ses assert Notification.query.get(notification.id).status == 'delivered' -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") -def test_should_by_able_to_update_status_by_id(sample_template, sample_job, mmg_provider): +def test_should_by_able_to_update_status_by_id(sample_template, sample_job, sns_provider): with freeze_time('2000-01-01 12:00:00'): data = _notification_json(sample_template, job_id=sample_job.id, status='sending') notification = Notification(**data) @@ -107,7 +105,6 @@ def test_should_not_update_status_by_reference_if_not_sending_and_does_not_updat assert sample_job == Job.query.get(notification.job_id) -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") def test_should_update_status_by_id_if_created(sample_template, sample_notification): assert Notification.query.get(sample_notification.id).status == 'created' updated = update_notification_status_by_id(sample_notification.id, 'failed') @@ -115,7 +112,6 @@ def test_should_update_status_by_id_if_created(sample_template, sample_notificat assert updated.status == 'failed' -@pytest.mark.skip(reason="Needs updating for TTS: Remove letters") def test_should_update_status_by_id_if_pending_virus_check(sample_letter_template): notification = create_notification(template=sample_letter_template, status='pending-virus-check') assert Notification.query.get(notification.id).status == 'pending-virus-check' @@ -124,7 +120,6 @@ def test_should_update_status_by_id_if_pending_virus_check(sample_letter_templat assert updated.status == 'cancelled' -@pytest.mark.skip(reason="Needs updating for TTS: Remove letters") def test_should_update_status_of_international_letter_to_cancelled(sample_letter_template): notification = create_notification( template=sample_letter_template, @@ -136,13 +131,12 @@ def test_should_update_status_of_international_letter_to_cancelled(sample_letter assert Notification.query.get(notification.id).status == 'cancelled' -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") def test_should_update_status_by_id_and_set_sent_by(sample_template): notification = create_notification(template=sample_template, status='sending') - updated = update_notification_status_by_id(notification.id, 'delivered', sent_by='mmg') + updated = update_notification_status_by_id(notification.id, 'delivered', sent_by='sns') assert updated.status == 'delivered' - assert updated.sent_by == 'mmg' + assert updated.sent_by == 'sns' def test_should_not_update_status_by_reference_if_from_country_with_no_delivery_receipts(sample_template): @@ -186,7 +180,6 @@ def test_should_not_update_status_by_id_if_sent_to_country_with_carrier_delivery assert notification.status == NOTIFICATION_SENT -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") def test_should_not_update_status_by_id_if_sent_to_country_with_delivery_receipts(sample_template): notification = create_notification( sample_template, @@ -209,7 +202,6 @@ def test_should_not_update_status_by_reference_if_not_sending(sample_template): assert not updated -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") def test_should_by_able_to_update_status_by_id_from_pending_to_delivered(sample_template, sample_job): notification = create_notification(template=sample_template, job=sample_job, status='sending') @@ -220,7 +212,6 @@ def test_should_by_able_to_update_status_by_id_from_pending_to_delivered(sample_ assert Notification.query.get(notification.id).status == 'delivered' -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") def test_should_by_able_to_update_status_by_id_from_pending_to_temporary_failure(sample_template, sample_job): notification = create_notification(template=sample_template, job=sample_job, status='sending', sent_by='sns') @@ -232,7 +223,6 @@ def test_should_by_able_to_update_status_by_id_from_pending_to_temporary_failure assert Notification.query.get(notification.id).status == 'temporary-failure' -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") def test_should_by_able_to_update_status_by_id_from_sending_to_permanent_failure(sample_template, sample_job): data = _notification_json(sample_template, job_id=sample_job.id, status='sending') notification = Notification(**data) @@ -246,7 +236,6 @@ def test_should_by_able_to_update_status_by_id_from_sending_to_permanent_failure assert Notification.query.get(notification.id).status == 'permanent-failure' -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") def test_should_not_update_status_once_notification_status_is_delivered( sample_email_template): notification = create_notification(template=sample_email_template, status='sending') @@ -372,7 +361,7 @@ def test_update_notification_with_research_mode_service_does_not_create_or_updat assert NotificationHistory.query.count() == 0 -def test_not_save_notification_and_not_create_stats_on_commit_error(sample_template, sample_job, mmg_provider): +def test_not_save_notification_and_not_create_stats_on_commit_error(sample_template, sample_job, sns_provider): random_id = str(uuid.uuid4()) assert Notification.query.count() == 0 @@ -386,7 +375,7 @@ def test_not_save_notification_and_not_create_stats_on_commit_error(sample_templ assert Job.query.get(sample_job.id).notifications_sent == 0 -def test_save_notification_and_increment_job(sample_template, sample_job, mmg_provider): +def test_save_notification_and_increment_job(sample_template, sample_job, sns_provider): assert Notification.query.count() == 0 data = _notification_json(sample_template, job_id=sample_job.id) @@ -409,7 +398,7 @@ def test_save_notification_and_increment_job(sample_template, sample_job, mmg_pr assert Notification.query.count() == 2 -def test_save_notification_and_increment_correct_job(sample_template, mmg_provider): +def test_save_notification_and_increment_correct_job(sample_template, sns_provider): job_1 = create_job(sample_template) job_2 = create_job(sample_template) @@ -432,7 +421,7 @@ def test_save_notification_and_increment_correct_job(sample_template, mmg_provid assert job_1.id != job_2.id -def test_save_notification_with_no_job(sample_template, mmg_provider): +def test_save_notification_with_no_job(sample_template, sns_provider): assert Notification.query.count() == 0 data = _notification_json(sample_template) @@ -1091,16 +1080,14 @@ def test_dao_get_notifications_by_reference_escapes_special_character( @pytest.mark.parametrize('search_term', [ - '001', - '100', - '09001', - '077009001', - '07700 9001', - '(0)7700 9001', - '4477009001', - '+4477009001', - pytest.param('+44077009001', marks=pytest.mark.skip(reason='No easy way to normalise this')), - pytest.param('+44(0)77009001', marks=pytest.mark.skip(reason='No easy way to normalise this')), + '309', + '530', + '8675309', + '202867', + '202 867', + '202-867-5309', + '2028675309', + '+12028675309', ]) def test_dao_get_notifications_by_recipient_matches_partial_phone_numbers( sample_template, @@ -1109,13 +1096,13 @@ def test_dao_get_notifications_by_recipient_matches_partial_phone_numbers( notification_1 = create_notification( template=sample_template, - to_field='+447700900100', - normalised_to='447700900100', + to_field='202-867-5309', + normalised_to='+12028675309', ) notification_2 = create_notification( template=sample_template, - to_field='+447700900200', - normalised_to='447700900200', + to_field='202-678-5000', + normalised_to='+12026785000', ) results = dao_get_notifications_by_recipient_or_reference( notification_1.service_id, search_term, notification_type='sms' @@ -1167,7 +1154,7 @@ def test_dao_get_notifications_by_recipient_ignores_spaces(sample_template): @pytest.mark.parametrize('phone_search', ( - '077', '7-7', '+44(0)7711 111111' + '202', '7-5', '+1 (202) 867-5309' )) @pytest.mark.parametrize('email_search', ( 'example', 'eXaMpLe', @@ -1180,9 +1167,9 @@ def test_dao_get_notifications_by_recipient_searches_across_notification_types( service = create_service() sms_template = create_template(service=service) email_template = create_template(service=service, template_type='email') - sms = create_notification(template=sms_template, to_field='07711111111', normalised_to='447711111111') + sms = create_notification(template=sms_template, to_field='202-867-5309', normalised_to='+12028675309') email = create_notification( - template=email_template, to_field='077@example.com', normalised_to='077@example.com' + template=email_template, to_field='202@example.com', normalised_to='202@example.com' ) results = dao_get_notifications_by_recipient_or_reference( @@ -1197,7 +1184,7 @@ def test_dao_get_notifications_by_recipient_searches_across_notification_types( assert len(results.items) == 1 assert results.items[0].id == email.id - results = dao_get_notifications_by_recipient_or_reference(service.id, '77') + results = dao_get_notifications_by_recipient_or_reference(service.id, '202') assert len(results.items) == 2 assert results.items[0].id == email.id assert results.items[1].id == sms.id diff --git a/tests/app/dao/test_fact_billing_dao.py b/tests/app/dao/test_fact_billing_dao.py index 1a50649bf..f9383cd39 100644 --- a/tests/app/dao/test_fact_billing_dao.py +++ b/tests/app/dao/test_fact_billing_dao.py @@ -174,14 +174,14 @@ def test_fetch_billing_data_for_day_is_grouped_by_service(notify_db_session): def test_fetch_billing_data_for_day_is_grouped_by_provider(notify_db_session): service = create_service() template = create_template(service=service) - create_notification(template=template, status='delivered', sent_by='mmg') - create_notification(template=template, status='delivered', sent_by='firetext') + create_notification(template=template, status='delivered', sent_by='sns') + create_notification(template=template, status='delivered', sent_by='sns') today = convert_utc_to_local_timezone(datetime.utcnow()) results = fetch_billing_data_for_day(today.date()) - assert len(results) == 2 - assert results[0].notifications_sent == 1 - assert results[1].notifications_sent == 1 + assert len(results) == 1 + assert results[0].notifications_sent == 2 + # assert results[1].notifications_sent == 1 def test_fetch_billing_data_for_day_is_grouped_by_rate_mulitplier(notify_db_session): diff --git a/tests/app/dao/test_provider_details_dao.py b/tests/app/dao/test_provider_details_dao.py index 939f8b9d8..1ca40cedd 100644 --- a/tests/app/dao/test_provider_details_dao.py +++ b/tests/app/dao/test_provider_details_dao.py @@ -23,7 +23,7 @@ from tests.conftest import set_config @pytest.fixture(autouse=True) def set_provider_resting_points(notify_api): - with set_config(notify_api, 'SMS_PROVIDER_RESTING_POINTS', {'mmg': 60, 'firetext': 40}): + with set_config(notify_api, 'SMS_PROVIDER_RESTING_POINTS', {'sns': 100}): yield @@ -69,11 +69,10 @@ def test_can_get_email_providers(notify_db_session): assert all('email' == notification_type for notification_type in types) -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") def test_should_not_error_if_any_provider_in_code_not_in_database(restore_provider_details): - ProviderDetails.query.filter_by(identifier='mmg').delete() + ProviderDetails.query.filter_by(identifier='sns').delete() - assert notification_provider_clients.get_sms_client('mmg') + assert notification_provider_clients.get_sms_client('sns') @freeze_time('2000-01-01T00:00:00') @@ -108,20 +107,22 @@ def test_update_adds_history(restore_provider_details): def test_update_sms_provider_to_inactive_sets_inactive(restore_provider_details): - mmg = get_provider_details_by_identifier('mmg') + sns = get_provider_details_by_identifier('sns') - mmg.active = False - dao_update_provider_details(mmg) + sns.active = False + dao_update_provider_details(sns) - assert not mmg.active + assert not sns.active @pytest.mark.parametrize('identifier, expected', [ - ('firetext', 'mmg'), - ('mmg', 'firetext'), + ('sns', 'other') ]) def test_get_alternative_sms_provider_returns_expected_provider(identifier, expected): - assert get_alternative_sms_provider(identifier) == expected + """Currently always raises, as we only have SNS configured""" + with pytest.raises(Exception): + get_alternative_sms_provider(identifier) + # assert get_alternative_sms_provider(identifier) == expected def test_get_alternative_sms_provider_fails_if_unrecognised(): @@ -133,38 +134,37 @@ def test_get_alternative_sms_provider_fails_if_unrecognised(): def test_adjust_provider_priority_sets_priority( restore_provider_details, notify_user, - mmg_provider, + sns_provider, ): # need to update these manually to avoid triggering the `onupdate` clause of the updated_at column - ProviderDetails.query.filter(ProviderDetails.identifier == 'mmg').update({'updated_at': datetime.min}) + ProviderDetails.query.filter(ProviderDetails.identifier == 'sns').update({'updated_at': datetime.min}) - _adjust_provider_priority(mmg_provider, 50) + _adjust_provider_priority(sns_provider, 50) - assert mmg_provider.updated_at == datetime.utcnow() - assert mmg_provider.created_by.id == notify_user.id - assert mmg_provider.priority == 50 + assert sns_provider.updated_at == datetime.utcnow() + assert sns_provider.created_by.id == notify_user.id + assert sns_provider.priority == 50 -@pytest.mark.skip(reason="Needs updating for TTS: MMG removal") @freeze_time('2016-01-01 00:30') def test_adjust_provider_priority_adds_history( restore_provider_details, notify_user, - mmg_provider, + sns_provider, ): # need to update these manually to avoid triggering the `onupdate` clause of the updated_at column - ProviderDetails.query.filter(ProviderDetails.identifier == 'mmg').update({'updated_at': datetime.min}) + ProviderDetails.query.filter(ProviderDetails.identifier == 'sns').update({'updated_at': datetime.min}) old_provider_history_rows = ProviderDetailsHistory.query.filter( - ProviderDetailsHistory.id == mmg_provider.id + ProviderDetailsHistory.id == sns_provider.id ).order_by( desc(ProviderDetailsHistory.version) ).all() - _adjust_provider_priority(mmg_provider, 50) + _adjust_provider_priority(sns_provider, 50) updated_provider_history_rows = ProviderDetailsHistory.query.filter( - ProviderDetailsHistory.id == mmg_provider.id + ProviderDetailsHistory.id == sns_provider.id ).order_by( desc(ProviderDetailsHistory.version) ).all() @@ -174,42 +174,38 @@ def test_adjust_provider_priority_adds_history( assert updated_provider_history_rows[0].priority == 50 -@pytest.mark.skip(reason="Needs updating for TTS: MMG removal") @freeze_time('2016-01-01 01:00') def test_get_sms_providers_for_update_returns_providers(restore_provider_details): - sixty_one_minutes_ago = datetime(2015, 12, 31, 23, 59) - ProviderDetails.query.filter(ProviderDetails.identifier == 'mmg').update({'updated_at': sixty_one_minutes_ago}) - ProviderDetails.query.filter(ProviderDetails.identifier == 'firetext').update({'updated_at': None}) + ProviderDetails.query.filter(ProviderDetails.identifier == 'sns').update({'updated_at': None}) resp = _get_sms_providers_for_update(timedelta(hours=1)) - assert {p.identifier for p in resp} == {'mmg', 'firetext'} + assert {p.identifier for p in resp} == {'sns'} -@pytest.mark.skip(reason="Needs updating for TTS: MMG removal") @freeze_time('2016-01-01 01:00') def test_get_sms_providers_for_update_returns_nothing_if_recent_updates(restore_provider_details): fifty_nine_minutes_ago = datetime(2016, 1, 1, 0, 1) - ProviderDetails.query.filter(ProviderDetails.identifier == 'mmg').update({'updated_at': fifty_nine_minutes_ago}) + ProviderDetails.query.filter(ProviderDetails.identifier == 'sns').update({'updated_at': fifty_nine_minutes_ago}) resp = _get_sms_providers_for_update(timedelta(hours=1)) assert not resp -@pytest.mark.skip(reason="Needs updating for TTS: MMG removal") +@pytest.mark.skip(reason="Reenable if/when we add a second SMS provider") @pytest.mark.parametrize(['starting_priorities', 'expected_priorities'], [ - ({'mmg': 50, 'firetext': 50}, {'mmg': 40, 'firetext': 60}), - ({'mmg': 0, 'firetext': 20}, {'mmg': 0, 'firetext': 30}), # lower bound respected - ({'mmg': 50, 'firetext': 100}, {'mmg': 40, 'firetext': 100}), # upper bound respected + ({'sns': 50, 'other': 50}, {'sns': 40, 'other': 60}), + ({'sns': 0, 'other': 20}, {'sns': 0, 'other': 30}), # lower bound respected + ({'sns': 50, 'other': 100}, {'sns': 40, 'other': 100}), # upper bound respected # document what happens if they have unexpected values outside of the 0 - 100 range (due to manual setting from # the admin app). the code never causes further issues, but sometimes doesn't actively reset the vaues to 0-100. - ({'mmg': 150, 'firetext': 50}, {'mmg': 140, 'firetext': 60}), - ({'mmg': 50, 'firetext': 150}, {'mmg': 40, 'firetext': 100}), + ({'sns': 150, 'other': 50}, {'sns': 140, 'other': 60}), + ({'sns': 50, 'other': 150}, {'sns': 40, 'other': 100}), - ({'mmg': -100, 'firetext': 50}, {'mmg': 0, 'firetext': 60}), - ({'mmg': 50, 'firetext': -100}, {'mmg': 40, 'firetext': -90}), + ({'sns': -100, 'other': 50}, {'sns': 0, 'other': 60}), + ({'sns': 50, 'other': -100}, {'sns': 40, 'other': -90}), ]) def test_reduce_sms_provider_priority_adjusts_provider_priorities( mocker, @@ -220,19 +216,19 @@ def test_reduce_sms_provider_priority_adjusts_provider_priorities( ): mock_adjust = mocker.patch('app.dao.provider_details_dao._adjust_provider_priority') - mmg = get_provider_details_by_identifier('mmg') - firetext = get_provider_details_by_identifier('firetext') + sns = get_provider_details_by_identifier('sns') + other = get_provider_details_by_identifier('other') - mmg.priority = starting_priorities['mmg'] - firetext.priority = starting_priorities['firetext'] + sns.priority = starting_priorities['sns'] + other.priority = starting_priorities['other'] # need to update these manually to avoid triggering the `onupdate` clause of the updated_at column ProviderDetails.query.filter(ProviderDetails.notification_type == 'sms').update({'updated_at': datetime.min}) - # switch away from mmg. currently both 50/50 - dao_reduce_sms_provider_priority('mmg', time_threshold=timedelta(minutes=10)) + # switch away from sns. currently both 50/50 + dao_reduce_sms_provider_priority('sns', time_threshold=timedelta(minutes=10)) - mock_adjust.assert_any_call(firetext, expected_priorities['firetext']) - mock_adjust.assert_any_call(mmg, expected_priorities['mmg']) + mock_adjust.assert_any_call(other, expected_priorities['other']) + mock_adjust.assert_any_call(sns, expected_priorities['sns']) def test_reduce_sms_provider_priority_does_nothing_if_providers_have_recently_changed( @@ -242,28 +238,25 @@ def test_reduce_sms_provider_priority_does_nothing_if_providers_have_recently_ch mock_get_providers = mocker.patch('app.dao.provider_details_dao._get_sms_providers_for_update', return_value=[]) mock_adjust = mocker.patch('app.dao.provider_details_dao._adjust_provider_priority') - dao_reduce_sms_provider_priority('firetext', time_threshold=timedelta(minutes=5)) + dao_reduce_sms_provider_priority('sns', time_threshold=timedelta(minutes=5)) mock_get_providers.assert_called_once_with(timedelta(minutes=5)) assert mock_adjust.called is False -@pytest.mark.skip(reason="Needs updating for TTS: Failing for unknown reason") def test_reduce_sms_provider_priority_does_nothing_if_there_is_only_one_active_provider( mocker, restore_provider_details, ): - firetext = get_provider_details_by_identifier('firetext') - firetext.active = False - mock_adjust = mocker.patch('app.dao.provider_details_dao._adjust_provider_priority') - dao_reduce_sms_provider_priority('firetext', time_threshold=timedelta(minutes=5)) + dao_reduce_sms_provider_priority('sns', time_threshold=timedelta(minutes=5)) assert mock_adjust.called is False -@pytest.mark.parametrize('existing_mmg, existing_firetext, new_mmg, new_firetext', [ +@pytest.mark.skip(reason="Reenable if/when we add a second SMS provider") +@pytest.mark.parametrize('existing_sns, existing_other, new_sns, new_other', [ (50, 50, 60, 40), # not just 50/50 - 60/40 specifically (65, 35, 60, 40), # doesn't overshoot if there's less than 10 difference (0, 100, 10, 90), # only adjusts by 10 @@ -272,39 +265,37 @@ def test_reduce_sms_provider_priority_does_nothing_if_there_is_only_one_active_p def test_adjust_provider_priority_back_to_resting_points_updates_all_providers( restore_provider_details, mocker, - existing_mmg, - existing_firetext, - new_mmg, - new_firetext + existing_sns, + existing_other, + new_sns, + new_other ): - mmg = get_provider_details_by_identifier('mmg') - firetext = get_provider_details_by_identifier('firetext') - mmg.priority = existing_mmg - firetext.priority = existing_firetext + sns = get_provider_details_by_identifier('sns') + other = get_provider_details_by_identifier('other') + sns.priority = existing_sns + other.priority = existing_other mock_adjust = mocker.patch('app.dao.provider_details_dao._adjust_provider_priority') mock_get_providers = mocker.patch('app.dao.provider_details_dao._get_sms_providers_for_update', return_value=[ - mmg, firetext + sns, other ]) dao_adjust_provider_priority_back_to_resting_points() mock_get_providers.assert_called_once_with(timedelta(hours=1)) - mock_adjust.assert_any_call(mmg, new_mmg) - mock_adjust.assert_any_call(firetext, new_firetext) + mock_adjust.assert_any_call(sns, new_sns) + mock_adjust.assert_any_call(other, new_other) def test_adjust_provider_priority_back_to_resting_points_does_nothing_if_theyre_already_at_right_values( restore_provider_details, mocker, ): - mmg = get_provider_details_by_identifier('mmg') - firetext = get_provider_details_by_identifier('firetext') - mmg.priority = 60 - firetext.priority = 40 + sns = get_provider_details_by_identifier('sns') + sns.priority = 100 mock_adjust = mocker.patch('app.dao.provider_details_dao._adjust_provider_priority') - mocker.patch('app.dao.provider_details_dao._get_sms_providers_for_update', return_value=[mmg, firetext]) + mocker.patch('app.dao.provider_details_dao._get_sms_providers_for_update', return_value=[sns]) dao_adjust_provider_priority_back_to_resting_points() @@ -323,7 +314,6 @@ def test_adjust_provider_priority_back_to_resting_points_does_nothing_if_no_prov assert mock_adjust.called is False -@pytest.mark.skip(reason="Needs updating for TTS: New SMS provider") @freeze_time('2018-06-28 12:00') def test_dao_get_provider_stats(notify_db_session): service_1 = create_service(service_name='1') @@ -331,34 +321,23 @@ def test_dao_get_provider_stats(notify_db_session): sms_template_1 = create_template(service_1, 'sms') sms_template_2 = create_template(service_2, 'sms') - create_ft_billing('2017-06-05', sms_template_2, provider='firetext', billable_unit=4) - create_ft_billing('2018-05-31', sms_template_1, provider='mmg', billable_unit=1) - create_ft_billing('2018-06-01', sms_template_1, provider='mmg', - rate_multiplier=2, billable_unit=1) - create_ft_billing('2018-06-03', sms_template_2, provider='firetext', billable_unit=4) - create_ft_billing('2018-06-15', sms_template_1, provider='firetext', billable_unit=1) - create_ft_billing('2018-06-28', sms_template_2, provider='mmg', billable_unit=2) + create_ft_billing('2017-06-05', sms_template_2, provider='sns', billable_unit=4) + create_ft_billing('2018-06-03', sms_template_2, provider='sns', billable_unit=4) + create_ft_billing('2018-06-15', sms_template_1, provider='sns', billable_unit=1) results = dao_get_provider_stats() assert len(results) > 0 ses = next(result for result in results if result.identifier == 'ses') - firetext = next(result for result in results if result.identifier == 'firetext') - mmg = next(result for result in results if result.identifier == 'mmg') + sns = next(result for result in results if result.identifier == 'sns') assert ses.display_name == 'AWS SES' assert ses.created_by_name is None assert ses.current_month_billable_sms == 0 - assert firetext.display_name == 'Firetext' - assert firetext.notification_type == 'sms' - assert firetext.supports_international is False - assert firetext.active is True - assert firetext.current_month_billable_sms == 5 - - assert mmg.identifier == 'mmg' - assert mmg.display_name == 'MMG' - assert mmg.supports_international is True - assert mmg.active is True - assert mmg.current_month_billable_sms == 4 + assert sns.display_name == 'AWS SNS' + assert sns.notification_type == 'sms' + assert sns.supports_international is True + assert sns.active is True + assert sns.current_month_billable_sms == 5 diff --git a/tests/app/dao/test_services_dao.py b/tests/app/dao/test_services_dao.py index 899f003c4..afc5dfc38 100644 --- a/tests/app/dao/test_services_dao.py +++ b/tests/app/dao/test_services_dao.py @@ -258,7 +258,7 @@ def test_should_add_user_to_service(notify_db_session): name='Test User', email_address='new_user@digital.cabinet-office.gov.uk', password='password', - mobile_number='+447700900986' + mobile_number='+12028675309' ) save_model_user(new_user, validated_email_access=True) dao_add_user_to_service(service, new_user) @@ -327,7 +327,7 @@ def test_should_remove_user_from_service(notify_db_session): name='Test User', email_address='new_user@digital.cabinet-office.gov.uk', password='password', - mobile_number='+447700900986' + mobile_number='+12028675309' ) save_model_user(new_user, validated_email_access=True) dao_add_user_to_service(service, new_user) @@ -428,7 +428,7 @@ def test_get_all_user_services_only_returns_services_user_has_access_to(notify_d name='Test User', email_address='new_user@digital.cabinet-office.gov.uk', password='password', - mobile_number='+447700900986' + mobile_number='+12028675309' ) save_model_user(new_user, validated_email_access=True) dao_add_user_to_service(service_3, new_user) @@ -486,7 +486,7 @@ def test_dao_fetch_live_services_data(sample_user): assert results == [ {'service_id': mock.ANY, 'service_name': 'Sample service', 'organisation_name': 'test_org_1', 'organisation_type': 'federal', 'consent_to_research': None, 'contact_name': 'Test User', - 'contact_email': 'notify@digital.cabinet-office.gov.uk', 'contact_mobile': '+447700900986', + 'contact_email': 'notify@digital.cabinet-office.gov.uk', 'contact_mobile': '+12028675309', 'live_date': datetime(2014, 4, 20, 10, 0), 'sms_volume_intent': None, 'email_volume_intent': None, 'letter_volume_intent': None, 'sms_totals': 2, 'email_totals': 1, 'letter_totals': 1, 'free_sms_fragment_limit': 100}, @@ -498,7 +498,7 @@ def test_dao_fetch_live_services_data(sample_user): 'free_sms_fragment_limit': 200}, {'service_id': mock.ANY, 'service_name': 'second', 'organisation_name': None, 'consent_to_research': None, 'contact_name': 'Test User', 'contact_email': 'notify@digital.cabinet-office.gov.uk', - 'contact_mobile': '+447700900986', 'live_date': datetime(2017, 4, 20, 10, 0), 'sms_volume_intent': None, + 'contact_mobile': '+12028675309', 'live_date': datetime(2017, 4, 20, 10, 0), 'sms_volume_intent': None, 'organisation_type': None, 'email_volume_intent': None, 'letter_volume_intent': None, 'sms_totals': 0, 'email_totals': 0, 'letter_totals': 1, 'free_sms_fragment_limit': 300} @@ -748,7 +748,7 @@ def test_add_existing_user_to_another_service_doesnot_change_old_permissions(not name='Other Test User', email_address='other_user@digital.cabinet-office.gov.uk', password='password', - mobile_number='+447700900987' + mobile_number='+12028672000' ) save_model_user(other_user, validated_email_access=True) service_two = Service(name="service_two", diff --git a/tests/app/dao/test_users_dao.py b/tests/app/dao/test_users_dao.py index c128d594d..2e953a4e7 100644 --- a/tests/app/dao/test_users_dao.py +++ b/tests/app/dao/test_users_dao.py @@ -37,11 +37,11 @@ from tests.app.db import ( @freeze_time('2020-01-28T12:00:00') -@pytest.mark.parametrize('phone_number', [ - '+447700900986', - '+1-800-555-5555', +@pytest.mark.parametrize('phone_number, expected_phone_number', [ + ('2028675309', '+12028675309'), + ('+1-800-555-5555', '+18005555555'), ]) -def test_create_user(notify_db_session, phone_number): +def test_create_user(notify_db_session, phone_number, expected_phone_number): email = 'notify@digital.cabinet-office.gov.uk' data = { 'name': 'Test User', @@ -55,7 +55,7 @@ def test_create_user(notify_db_session, phone_number): user_query = User.query.first() assert user_query.email_address == email assert user_query.id == user.id - assert user_query.mobile_number == phone_number + assert user_query.mobile_number == expected_phone_number assert user_query.email_access_validated_at == datetime.utcnow() assert not user_query.platform_admin diff --git a/tests/app/db.py b/tests/app/db.py index 729ed8b29..edcfd6cd7 100644 --- a/tests/app/db.py +++ b/tests/app/db.py @@ -72,7 +72,7 @@ from app.models import ( def create_user( *, - mobile_number="+447700900986", + mobile_number="+12028675309", email=None, state='active', id_=None, diff --git a/tests/app/delivery/test_send_to_providers.py b/tests/app/delivery/test_send_to_providers.py index eda4edae3..48ff35368 100644 --- a/tests/app/delivery/test_send_to_providers.py +++ b/tests/app/delivery/test_send_to_providers.py @@ -6,9 +6,11 @@ from unittest.mock import ANY import pytest from flask import current_app +# from notifications_utils.recipients import validate_and_format_phone_number +from requests import HTTPError import app -# from app import firetext_client, mmg_client, notification_provider_clients +from app import aws_sns_client, notification_provider_clients from app.dao import notifications_dao from app.dao.provider_details_dao import get_provider_details_by_identifier from app.delivery import send_to_providers @@ -25,16 +27,16 @@ from app.models import ( Notification, ) from app.serialised_models import SerialisedService -from tests.app.db import ( # create_service,; create_service_with_defined_sms_sender,; create_template, +from tests.app.db import ( create_email_branding, create_notification, create_reply_to_email, + create_service, create_service_sms_sender, + create_service_with_defined_sms_sender, + create_template, ) -# from notifications_utils.recipients import validate_and_format_phone_number -# from requests import HTTPError - def setup_function(_function): # pytest will run this function before each test. It makes sure the @@ -42,21 +44,21 @@ def setup_function(_function): send_to_providers.provider_cache.clear() -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") +@pytest.mark.skip(reason="Reenable when we have more than 1 SMS provider") def test_provider_to_use_should_return_random_provider(mocker, notify_db_session): - mmg = get_provider_details_by_identifier('mmg') - firetext = get_provider_details_by_identifier('firetext') - mmg.priority = 25 - firetext.priority = 75 - mock_choices = mocker.patch('app.delivery.send_to_providers.random.choices', return_value=[mmg]) + sns = get_provider_details_by_identifier('sns') + other = get_provider_details_by_identifier('other') + sns.priority = 60 + other.priority = 40 + mock_choices = mocker.patch('app.delivery.send_to_providers.random.choices', return_value=[sns]) - ret = send_to_providers.provider_to_use('sms', international=False) + ret = send_to_providers.provider_to_use('sms', international=True) - mock_choices.assert_called_once_with([mmg, firetext], weights=[25, 75]) - assert ret.name == 'mmg' + mock_choices.assert_called_once_with([sns, other], weights=[60, 40]) + assert ret.name == 'sns' -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") +@pytest.mark.skip(reason="Reenable when we have more than 1 SMS provider") def test_provider_to_use_should_cache_repeated_calls(mocker, notify_db_session): mock_choices = mocker.patch( 'app.delivery.send_to_providers.random.choices', @@ -72,85 +74,78 @@ def test_provider_to_use_should_cache_repeated_calls(mocker, notify_db_session): assert len(mock_choices.call_args_list) == 1 -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") @pytest.mark.parametrize('international_provider_priority', ( # Since there’s only one international provider it should always # be used, no matter what its priority is set to 0, 50, 100, )) -def test_provider_to_use_should_only_return_mmg_for_international( +def test_provider_to_use_should_only_return_sns_for_international( mocker, notify_db_session, international_provider_priority, ): - mmg = get_provider_details_by_identifier('mmg') - mmg.priority = international_provider_priority - mock_choices = mocker.patch('app.delivery.send_to_providers.random.choices', return_value=[mmg]) + sns = get_provider_details_by_identifier('sns') + sns.priority = international_provider_priority ret = send_to_providers.provider_to_use('sms', international=True) - mock_choices.assert_called_once_with([mmg], weights=[100]) - assert ret.name == 'mmg' + assert ret.name == 'sns' -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") +@pytest.mark.skip(reason="Reenable when we have more than 1 SMS provider") def test_provider_to_use_should_only_return_active_providers(mocker, restore_provider_details): - mmg = get_provider_details_by_identifier('mmg') - firetext = get_provider_details_by_identifier('firetext') - mmg.active = False - mock_choices = mocker.patch('app.delivery.send_to_providers.random.choices', return_value=[firetext]) + sns = get_provider_details_by_identifier('sns') + other = get_provider_details_by_identifier('other') + sns.active = False + other.active = True ret = send_to_providers.provider_to_use('sms') - mock_choices.assert_called_once_with([firetext], weights=[100]) - assert ret.name == 'firetext' + assert ret.name == 'other' -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_provider_to_use_raises_if_no_active_providers(mocker, restore_provider_details): - mmg = get_provider_details_by_identifier('mmg') - mmg.active = False + sns = get_provider_details_by_identifier('sns') + sns.active = False with pytest.raises(Exception): - send_to_providers.provider_to_use('sms', international=True) + send_to_providers.provider_to_use('sms') -# @pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -# def test_should_send_personalised_template_to_correct_sms_provider_and_persist( -# sample_sms_template_with_html, -# mocker -# ): -# db_notification = create_notification(template=sample_sms_template_with_html, -# to_field="+447234123123", personalisation={"name": "Jo"}, -# status='created', -# reply_to_text=sample_sms_template_with_html.service.get_default_sms_sender(), -# normalised_to="447234123123" -# ) +def test_should_send_personalised_template_to_correct_sms_provider_and_persist( + sample_sms_template_with_html, + mocker +): + db_notification = create_notification(template=sample_sms_template_with_html, + to_field="2028675309", personalisation={"name": "Jo"}, + status='created', + reply_to_text=sample_sms_template_with_html.service.get_default_sms_sender(), + normalised_to="2028675309" + ) -# mocker.patch('app.mmg_client.send_sms') + mocker.patch('app.aws_sns_client.send_sms') -# send_to_providers.send_sms_to_provider( -# db_notification -# ) + send_to_providers.send_sms_to_provider( + db_notification + ) -# mmg_client.send_sms.assert_called_once_with( -# to="447234123123", -# content="Sample service: Hello Jo\nHere is some HTML & entities", -# reference=str(db_notification.id), -# sender=current_app.config['FROM_NUMBER'], -# international=False -# ) + aws_sns_client.send_sms.assert_called_once_with( + to="2028675309", + content="Sample service: Hello Jo\nHere is some HTML & entities", + reference=str(db_notification.id), + sender=current_app.config['FROM_NUMBER'], + international=False + ) -# notification = Notification.query.filter_by(id=db_notification.id).one() + notification = Notification.query.filter_by(id=db_notification.id).one() -# assert notification.status == 'sending' -# assert notification.sent_at <= datetime.utcnow() -# assert notification.sent_by == 'mmg' -# assert notification.billable_units == 1 -# assert notification.personalisation == {"name": "Jo"} + assert notification.status == 'sent' + assert notification.sent_at <= datetime.utcnow() + assert notification.sent_by == 'sns' + assert notification.billable_units == 1 + assert notification.personalisation == {"name": "Jo"} -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_should_send_personalised_template_to_correct_email_provider_and_persist( sample_email_template_with_html, mocker @@ -169,7 +164,7 @@ def test_should_send_personalised_template_to_correct_email_provider_and_persist ) app.aws_ses_client.send_email.assert_called_once_with( - '"Sample service" ', + '"Sample service" ', 'jo.smith@example.com', 'Jo some HTML', body='Hello Jo\nThis is an email from GOV.\u200bUK with some HTML\n', @@ -187,7 +182,6 @@ def test_should_send_personalised_template_to_correct_email_provider_and_persist assert notification.personalisation == {"name": "Jo"} -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_should_not_send_email_message_when_service_is_inactive_notifcation_is_in_tech_failure( sample_service, sample_notification, mocker ): @@ -201,12 +195,10 @@ def test_should_not_send_email_message_when_service_is_inactive_notifcation_is_i assert Notification.query.get(sample_notification.id).status == 'technical-failure' -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -@pytest.mark.parametrize("client_send", ["app.mmg_client.send_sms", "app.firetext_client.send_sms"]) def test_should_not_send_sms_message_when_service_is_inactive_notification_is_in_tech_failure( - sample_service, sample_notification, mocker, client_send): + sample_service, sample_notification, mocker): sample_service.active = False - send_mock = mocker.patch(client_send, return_value='reference') + send_mock = mocker.patch("app.aws_sns_client.send_sms", return_value='reference') with pytest.raises(NotificationTechnicalFailureException) as e: send_to_providers.send_sms_to_provider(sample_notification) @@ -215,153 +207,148 @@ def test_should_not_send_sms_message_when_service_is_inactive_notification_is_in assert Notification.query.get(sample_notification.id).status == 'technical-failure' -# @pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -# def test_send_sms_should_use_template_version_from_notification_not_latest( -# sample_template, -# mocker): -# db_notification = create_notification(template=sample_template, to_field='+447234123123', status='created', -# reply_to_text=sample_template.service.get_default_sms_sender(), -# normalised_to='447234123123') +def test_send_sms_should_use_template_version_from_notification_not_latest( + sample_template, + mocker): + db_notification = create_notification(template=sample_template, to_field='2028675309', status='created', + reply_to_text=sample_template.service.get_default_sms_sender(), + normalised_to='2028675309') -# mocker.patch('app.mmg_client.send_sms') + mocker.patch('app.aws_sns_client.send_sms') -# version_on_notification = sample_template.version -# expected_template_id = sample_template.id + version_on_notification = sample_template.version + expected_template_id = sample_template.id -# # Change the template -# from app.dao.templates_dao import ( -# dao_get_template_by_id, -# dao_update_template, -# ) -# sample_template.content = sample_template.content + " another version of the template" -# dao_update_template(sample_template) -# t = dao_get_template_by_id(sample_template.id) -# assert t.version > version_on_notification + # Change the template + from app.dao.templates_dao import ( + dao_get_template_by_id, + dao_update_template, + ) + sample_template.content = sample_template.content + " another version of the template" + dao_update_template(sample_template) + t = dao_get_template_by_id(sample_template.id) + assert t.version > version_on_notification -# send_to_providers.send_sms_to_provider( -# db_notification -# ) + send_to_providers.send_sms_to_provider( + db_notification + ) -# mmg_client.send_sms.assert_called_once_with( -# to=validate_and_format_phone_number("+447234123123"), -# content="Sample service: This is a template:\nwith a newline", -# reference=str(db_notification.id), -# sender=current_app.config['FROM_NUMBER'], -# international=False -# ) + aws_sns_client.send_sms.assert_called_once_with( + to="2028675309", + content="Sample service: This is a template:\nwith a newline", + reference=str(db_notification.id), + sender=current_app.config['FROM_NUMBER'], + international=False + ) -# t = dao_get_template_by_id(expected_template_id) + t = dao_get_template_by_id(expected_template_id) -# persisted_notification = notifications_dao.get_notification_by_id(db_notification.id) -# assert persisted_notification.to == db_notification.to -# assert persisted_notification.template_id == expected_template_id -# assert persisted_notification.template_version == version_on_notification -# assert persisted_notification.template_version != t.version -# assert persisted_notification.status == 'sending' -# assert not persisted_notification.personalisation + persisted_notification = notifications_dao.get_notification_by_id(db_notification.id) + assert persisted_notification.to == db_notification.to + assert persisted_notification.template_id == expected_template_id + assert persisted_notification.template_version == version_on_notification + assert persisted_notification.template_version != t.version + assert persisted_notification.status == 'sent' + assert not persisted_notification.personalisation -# @pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -# @pytest.mark.parametrize('research_mode,key_type', [ -# (True, KEY_TYPE_NORMAL), -# (False, KEY_TYPE_TEST) -# ]) -# def test_should_call_send_sms_response_task_if_research_mode( -# notify_db_session, sample_service, sample_notification, mocker, research_mode, key_type -# ): -# mocker.patch('app.mmg_client.send_sms') -# mocker.patch('app.delivery.send_to_providers.send_sms_response') +@pytest.mark.parametrize('research_mode,key_type', [ + (True, KEY_TYPE_NORMAL), + (False, KEY_TYPE_TEST) +]) +def test_should_call_send_sms_response_task_if_research_mode( + notify_db_session, sample_service, sample_notification, mocker, research_mode, key_type +): + mocker.patch('app.aws_sns_client.send_sms') + mocker.patch('app.delivery.send_to_providers.send_sms_response') -# if research_mode: -# sample_service.research_mode = True -# notify_db_session.add(sample_service) -# notify_db_session.commit() + if research_mode: + sample_service.research_mode = True + notify_db_session.add(sample_service) + notify_db_session.commit() -# sample_notification.key_type = key_type + sample_notification.key_type = key_type -# send_to_providers.send_sms_to_provider( -# sample_notification -# ) -# assert not mmg_client.send_sms.called + send_to_providers.send_sms_to_provider( + sample_notification + ) + assert not aws_sns_client.send_sms.called -# app.delivery.send_to_providers.send_sms_response.assert_called_once_with( -# 'mmg', str(sample_notification.id), sample_notification.to -# ) + app.delivery.send_to_providers.send_sms_response.assert_called_once_with( + 'sns', str(sample_notification.id), sample_notification.to + ) -# persisted_notification = notifications_dao.get_notification_by_id(sample_notification.id) -# assert persisted_notification.to == sample_notification.to -# assert persisted_notification.template_id == sample_notification.template_id -# assert persisted_notification.status == 'sending' -# assert persisted_notification.sent_at <= datetime.utcnow() -# assert persisted_notification.sent_by == 'mmg' -# assert not persisted_notification.personalisation + persisted_notification = notifications_dao.get_notification_by_id(sample_notification.id) + assert persisted_notification.to == sample_notification.to + assert persisted_notification.template_id == sample_notification.template_id + assert persisted_notification.status == 'sent' + assert persisted_notification.sent_at <= datetime.utcnow() + assert persisted_notification.sent_by == 'sns' + assert not persisted_notification.personalisation -# @pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -# def test_should_have_sending_status_if_fake_callback_function_fails(sample_notification, mocker): -# mocker.patch('app.delivery.send_to_providers.send_sms_response', side_effect=HTTPError) +@pytest.mark.skip(reason="Needs updating when we get SMS delivery receipts done") +def test_should_have_sending_status_if_fake_callback_function_fails(sample_notification, mocker): + mocker.patch('app.delivery.send_to_providers.send_sms_response', side_effect=HTTPError) -# sample_notification.key_type = KEY_TYPE_TEST + sample_notification.key_type = KEY_TYPE_TEST -# with pytest.raises(HTTPError): -# send_to_providers.send_sms_to_provider( -# sample_notification -# ) -# assert sample_notification.status == 'sending' -# assert sample_notification.sent_by == 'mmg' + with pytest.raises(HTTPError): + send_to_providers.send_sms_to_provider( + sample_notification + ) + assert sample_notification.status == 'sending' + assert sample_notification.sent_by == 'sns' -# @pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -# def test_should_not_send_to_provider_when_status_is_not_created( -# sample_template, -# mocker -# ): -# notification = create_notification(template=sample_template, status='sending') -# mocker.patch('app.mmg_client.send_sms') -# response_mock = mocker.patch('app.delivery.send_to_providers.send_sms_response') +def test_should_not_send_to_provider_when_status_is_not_created( + sample_template, + mocker +): + notification = create_notification(template=sample_template, status='sending') + mocker.patch('app.aws_sns_client.send_sms') + response_mock = mocker.patch('app.delivery.send_to_providers.send_sms_response') -# send_to_providers.send_sms_to_provider( -# notification -# ) + send_to_providers.send_sms_to_provider( + notification + ) -# app.mmg_client.send_sms.assert_not_called() -# response_mock.assert_not_called() + app.aws_sns_client.send_sms.assert_not_called() + response_mock.assert_not_called() -# @pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -# def test_should_send_sms_with_downgraded_content(notify_db_session, mocker): -# # Γ©, o, and u are in GSM. -# # Δ«, grapes, tabs, zero width space and ellipsis are not -# # Γ³ isn't in GSM, but it is in the welsh alphabet so will still be sent -# msg = "a Γ© Δ« o u πŸ‡ foo\tbar\u200bbaz((misc))…" -# placeholder = 'βˆ†βˆ†βˆ†abc' -# gsm_message = "?Γ³dz Housing Service: a Γ© i o u ? foo barbaz???abc..." -# service = create_service(service_name='ŁódΕΊ Housing Service') -# template = create_template(service, content=msg) -# db_notification = create_notification( -# template=template, -# personalisation={'misc': placeholder} -# ) +def test_should_send_sms_with_downgraded_content(notify_db_session, mocker): + # Γ©, o, and u are in GSM. + # Δ«, grapes, tabs, zero width space and ellipsis are not + # Γ³ isn't in GSM, but it is in the welsh alphabet so will still be sent + msg = "a Γ© Δ« o u πŸ‡ foo\tbar\u200bbaz((misc))…" + placeholder = 'βˆ†βˆ†βˆ†abc' + gsm_message = "?Γ³dz Housing Service: a Γ© i o u ? foo barbaz???abc..." + service = create_service(service_name='ŁódΕΊ Housing Service') + template = create_template(service, content=msg) + db_notification = create_notification( + template=template, + personalisation={'misc': placeholder} + ) -# mocker.patch('app.mmg_client.send_sms') + mocker.patch('app.aws_sns_client.send_sms') -# send_to_providers.send_sms_to_provider(db_notification) + send_to_providers.send_sms_to_provider(db_notification) -# mmg_client.send_sms.assert_called_once_with( -# to=ANY, -# content=gsm_message, -# reference=ANY, -# sender=ANY, -# international=False -# ) + aws_sns_client.send_sms.assert_called_once_with( + to=ANY, + content=gsm_message, + reference=ANY, + sender=ANY, + international=False + ) -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_send_sms_should_use_service_sms_sender( sample_service, sample_template, mocker): - mocker.patch('app.mmg_client.send_sms') + mocker.patch('app.aws_sns_client.send_sms') sms_sender = create_service_sms_sender(service=sample_service, sms_sender='123456', is_default=False) db_notification = create_notification(template=sample_template, reply_to_text=sms_sender.sms_sender) @@ -371,7 +358,7 @@ def test_send_sms_should_use_service_sms_sender( db_notification, ) - app.mmg_client.send_sms.assert_called_once_with( + app.aws_sns_client.send_sms.assert_called_once_with( to=ANY, content=ANY, reference=ANY, @@ -380,7 +367,6 @@ def test_send_sms_should_use_service_sms_sender( ) -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") @pytest.mark.parametrize('research_mode,key_type', [ (True, KEY_TYPE_NORMAL), (False, KEY_TYPE_TEST) @@ -421,7 +407,6 @@ def test_send_email_to_provider_should_call_research_mode_task_response_task_if_ assert persisted_notification.billable_units == 0 -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_send_email_to_provider_should_not_send_to_provider_when_status_is_not_created( sample_email_template, mocker @@ -437,7 +422,6 @@ def test_send_email_to_provider_should_not_send_to_provider_when_status_is_not_c app.delivery.send_to_providers.send_email_response.assert_not_called() -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_send_email_should_use_service_reply_to_email( sample_service, sample_email_template, @@ -461,7 +445,6 @@ def test_send_email_should_use_service_reply_to_email( ) -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_get_html_email_renderer_should_return_for_normal_service(sample_service): options = send_to_providers.get_html_email_options(sample_service) assert options['govuk_banner'] is True @@ -512,7 +495,6 @@ def test_get_html_email_renderer_with_branding_details_and_render_govuk_banner_o assert options == {'govuk_banner': True, 'brand_banner': False} -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_get_html_email_renderer_prepends_logo_path(notify_api): Service = namedtuple('Service', ['email_branding']) EmailBranding = namedtuple('EmailBranding', ['brand_type', 'colour', 'name', 'logo', 'text']) @@ -593,22 +575,21 @@ def test_should_not_update_notification_if_research_mode_on_exception( assert update_mock.called -# @pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -# @pytest.mark.parametrize("starting_status, expected_status", [ -# ("delivered", "delivered"), -# ("created", "sending"), -# ("technical-failure", "technical-failure"), -# ]) -# def test_update_notification_to_sending_does_not_update_status_from_a_final_status( -# sample_service, notify_db_session, starting_status, expected_status -# ): -# template = create_template(sample_service) -# notification = create_notification(template=template, status=starting_status) -# send_to_providers.update_notification_to_sending( -# notification, -# notification_provider_clients.get_client_by_name_and_type("mmg", "sms") -# ) -# assert notification.status == expected_status +@pytest.mark.parametrize("starting_status, expected_status", [ + ("delivered", "delivered"), + ("created", "sent"), + ("technical-failure", "technical-failure"), +]) +def test_update_notification_to_sending_does_not_update_status_from_a_final_status( + sample_service, notify_db_session, starting_status, expected_status +): + template = create_template(sample_service) + notification = create_notification(template=template, status=starting_status) + send_to_providers.update_notification_to_sending( + notification, + notification_provider_clients.get_client_by_name_and_type("sns", "sms") + ) + assert notification.status == expected_status def __update_notification(notification_to_update, research_mode, expected_status): @@ -616,14 +597,13 @@ def __update_notification(notification_to_update, research_mode, expected_status notification_to_update.status = expected_status -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") @pytest.mark.parametrize('research_mode,key_type, billable_units, expected_status', [ (True, KEY_TYPE_NORMAL, 0, 'delivered'), - (False, KEY_TYPE_NORMAL, 1, 'sending'), + (False, KEY_TYPE_NORMAL, 1, 'sent'), (False, KEY_TYPE_TEST, 0, 'sending'), (True, KEY_TYPE_TEST, 0, 'sending'), (True, KEY_TYPE_TEAM, 0, 'delivered'), - (False, KEY_TYPE_TEAM, 1, 'sending') + (False, KEY_TYPE_TEAM, 1, 'sent') ]) def test_should_update_billable_units_and_status_according_to_research_mode_and_key_type( sample_template, @@ -634,7 +614,7 @@ def test_should_update_billable_units_and_status_according_to_research_mode_and_ expected_status ): notification = create_notification(template=sample_template, billable_units=0, status='created', key_type=key_type) - mocker.patch('app.mmg_client.send_sms') + mocker.patch('app.aws_sns_client.send_sms') mocker.patch('app.delivery.send_to_providers.send_sms_response', side_effect=__update_notification(notification, research_mode, expected_status)) @@ -648,12 +628,11 @@ def test_should_update_billable_units_and_status_according_to_research_mode_and_ assert notification.status == expected_status -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_should_set_notification_billable_units_and_reduces_provider_priority_if_sending_to_provider_fails( sample_notification, mocker, ): - mocker.patch('app.mmg_client.send_sms', side_effect=Exception()) + mocker.patch('app.aws_sns_client.send_sms', side_effect=Exception()) mock_reduce = mocker.patch('app.delivery.send_to_providers.dao_reduce_sms_provider_priority') sample_notification.billable_units = 0 @@ -663,119 +642,73 @@ def test_should_set_notification_billable_units_and_reduces_provider_priority_if send_to_providers.send_sms_to_provider(sample_notification) assert sample_notification.billable_units == 1 - mock_reduce.assert_called_once_with('mmg', time_threshold=timedelta(minutes=1)) + mock_reduce.assert_called_once_with('sns', time_threshold=timedelta(minutes=1)) -# @pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -# def test_should_send_sms_to_international_providers( -# sample_template, -# sample_user, -# mocker -# ): -# mocker.patch('app.mmg_client.send_sms') -# mocker.patch('app.firetext_client.send_sms') +def test_should_send_sms_to_international_providers( + sample_template, + sample_user, + mocker +): + mocker.patch('app.aws_sns_client.send_sms') -# # set firetext to active -# get_provider_details_by_identifier('firetext').priority = 100 -# get_provider_details_by_identifier('mmg').priority = 0 + notification_international = create_notification( + template=sample_template, + to_field="+6011-17224412", + personalisation={"name": "Jo"}, + status='created', + international=True, + reply_to_text=sample_template.service.get_default_sms_sender(), + normalised_to='601117224412' + ) -# notification_international = create_notification( -# template=sample_template, -# to_field="+6011-17224412", -# personalisation={"name": "Jo"}, -# status='created', -# international=True, -# reply_to_text=sample_template.service.get_default_sms_sender(), -# normalised_to='601117224412' -# ) + send_to_providers.send_sms_to_provider( + notification_international + ) -# send_to_providers.send_sms_to_provider( -# notification_international -# ) + aws_sns_client.send_sms.assert_called_once_with( + to="601117224412", + content=ANY, + reference=str(notification_international.id), + sender=current_app.config['FROM_NUMBER'], + international=True + ) -# mmg_client.send_sms.assert_called_once_with( -# to="601117224412", -# content=ANY, -# reference=str(notification_international.id), -# sender=current_app.config['FROM_NUMBER'], -# international=True -# ) - -# assert notification_international.status == 'sent' -# assert notification_international.sent_by == 'mmg' + assert notification_international.status == 'sent' + assert notification_international.sent_by == 'sns' -# @pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -# def test_should_send_non_international_sms_to_default_provider( -# sample_template, -# sample_user, -# mocker -# ): -# mocker.patch('app.mmg_client.send_sms') -# mocker.patch('app.firetext_client.send_sms') +@pytest.mark.parametrize('sms_sender, expected_sender, prefix_sms, expected_content', [ + ('foo', 'foo', False, 'bar'), + ('foo', 'foo', True, 'Sample service: bar'), + # if 40604 is actually in DB then treat that as if entered manually + ('40604', '40604', False, 'bar'), + # 'testing' is the FROM_NUMBER during unit tests + ('testing', 'testing', True, 'Sample service: bar'), + ('testing', 'testing', False, 'bar'), +]) +def test_should_handle_sms_sender_and_prefix_message( + mocker, + sms_sender, + prefix_sms, + expected_sender, + expected_content, + notify_db_session +): + mocker.patch('app.aws_sns_client.send_sms') + service = create_service_with_defined_sms_sender(sms_sender_value=sms_sender, prefix_sms=prefix_sms) + template = create_template(service, content='bar') + notification = create_notification(template, reply_to_text=sms_sender) -# # set firetext to active -# get_provider_details_by_identifier('firetext').priority = 100 -# get_provider_details_by_identifier('mmg').priority = 0 + send_to_providers.send_sms_to_provider(notification) -# notification_uk = create_notification( -# template=sample_template, -# to_field="+447234123999", -# personalisation={"name": "Jo"}, -# status='created', -# international=False, -# reply_to_text=sample_template.service.get_default_sms_sender(), -# normalised_to="447234123999" -# ) - -# send_to_providers.send_sms_to_provider( -# notification_uk -# ) - -# firetext_client.send_sms.assert_called_once_with( -# to="447234123999", -# content=ANY, -# reference=str(notification_uk.id), -# sender=current_app.config['FROM_NUMBER'], -# international=False -# ) - -# assert notification_uk.status == 'sending' -# assert notification_uk.sent_by == 'firetext' - - -# @pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") -# @pytest.mark.parametrize('sms_sender, expected_sender, prefix_sms, expected_content', [ -# ('foo', 'foo', False, 'bar'), -# ('foo', 'foo', True, 'Sample service: bar'), -# # if 40604 is actually in DB then treat that as if entered manually -# ('40604', '40604', False, 'bar'), -# # 'testing' is the FROM_NUMBER during unit tests -# ('testing', 'testing', True, 'Sample service: bar'), -# ('testing', 'testing', False, 'bar'), -# ]) -# def test_should_handle_sms_sender_and_prefix_message( -# mocker, -# sms_sender, -# prefix_sms, -# expected_sender, -# expected_content, -# notify_db_session -# ): -# mocker.patch('app.mmg_client.send_sms') -# service = create_service_with_defined_sms_sender(sms_sender_value=sms_sender, prefix_sms=prefix_sms) -# template = create_template(service, content='bar') -# notification = create_notification(template, reply_to_text=sms_sender) - -# send_to_providers.send_sms_to_provider(notification) - -# mmg_client.send_sms.assert_called_once_with( -# content=expected_content, -# sender=expected_sender, -# to=ANY, -# reference=ANY, -# international=False -# ) + aws_sns_client.send_sms.assert_called_once_with( + content=expected_content, + sender=expected_sender, + to=ANY, + reference=ANY, + international=False + ) def test_send_email_to_provider_uses_reply_to_from_notification( @@ -799,14 +732,13 @@ def test_send_email_to_provider_uses_reply_to_from_notification( ) -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_send_sms_to_provider_should_use_normalised_to( mocker, client, sample_template ): - send_mock = mocker.patch('app.mmg_client.send_sms') + send_mock = mocker.patch('app.aws_sns_client.send_sms') notification = create_notification(template=sample_template, - to_field='+447700900855', - normalised_to='447700900855') + to_field='+12028675309', + normalised_to='2028675309') send_to_providers.send_sms_to_provider(notification) send_mock.assert_called_once_with(to=notification.normalised_to, content=ANY, @@ -832,7 +764,6 @@ def test_send_email_to_provider_should_user_normalised_to( reply_to_address=notification.reply_to_text) -@pytest.mark.skip(reason="Needs updating for TTS: Update with new providers") def test_send_sms_to_provider_should_return_template_if_found_in_redis( mocker, client, sample_template ): @@ -854,7 +785,7 @@ def test_send_sms_to_provider_should_return_template_if_found_in_redis( 'app.dao.services_dao.dao_fetch_service_by_id' ) - send_mock = mocker.patch('app.mmg_client.send_sms') + send_mock = mocker.patch('app.aws_sns_client.send_sms') notification = create_notification(template=sample_template, to_field='+447700900855', normalised_to='447700900855') diff --git a/tests/app/inbound_number/test_rest.py b/tests/app/inbound_number/test_rest.py index 62e66b51a..90f975271 100644 --- a/tests/app/inbound_number/test_rest.py +++ b/tests/app/inbound_number/test_rest.py @@ -16,7 +16,7 @@ def test_rest_get_inbound_numbers(admin_request, sample_inbound_numbers): def test_rest_get_inbound_number(admin_request, notify_db_session, sample_service): - inbound_number = create_inbound_number(number='1', provider='mmg', active=False, service_id=sample_service.id) + inbound_number = create_inbound_number(number='1', provider='sns', active=False, service_id=sample_service.id) result = admin_request.get( 'inbound_number.get_inbound_number_for_service', @@ -37,7 +37,7 @@ def test_rest_get_inbound_number_when_service_is_not_assigned_returns_empty_dict def test_rest_set_inbound_number_active_flag_off( admin_request, notify_db_session): service = create_service(service_name='test service 1') - create_inbound_number(number='1', provider='mmg', active=True, service_id=service.id) + create_inbound_number(number='1', provider='sns', active=True, service_id=service.id) admin_request.post( 'inbound_number.post_set_inbound_number_off', diff --git a/tests/app/notifications/test_notifications_sms_callbacks.py b/tests/app/notifications/test_notifications_sms_callbacks.py deleted file mode 100644 index 523e7e584..000000000 --- a/tests/app/notifications/test_notifications_sms_callbacks.py +++ /dev/null @@ -1,184 +0,0 @@ -import pytest -from flask import json - -from app.notifications.notifications_sms_callback import validate_callback_data - - -def firetext_post(client, data): - return client.post( - path='/notifications/sms/firetext', - data=data, - headers=[('Content-Type', 'application/x-www-form-urlencoded')]) - - -def mmg_post(client, data): - return client.post( - path='/notifications/sms/mmg', - data=data, - headers=[('Content-Type', 'application/json')]) - - -@pytest.mark.skip(reason="Needs updating for TTS: Firetext removal") -def test_firetext_callback_should_not_need_auth(client, mocker): - mocker.patch('app.notifications.notifications_sms_callback.process_sms_client_response') - data = 'mobile=441234123123&status=0&reference=notification_id&time=2016-03-10 14:17:00' - - response = firetext_post(client, data) - assert response.status_code == 200 - - -@pytest.mark.skip(reason="Needs updating for TTS: Firetext removal") -def test_firetext_callback_should_return_400_if_empty_reference(client, mocker): - data = 'mobile=441234123123&status=0&reference=&time=2016-03-10 14:17:00' - response = firetext_post(client, data) - - json_resp = json.loads(response.get_data(as_text=True)) - assert response.status_code == 400 - assert json_resp['result'] == 'error' - assert json_resp['message'] == ['Firetext callback failed: reference missing'] - - -@pytest.mark.skip(reason="Needs updating for TTS: Firetext removal") -def test_firetext_callback_should_return_400_if_no_reference(client, mocker): - data = 'mobile=441234123123&status=0&time=2016-03-10 14:17:00' - response = firetext_post(client, data) - json_resp = json.loads(response.get_data(as_text=True)) - assert response.status_code == 400 - assert json_resp['result'] == 'error' - assert json_resp['message'] == ['Firetext callback failed: reference missing'] - - -@pytest.mark.skip(reason="Needs updating for TTS: Firetext removal") -def test_firetext_callback_should_return_400_if_no_status(client, mocker): - data = 'mobile=441234123123&time=2016-03-10 14:17:00&reference=notification_id' - response = firetext_post(client, data) - json_resp = json.loads(response.get_data(as_text=True)) - assert response.status_code == 400 - assert json_resp['result'] == 'error' - assert json_resp['message'] == ['Firetext callback failed: status missing'] - - -@pytest.mark.skip(reason="Needs updating for TTS: Firetext removal") -def test_firetext_callback_should_return_200_and_call_task_with_valid_data(client, mocker): - mock_celery = mocker.patch( - 'app.notifications.notifications_sms_callback.process_sms_client_response.apply_async') - - data = 'mobile=441234123123&status=0&time=2016-03-10 14:17:00&reference=notification_id' - response = firetext_post(client, data) - json_resp = json.loads(response.get_data(as_text=True)) - assert response.status_code == 200 - assert json_resp['result'] == 'success' - - mock_celery.assert_called_once_with( - ['0', 'notification_id', 'Firetext', None], - queue='sms-callbacks', - ) - - -@pytest.mark.skip(reason="Needs updating for TTS: Firetext removal") -def test_firetext_callback_including_a_code_should_return_200_and_call_task_with_valid_data(client, mocker): - mock_celery = mocker.patch( - 'app.notifications.notifications_sms_callback.process_sms_client_response.apply_async') - - data = 'mobile=441234123123&status=1&code=101&time=2016-03-10 14:17:00&reference=notification_id' - response = firetext_post(client, data) - json_resp = json.loads(response.get_data(as_text=True)) - assert response.status_code == 200 - assert json_resp['result'] == 'success' - - mock_celery.assert_called_once_with( - ['1', 'notification_id', 'Firetext', '101'], - queue='sms-callbacks', - ) - - -@pytest.mark.skip(reason="Needs updating for TTS: MMG removal") -def test_mmg_callback_should_not_need_auth(client, mocker, sample_notification): - mocker.patch('app.notifications.notifications_sms_callback.process_sms_client_response') - data = json.dumps({"reference": "mmg_reference", - "CID": str(sample_notification.id), - "MSISDN": "447777349060", - "status": "3", - "deliverytime": "2016-04-05 16:01:07"}) - - response = mmg_post(client, data) - assert response.status_code == 200 - - -@pytest.mark.skip(reason="Needs updating for TTS: MMG removal") -def test_process_mmg_response_returns_400_for_malformed_data(client): - data = json.dumps({"reference": "mmg_reference", - "monkey": 'random thing', - "MSISDN": "447777349060", - "no_status": 00, - "deliverytime": "2016-04-05 16:01:07"}) - - response = mmg_post(client, data) - assert response.status_code == 400 - json_data = json.loads(response.data) - assert json_data['result'] == 'error' - assert len(json_data['message']) == 2 - assert "{} callback failed: {} missing".format('MMG', 'status') in json_data['message'] - assert "{} callback failed: {} missing".format('MMG', 'CID') in json_data['message'] - - -@pytest.mark.skip(reason="Needs updating for TTS: MMG removal") -def test_mmg_callback_should_return_200_and_call_task_with_valid_data(client, mocker): - mock_celery = mocker.patch( - 'app.notifications.notifications_sms_callback.process_sms_client_response.apply_async') - data = json.dumps({"reference": "mmg_reference", - "CID": "notification_id", - "MSISDN": "447777349060", - "status": "3", - "substatus": "5", - "deliverytime": "2016-04-05 16:01:07"}) - - response = mmg_post(client, data) - - assert response.status_code == 200 - json_data = json.loads(response.data) - assert json_data['result'] == 'success' - - mock_celery.assert_called_once_with( - ['3', 'notification_id', 'MMG', '5'], - queue='sms-callbacks', - ) - - -def test_validate_callback_data_returns_none_when_valid(): - form = {'status': 'good', - 'reference': 'send-sms-code'} - fields = ['status', 'reference'] - client_name = 'sms client' - - assert validate_callback_data(form, fields, client_name) is None - - -def test_validate_callback_data_return_errors_when_fields_are_empty(): - form = {'monkey': 'good'} - fields = ['status', 'cid'] - client_name = 'sms client' - - errors = validate_callback_data(form, fields, client_name) - assert len(errors) == 2 - assert "{} callback failed: {} missing".format(client_name, 'status') in errors - assert "{} callback failed: {} missing".format(client_name, 'cid') in errors - - -def test_validate_callback_data_can_handle_integers(): - form = {'status': 00, 'cid': 'fsdfadfsdfas'} - fields = ['status', 'cid'] - client_name = 'sms client' - - result = validate_callback_data(form, fields, client_name) - assert result is None - - -def test_validate_callback_data_returns_error_for_empty_string(): - form = {'status': '', 'cid': 'fsdfadfsdfas'} - fields = ['status', 'cid'] - client_name = 'sms client' - - result = validate_callback_data(form, fields, client_name) - assert result is not None - assert "{} callback failed: {} missing".format(client_name, 'status') in result diff --git a/tests/app/notifications/test_process_notification.py b/tests/app/notifications/test_process_notification.py index b0548bda1..c1057f2b5 100644 --- a/tests/app/notifications/test_process_notification.py +++ b/tests/app/notifications/test_process_notification.py @@ -122,7 +122,7 @@ def test_persist_notification_with_optionals(sample_job, sample_api_key): persist_notification( template_id=sample_job.template.id, template_version=sample_job.template.version, - recipient='+447111111111', + recipient='+12028675309', service=sample_job.service, personalisation=None, notification_type='sms', @@ -146,7 +146,7 @@ def test_persist_notification_with_optionals(sample_job, sample_api_key): assert persisted_notification.client_reference == "ref from client" assert persisted_notification.reference is None assert persisted_notification.international is False - assert persisted_notification.phone_prefix == '44' + assert persisted_notification.phone_prefix == '1' assert persisted_notification.rate_multiplier == 1 assert persisted_notification.created_by_id == sample_job.created_by_id assert not persisted_notification.reply_to_text @@ -295,15 +295,15 @@ def test_send_notification_to_queue_throws_exception_deletes_notification(sample @pytest.mark.parametrize("to_address, notification_type, expected", [ - ("+447700900000", "sms", True), - ("+447700900111", "sms", True), - ("+447700900222", "sms", True), - ("07700900000", "sms", True), - ("7700900111", "sms", True), + ("+12028675000", "sms", True), + ("+12028675111", "sms", True), + ("+12028675222", "sms", True), + ("2028675000", "sms", True), + ("2028675111", "sms", True), ("simulate-delivered@notifications.service.gov.uk", "email", True), ("simulate-delivered-2@notifications.service.gov.uk", "email", True), ("simulate-delivered-3@notifications.service.gov.uk", "email", True), - ("07515896969", "sms", False), + ("2028675309", "sms", False), ("valid_email@test.com", "email", False) ]) def test_simulated_recipient(notify_api, to_address, notification_type, expected): @@ -315,7 +315,7 @@ def test_simulated_recipient(notify_api, to_address, notification_type, expected 'simulate-delivered-2@notifications.service.gov.uk', 'simulate-delivered-2@notifications.service.gov.uk' ) - SIMULATED_SMS_NUMBERS = ('+447700900000', '+447700900111', '+447700900222') + SIMULATED_SMS_NUMBERS = ('+12028675000', '+12028675111', '+12028675222') """ formatted_address = None @@ -330,12 +330,10 @@ def test_simulated_recipient(notify_api, to_address, notification_type, expected @pytest.mark.parametrize('recipient, expected_international, expected_prefix, expected_units', [ - ('7900900123', False, '44', 1), # UK - ('+447900900123', False, '44', 1), # UK - ('07700910222', True, '44', 1), # UK (Jersey) - ('07700900222', False, '44', 1), # TV number - ('73122345678', True, '7', 1), # Russia - ('360623400400', True, '36', 3)] # Hungary + ('+447900900123', True, '44', 1), # UK + ('+73122345678', True, '7', 1), # Russia + ('+360623400400', True, '36', 1), # Hungary + ('2028675309', False, '1', 1)] # USA ) def test_persist_notification_with_international_info_stores_correct_info( sample_job, @@ -392,15 +390,12 @@ def test_persist_notification_with_international_info_does_not_store_for_email( @pytest.mark.parametrize('recipient, expected_recipient_normalised', [ - ('7900900123', '447900900123'), - ('+447900 900 123', '447900900123'), - (' 07700900222', '447700900222'), - ('07700900222', '447700900222'), - (' 73122345678', '73122345678'), - ('360623400400', '360623400400'), - ('-077-00900222-', '447700900222'), - ('(360623(400400)', '360623400400') - + ('+4407900900123', '+447900900123'), + ('202-867-5309', '+12028675309'), + ('1 202-867-5309', '+12028675309'), + ('+1 (202) 867-5309', '+12028675309'), + ('(202) 867-5309', '+12028675309'), + ('2028675309', '+12028675309') ]) def test_persist_sms_notification_stores_normalised_number( sample_job, diff --git a/tests/app/notifications/test_receive_notification.py b/tests/app/notifications/test_receive_notification.py index 56efffdfd..89577b3ad 100644 --- a/tests/app/notifications/test_receive_notification.py +++ b/tests/app/notifications/test_receive_notification.py @@ -1,17 +1,13 @@ -import base64 +from base64 import b64encode from datetime import datetime import pytest from flask import json -from freezegun import freeze_time from app.models import EMAIL_TYPE, INBOUND_SMS_TYPE, SMS_TYPE, InboundSms from app.notifications.receive_notifications import ( create_inbound_sms_object, - format_mmg_datetime, - format_mmg_message, has_inbound_sms_permissions, - strip_leading_forty_four, unescape_string, ) from tests.app.db import ( @@ -22,57 +18,41 @@ from tests.app.db import ( from tests.conftest import set_config -def firetext_post(client, data, auth=True, password='testkey'): - headers = [ - ('Content-Type', 'application/x-www-form-urlencoded'), - ] - - if auth: - auth_value = base64.b64encode("notify:{}".format(password).encode('utf-8')).decode('utf-8') - headers.append(('Authorization', 'Basic ' + auth_value)) - - return client.post( - path='/notifications/sms/receive/firetext', - data=data, - headers=headers - ) - - -def mmg_post(client, data, auth=True, password='testkey'): +def sns_post(client, data, auth=True, password='testkey'): headers = [ ('Content-Type', 'application/json'), ] if auth: - auth_value = base64.b64encode("username:{}".format(password).encode('utf-8')).decode('utf-8') - headers.append(('Authorization', 'Basic ' + auth_value)) + auth_value = b64encode(f"notify:{password}".encode()) + headers.append(('Authorization', f"Basic {auth_value}")) return client.post( - path='/notifications/sms/receive/mmg', - data=json.dumps(data), + path='/notifications/sms/receive/sns', + data={"Message": data}, headers=headers ) -def test_receive_notification_returns_received_to_mmg(client, mocker, sample_service_full_permissions): +@pytest.mark.skip(reason="Need to implement SNS tests. Body here mostly from MMG") +def test_receive_notification_returns_received_to_sns(client, mocker, sample_service_full_permissions): mocked = mocker.patch("app.notifications.receive_notifications.tasks.send_inbound_sms_to_service.apply_async") prom_counter_labels_mock = mocker.patch('app.notifications.receive_notifications.INBOUND_SMS_COUNTER.labels') data = { - "ID": "1234", - "MSISDN": "447700900855", - "Message": "Some message to notify", - "Trigger": "Trigger?", - "Number": sample_service_full_permissions.get_inbound_number(), - "Channel": "SMS", - "DateRecieved": "2012-06-27 12:33:00" + "originationNumber": "+12028675309", + "destinationNumber": sample_service_full_permissions.get_inbound_number(), + "messageKeyword": "JOIN", + "messageBody": "EXAMPLE", + "inboundMessageId": "cae173d2-66b9-564c-8309-21f858e9fb84", + "previousPublishedMessageId": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" } - response = mmg_post(client, data) + response = sns_post(client, data) assert response.status_code == 200 result = json.loads(response.get_data(as_text=True)) - assert result['status'] == 'ok' + assert result['result'] == 'success' - prom_counter_labels_mock.assert_called_once_with("mmg") + prom_counter_labels_mock.assert_called_once_with("sns") prom_counter_labels_mock.return_value.inc.assert_called_once_with() inbound_sms_id = InboundSms.query.all()[0].id @@ -84,7 +64,7 @@ def test_receive_notification_returns_received_to_mmg(client, mocker, sample_ser [SMS_TYPE], [INBOUND_SMS_TYPE], ]) -def test_receive_notification_from_mmg_without_permissions_does_not_persist( +def test_receive_notification_from_sns_without_permissions_does_not_persist( client, mocker, notify_db_session, @@ -101,42 +81,17 @@ def test_receive_notification_from_mmg_without_permissions_does_not_persist( "Channel": "SMS", "DateRecieved": "2012-06-27 12:33:00" } - response = mmg_post(client, data) - + response = sns_post(client, data) assert response.status_code == 200 - assert response.get_data(as_text=True) == 'RECEIVED' + + parsed_response = json.loads(response.get_data(as_text=True)) + assert parsed_response['result'] == 'success' + assert InboundSms.query.count() == 0 assert mocked.called is False -@pytest.mark.parametrize('permissions', [ - [SMS_TYPE], - [INBOUND_SMS_TYPE], -]) -def test_receive_notification_from_firetext_without_permissions_does_not_persist( - client, - mocker, - notify_db_session, - permissions -): - service = create_service_with_inbound_number(inbound_number='07111111111', service_permissions=permissions) - mocker.patch("app.notifications.receive_notifications.dao_fetch_service_by_inbound_number", - return_value=service) - mocked_send_inbound_sms = mocker.patch( - "app.notifications.receive_notifications.tasks.send_inbound_sms_to_service.apply_async") - mocker.patch("app.notifications.receive_notifications.has_inbound_sms_permissions", return_value=False) - - data = "source=07999999999&destination=07111111111&message=this is a message&time=2017-01-01 12:00:00" - response = firetext_post(client, data) - - assert response.status_code == 200 - result = json.loads(response.get_data(as_text=True)) - - assert result['status'] == 'ok' - assert InboundSms.query.count() == 0 - assert not mocked_send_inbound_sms.called - - +@pytest.mark.skip(reason="Need to implement inbound SNS tests. Body here from MMG") def test_receive_notification_without_permissions_does_not_create_inbound_even_with_inbound_number_set( client, mocker, sample_service): inbound_number = create_inbound_number('1', service_id=sample_service.id, active=True) @@ -156,7 +111,7 @@ def test_receive_notification_without_permissions_does_not_create_inbound_even_w "DateRecieved": "2012-06-27 12:33:00" } - response = mmg_post(client, data) + response = sns_post(client, data) assert response.status_code == 200 assert len(InboundSms.query.all()) == 0 @@ -174,17 +129,6 @@ def test_check_permissions_for_inbound_sms(notify_db_session, permissions, expec assert has_inbound_sms_permissions(service.permissions) is expected_response -@pytest.mark.parametrize('message, expected_output', [ - ('abc', 'abc'), - ('', ''), - ('lots+of+words', 'lots of words'), - ('%F0%9F%93%A9+%F0%9F%93%A9+%F0%9F%93%A9', 'πŸ“© πŸ“© πŸ“©'), - ('x+%2B+y', 'x + y') -]) -def test_format_mmg_message(message, expected_output): - assert format_mmg_message(message) == expected_output - - @pytest.mark.parametrize('raw, expected', [ ( '😬', @@ -215,20 +159,8 @@ def test_unescape_string(raw, expected): assert unescape_string(raw) == expected -@pytest.mark.parametrize('provider_date, expected_output', [ - ('2017-01-21+11%3A56%3A11', datetime(2017, 1, 21, 11, 56, 11)), - ('2017-05-21+11%3A56%3A11', datetime(2017, 5, 21, 11, 56, 11)) -]) -def test_format_mmg_datetime(provider_date, expected_output): - assert format_mmg_datetime(provider_date) == expected_output - - -@freeze_time('2020-05-14 14:30:00') -def test_format_mmg_datetime_returns_now_if_cannot_parse_date(): - assert format_mmg_datetime('13-05-2020 08%3A37%3A43') == datetime.utcnow() - - -def test_create_inbound_mmg_sms_object(sample_service_full_permissions): +@pytest.mark.skip(reason="Need to implement inbound SNS tests. Body here from MMG") +def test_create_inbound_sns_sms_object(sample_service_full_permissions): data = { 'Message': 'hello+there+%F0%9F%93%A9', 'Number': sample_service_full_permissions.get_inbound_number(), @@ -237,8 +169,8 @@ def test_create_inbound_mmg_sms_object(sample_service_full_permissions): 'ID': 'bar', } - inbound_sms = create_inbound_sms_object(sample_service_full_permissions, format_mmg_message(data["Message"]), - data["MSISDN"], data["ID"], data["DateRecieved"], "mmg") + inbound_sms = create_inbound_sms_object(sample_service_full_permissions, data["Message"], + data["MSISDN"], data["ID"], data["DateRecieved"], "sns") assert inbound_sms.service_id == sample_service_full_permissions.id assert inbound_sms.notify_number == sample_service_full_permissions.get_inbound_number() @@ -247,10 +179,11 @@ def test_create_inbound_mmg_sms_object(sample_service_full_permissions): assert inbound_sms.provider_reference == 'bar' assert inbound_sms._content != 'hello there πŸ“©' assert inbound_sms.content == 'hello there πŸ“©' - assert inbound_sms.provider == 'mmg' + assert inbound_sms.provider == 'sns' -def test_create_inbound_mmg_sms_object_uses_inbound_number_if_set(sample_service_full_permissions): +@pytest.mark.skip(reason="Need to implement inbound SNS tests. Body here from MMG") +def test_create_inbound_sns_sms_object_uses_inbound_number_if_set(sample_service_full_permissions): sample_service_full_permissions.sms_sender = 'foo' inbound_number = sample_service_full_permissions.get_inbound_number() @@ -264,17 +197,18 @@ def test_create_inbound_mmg_sms_object_uses_inbound_number_if_set(sample_service inbound_sms = create_inbound_sms_object( sample_service_full_permissions, - format_mmg_message(data["Message"]), + data["Message"], data["MSISDN"], data["ID"], data["DateRecieved"], - "mmg" + "sns" ) assert inbound_sms.service_id == sample_service_full_permissions.id assert inbound_sms.notify_number == inbound_number +@pytest.mark.skip(reason="Need to implement inbound SNS tests. Body here from MMG") @pytest.mark.parametrize('notify_number', ['foo', 'baz'], ids=['two_matching_services', 'no_matching_services']) def test_receive_notification_error_if_not_single_matching_service(client, notify_db_session, notify_number): create_service_with_inbound_number( @@ -295,7 +229,7 @@ def test_receive_notification_error_if_not_single_matching_service(client, notif 'DateRecieved': '2017-01-02 03:04:05', 'ID': 'bar', } - response = mmg_post(client, data) + response = sns_post(client, data) # we still return 'RECEIVED' to MMG assert response.status_code == 200 @@ -303,107 +237,7 @@ def test_receive_notification_error_if_not_single_matching_service(client, notif assert InboundSms.query.count() == 0 -def test_receive_notification_returns_received_to_firetext(notify_db_session, client, mocker): - mocked = mocker.patch("app.notifications.receive_notifications.tasks.send_inbound_sms_to_service.apply_async") - prom_counter_labels_mock = mocker.patch('app.notifications.receive_notifications.INBOUND_SMS_COUNTER.labels') - - service = create_service_with_inbound_number( - service_name='b', inbound_number='07111111111', service_permissions=[EMAIL_TYPE, SMS_TYPE, INBOUND_SMS_TYPE]) - - data = "source=07999999999&destination=07111111111&message=this is a message&time=2017-01-01 12:00:00" - - response = firetext_post(client, data) - - assert response.status_code == 200 - result = json.loads(response.get_data(as_text=True)) - - prom_counter_labels_mock.assert_called_once_with("firetext") - prom_counter_labels_mock.return_value.inc.assert_called_once_with() - - assert result['status'] == 'ok' - inbound_sms_id = InboundSms.query.all()[0].id - mocked.assert_called_once_with([str(inbound_sms_id), str(service.id)], queue="notify-internal-tasks") - - -def test_receive_notification_from_firetext_persists_message(notify_db_session, client, mocker): - mocked = mocker.patch("app.notifications.receive_notifications.tasks.send_inbound_sms_to_service.apply_async") - mocker.patch('app.notifications.receive_notifications.INBOUND_SMS_COUNTER') - - service = create_service_with_inbound_number( - inbound_number='07111111111', - service_name='b', - service_permissions=[EMAIL_TYPE, SMS_TYPE, INBOUND_SMS_TYPE]) - - data = "source=07999999999&destination=07111111111&message=this is a message&time=2017-01-01 12:00:00" - - response = firetext_post(client, data) - - assert response.status_code == 200 - result = json.loads(response.get_data(as_text=True)) - - persisted = InboundSms.query.first() - assert result['status'] == 'ok' - assert persisted.notify_number == '07111111111' - assert persisted.user_number == '447999999999' - assert persisted.service == service - assert persisted.content == 'this is a message' - assert persisted.provider == 'firetext' - assert persisted.provider_date == datetime(2017, 1, 1, 12, 0, 0, 0) - mocked.assert_called_once_with([str(persisted.id), str(service.id)], queue="notify-internal-tasks") - - -def test_receive_notification_from_firetext_persists_message_with_normalized_phone(notify_db_session, client, mocker): - mocker.patch("app.notifications.receive_notifications.tasks.send_inbound_sms_to_service.apply_async") - mocker.patch('app.notifications.receive_notifications.INBOUND_SMS_COUNTER') - - create_service_with_inbound_number( - inbound_number='07111111111', service_name='b', service_permissions=[EMAIL_TYPE, SMS_TYPE, INBOUND_SMS_TYPE]) - - data = "source=(+44)7999999999&destination=07111111111&message=this is a message&time=2017-01-01 12:00:00" - - response = firetext_post(client, data) - - assert response.status_code == 200 - result = json.loads(response.get_data(as_text=True)) - - persisted = InboundSms.query.first() - - assert result['status'] == 'ok' - assert persisted.user_number == '447999999999' - - -def test_returns_ok_to_firetext_if_mismatched_sms_sender(notify_db_session, client, mocker): - mocked = mocker.patch("app.notifications.receive_notifications.tasks.send_inbound_sms_to_service.apply_async") - mocker.patch('app.notifications.receive_notifications.INBOUND_SMS_COUNTER') - - create_service_with_inbound_number( - inbound_number='07111111199', service_name='b', service_permissions=[EMAIL_TYPE, SMS_TYPE, INBOUND_SMS_TYPE]) - - data = "source=(+44)7999999999&destination=07111111111&message=this is a message&time=2017-01-01 12:00:00" - - response = firetext_post(client, data) - - assert response.status_code == 200 - result = json.loads(response.get_data(as_text=True)) - - assert not InboundSms.query.all() - assert result['status'] == 'ok' - assert mocked.call_count == 0 - - -@pytest.mark.parametrize( - 'number, expected', - [ - ('447123123123', '07123123123'), - ('447123123144', '07123123144'), - ('07123123123', '07123123123'), - ('447444444444', '07444444444') - ] -) -def test_strip_leading_country_code(number, expected): - assert strip_leading_forty_four(number) == expected - - +@pytest.mark.skip(reason="Need to implement inbound SNS tests. Body here from MMG") @pytest.mark.parametrize("auth, keys, status_code", [ ["testkey", ["testkey"], 200], ["", ["testkey"], 401], @@ -414,31 +248,7 @@ def test_strip_leading_country_code(number, expected): ["", [], 401], ["testkey", [], 403], ]) -def test_firetext_inbound_sms_auth(notify_db_session, notify_api, client, mocker, auth, keys, status_code): - mocker.patch("app.notifications.receive_notifications.tasks.send_inbound_sms_to_service.apply_async") - - create_service_with_inbound_number( - service_name='b', inbound_number='07111111111', service_permissions=[EMAIL_TYPE, SMS_TYPE, INBOUND_SMS_TYPE] - ) - - data = "source=07999999999&destination=07111111111&message=this is a message&time=2017-01-01 12:00:00" - - with set_config(notify_api, 'FIRETEXT_INBOUND_SMS_AUTH', keys): - response = firetext_post(client, data, auth=bool(auth), password=auth) - assert response.status_code == status_code - - -@pytest.mark.parametrize("auth, keys, status_code", [ - ["testkey", ["testkey"], 200], - ["", ["testkey"], 401], - ["wrong", ["testkey"], 403], - ["testkey1", ["testkey1", "testkey2"], 200], - ["testkey2", ["testkey1", "testkey2"], 200], - ["wrong", ["testkey1", "testkey2"], 403], - ["", [], 401], - ["testkey", [], 403], -]) -def test_mmg_inbound_sms_auth(notify_db_session, notify_api, client, mocker, auth, keys, status_code): +def test_sns_inbound_sms_auth(notify_db_session, notify_api, client, mocker, auth, keys, status_code): mocker.patch("app.notifications.receive_notifications.tasks.send_inbound_sms_to_service.apply_async") create_service_with_inbound_number( @@ -456,10 +266,11 @@ def test_mmg_inbound_sms_auth(notify_db_session, notify_api, client, mocker, aut } with set_config(notify_api, 'MMG_INBOUND_SMS_AUTH', keys): - response = mmg_post(client, data, auth=bool(auth), password=auth) + response = sns_post(client, data, auth=bool(auth), password=auth) assert response.status_code == status_code +@pytest.mark.skip(reason="Need to implement inbound SNS tests. Body here from MMG") def test_create_inbound_sms_object_works_with_alphanumeric_sender(sample_service_full_permissions): data = { 'Message': 'hello', @@ -471,7 +282,7 @@ def test_create_inbound_sms_object_works_with_alphanumeric_sender(sample_service inbound_sms = create_inbound_sms_object( service=sample_service_full_permissions, - content=format_mmg_message(data["Message"]), + content=data["Message"], from_number='ALPHANUM3R1C', provider_ref='foo', date_received=None, diff --git a/tests/app/notifications/test_validators.py b/tests/app/notifications/test_validators.py index babada8fc..f354d184e 100644 --- a/tests/app/notifications/test_validators.py +++ b/tests/app/notifications/test_validators.py @@ -167,10 +167,10 @@ def test_service_can_send_to_recipient_passes(key_type, notify_db_session): @pytest.mark.parametrize('user_number, recipient_number', [ - ['0048601234567', '+486 012 34567'], - ['07513332413', '(07513) 332413'], + ['+12028675309', '202-867-5309'], + ['+447513332413', '+44 (07513) 332413'], ]) -def test_service_can_send_to_recipient_passes_with_non_normalised_number(sample_service, user_number, recipient_number): +def test_service_can_send_to_recipient_passes_with_non_normalized_number(sample_service, user_number, recipient_number): sample_service.users[0].mobile_number = user_number serialised_service = SerialisedService.from_id(sample_service.id) @@ -181,7 +181,7 @@ def test_service_can_send_to_recipient_passes_with_non_normalised_number(sample_ @pytest.mark.parametrize('user_email, recipient_email', [ ['test@example.com', 'TeSt@EXAMPLE.com'], ]) -def test_service_can_send_to_recipient_passes_with_non_normalised_email(sample_service, user_email, recipient_email): +def test_service_can_send_to_recipient_passes_with_non_normalized_email(sample_service, user_email, recipient_email): sample_service.users[0].email_address = user_email serialised_service = SerialisedService.from_id(sample_service.id) @@ -206,15 +206,15 @@ def test_service_can_send_to_recipient_passes_for_guest_list_recipient_passes(sa assert service_can_send_to_recipient("some_other_email@test.com", 'team', sample_service) is None - create_service_guest_list(sample_service, mobile_number='07513332413') - assert service_can_send_to_recipient('07513332413', + create_service_guest_list(sample_service, mobile_number='2028675309') + assert service_can_send_to_recipient('2028675309', 'team', sample_service) is None @pytest.mark.parametrize('recipient', [ {"email_address": "some_other_email@test.com"}, - {"mobile_number": "07513332413"}, + {"mobile_number": "2028675300"}, ]) def test_service_can_send_to_recipient_fails_when_ignoring_guest_list( notify_db_session, @@ -234,7 +234,7 @@ def test_service_can_send_to_recipient_fails_when_ignoring_guest_list( assert exec_info.value.fields == [] -@pytest.mark.parametrize('recipient', ['07513332413', 'some_other_email@test.com']) +@pytest.mark.parametrize('recipient', ['2028675300', 'some_other_email@test.com']) @pytest.mark.parametrize('key_type, error_message', [('team', 'Can’t send to this recipient using a team-only API key'), ('normal', @@ -482,7 +482,7 @@ def test_validate_and_format_recipient_fails_when_international_number_and_servi service = create_service(service_permissions=[SMS_TYPE]) service_model = SerialisedService.from_id(service.id) with pytest.raises(BadRequestError) as e: - validate_and_format_recipient('20-12-1234-1234', key_type, service_model, SMS_TYPE) + validate_and_format_recipient('+20-12-1234-1234', key_type, service_model, SMS_TYPE) assert e.value.status_code == 400 assert e.value.message == 'Cannot send to international mobile numbers' assert e.value.fields == [] @@ -492,8 +492,8 @@ def test_validate_and_format_recipient_fails_when_international_number_and_servi def test_validate_and_format_recipient_succeeds_with_international_numbers_if_service_does_allow_int_sms( key_type, sample_service_full_permissions): service_model = SerialisedService.from_id(sample_service_full_permissions.id) - result = validate_and_format_recipient('20-12-1234-1234', key_type, service_model, SMS_TYPE) - assert result == '201212341234' + result = validate_and_format_recipient('+4407513332413', key_type, service_model, SMS_TYPE) + assert result == '+447513332413' def test_validate_and_format_recipient_fails_when_no_recipient(): diff --git a/tests/app/provider_details/test_rest.py b/tests/app/provider_details/test_rest.py index 2b347fbba..4e2c001e9 100644 --- a/tests/app/provider_details/test_rest.py +++ b/tests/app/provider_details/test_rest.py @@ -11,7 +11,7 @@ def test_get_provider_details_returns_all_providers(admin_request, notify_db_ses json_resp = admin_request.get('provider_details.get_providers')['provider_details'] assert len(json_resp) > 0 - assert {'ses', 'firetext', 'mmg', 'dvla'} <= {x['identifier'] for x in json_resp} + assert {'ses', 'sns', 'dvla'} == {x['identifier'] for x in json_resp} def test_get_provider_details_by_id(client, notify_db_session): @@ -32,7 +32,7 @@ def test_get_provider_details_by_id(client, notify_db_session): @freeze_time('2018-06-28 12:00') def test_get_provider_contains_correct_fields(client, sample_template): - create_ft_billing('2018-06-01', sample_template, provider='mmg', billable_unit=1) + create_ft_billing('2018-06-01', sample_template, provider='sns', billable_unit=1) response = client.get( '/provider-details', diff --git a/tests/app/public_contracts/test_POST_notification.py b/tests/app/public_contracts/test_POST_notification.py index 2396359a5..3a4d2dc10 100644 --- a/tests/app/public_contracts/test_POST_notification.py +++ b/tests/app/public_contracts/test_POST_notification.py @@ -24,7 +24,7 @@ def test_post_sms_contract(client, mocker, sample_template): mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') response_json = return_json_from_response(_post_notification( - client, sample_template, url='/notifications/sms', to='07700 900 855' + client, sample_template, url='/notifications/sms', to='202-867-5309' )) validate_v0(response_json, 'POST_notification_return_sms.json') diff --git a/tests/app/service/send_notification/test_send_notification.py b/tests/app/service/send_notification/test_send_notification.py index 94d512f3d..83ee417c9 100644 --- a/tests/app/service/send_notification/test_send_notification.py +++ b/tests/app/service/send_notification/test_send_notification.py @@ -83,7 +83,8 @@ def test_should_reject_bad_phone_numbers(notify_api, sample_template, mocker): mocked.assert_not_called() assert json_resp['result'] == 'error' assert len(json_resp['message'].keys()) == 1 - assert 'Invalid phone number: Must not contain letters or symbols' in json_resp['message']['to'] + assert 'Invalid phone number: The string supplied did not seem to be a phone number.' \ + in json_resp['message']['to'] assert response.status_code == 400 @@ -338,7 +339,7 @@ def test_should_allow_valid_sms_notification(notify_api, sample_template, mocker mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') data = { - 'to': '07700 900 855', + 'to': '202 867 5309', 'template': str(sample_template.id) } @@ -497,7 +498,7 @@ def test_should_not_send_sms_if_team_api_key_and_not_a_service_user(client, samp mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') data = { - 'to': '07123123123', + 'to': '2028675300', 'template': str(sample_template.id), } @@ -550,7 +551,7 @@ def test_should_send_sms_to_anyone_with_test_key( mocker.patch('app.notifications.process_notifications.uuid.uuid4', return_value=fake_uuid) data = { - 'to': '07811111111', + 'to': '2028675300', 'template': sample_template.id } sample_template.service.restricted = restricted @@ -756,9 +757,9 @@ def test_should_not_persist_notification_or_send_email_if_simulated_email( @pytest.mark.parametrize('to_sms', [ - '07700 900000', - '07700 900111', - '07700 900222' + '2028675000', + '2028675111', + '+12028675222' ]) def test_should_not_persist_notification_or_send_sms_if_simulated_number( client, @@ -788,7 +789,7 @@ def test_should_not_persist_notification_or_send_sms_if_simulated_number( KEY_TYPE_NORMAL, KEY_TYPE_TEAM ]) @pytest.mark.parametrize('notification_type, to', [ - (SMS_TYPE, '07827992635'), + (SMS_TYPE, '2028675300'), (EMAIL_TYPE, 'non_guest_list_recipient@mail.com')] ) def test_should_not_send_notification_to_non_guest_list_recipient_in_trial_mode( @@ -841,15 +842,16 @@ def test_should_not_send_notification_to_non_guest_list_recipient_in_trial_mode( @pytest.mark.parametrize('key_type', [ KEY_TYPE_NORMAL, KEY_TYPE_TEAM ]) -@pytest.mark.parametrize('notification_type, to', [ - (SMS_TYPE, '07123123123'), - (EMAIL_TYPE, 'guest_list_recipient@mail.com')] +@pytest.mark.parametrize('notification_type, to, normalized_to', [ + (SMS_TYPE, '2028675300', '+12028675300'), + (EMAIL_TYPE, 'guest_list_recipient@mail.com', None)] ) def test_should_send_notification_to_guest_list_recipient( client, sample_service, notification_type, to, + normalized_to, key_type, service_restricted, mocker @@ -865,7 +867,7 @@ def test_should_send_notification_to_guest_list_recipient( service_guest_list = create_service_guest_list(sample_service, email_address=to) assert service_guest_list.service_id == sample_service.id - assert to in [member.recipient for member in sample_service.guest_list] + assert (normalized_to or to) in [member.recipient for member in sample_service.guest_list] create_notification(template=template) @@ -893,7 +895,7 @@ def test_should_send_notification_to_guest_list_recipient( @pytest.mark.parametrize( 'notification_type, template_type, to', [ (EMAIL_TYPE, SMS_TYPE, 'notify@digital.cabinet-office.gov.uk'), - (SMS_TYPE, EMAIL_TYPE, '+447700900986') + (SMS_TYPE, EMAIL_TYPE, '+12028675309') ]) def test_should_error_if_notification_type_does_not_match_template_type( client, @@ -966,7 +968,7 @@ def test_create_template_raises_invalid_request_when_content_too_large( @pytest.mark.parametrize("notification_type, send_to", - [("sms", "07700 900 855"), + [("sms", "2028675309"), ("email", "sample@email.com")]) def test_send_notification_uses_priority_queue_when_template_is_marked_as_priority( client, @@ -999,7 +1001,7 @@ def test_send_notification_uses_priority_queue_when_template_is_marked_as_priori @pytest.mark.parametrize( "notification_type, send_to", - [("sms", "07700 900 855"), ("email", "sample@email.com")] + [("sms", "2028675309"), ("email", "sample@email.com")] ) def test_returns_a_429_limit_exceeded_if_rate_limit_exceeded( client, @@ -1042,7 +1044,7 @@ def test_should_allow_store_original_number_on_sms_notification(client, sample_t mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') data = { - 'to': '+(44) 7700-900 855', + 'to': '(202) 867-5309', 'template': str(sample_template.id) } @@ -1061,7 +1063,7 @@ def test_should_allow_store_original_number_on_sms_notification(client, sample_t assert notification_id notifications = Notification.query.all() assert len(notifications) == 1 - assert '+(44) 7700-900 855' == notifications[0].to + assert '(202) 867-5309' == notifications[0].to def test_should_not_allow_sending_to_international_number_without_international_permission( @@ -1070,7 +1072,7 @@ def test_should_not_allow_sending_to_international_number_without_international_ mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') data = { - 'to': '20-12-1234-1234', + 'to': '+(44) 7700-900 855', 'template': str(sample_template.id) } @@ -1088,29 +1090,6 @@ def test_should_not_allow_sending_to_international_number_without_international_ assert error_json['message'] == 'Cannot send to international mobile numbers' -def test_should_allow_sending_to_crown_dependency_number_without_international_permission( - client, mocker, notify_db_session -): - service = create_service() - - mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') - template = create_template(service) - - data = { - 'to': '07700-900-123', - 'template': str(template.id) - } - - auth_header = create_service_authorization_header(service_id=service.id) - - response = client.post( - path='/notifications/sms', - data=json.dumps(data), - headers=[('Content-Type', 'application/json'), auth_header]) - - assert response.status_code == 201 - - def test_should_allow_sending_to_international_number_with_international_permission( client, sample_service_full_permissions, mocker ): @@ -1118,7 +1097,7 @@ def test_should_allow_sending_to_international_number_with_international_permiss template = create_template(sample_service_full_permissions) data = { - 'to': '20-12-1234-1234', + 'to': '+(44) 7700-900 855', 'template': str(template.id) } @@ -1140,7 +1119,7 @@ def test_should_not_allow_sms_notifications_if_service_permission_not_set( mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') data = { - 'to': '+447700900986', + 'to': '+12028675309', 'template': str(sample_template_without_sms_permission.id) } @@ -1201,7 +1180,7 @@ def test_should_throw_exception_if_notification_type_is_invalid(client, sample_s @pytest.mark.parametrize("notification_type, recipient", - [("sms", '07700 900 855'), + [("sms", '2028675309'), ("email", "test@gov.uk") ] ) diff --git a/tests/app/service/send_notification/test_send_one_off_notification.py b/tests/app/service/send_notification/test_send_one_off_notification.py index 5443f42fb..d0065d0be 100644 --- a/tests/app/service/send_notification/test_send_one_off_notification.py +++ b/tests/app/service/send_notification/test_send_one_off_notification.py @@ -50,7 +50,7 @@ def test_send_one_off_notification_calls_celery_correctly(persist_mock, celery_m post_data = { 'template_id': str(template.id), - 'to': '07700 900 001', + 'to': '202-867-5309', 'created_by': str(service.created_by_id) } @@ -81,7 +81,7 @@ def test_send_one_off_notification_calls_persist_correctly_for_sms( post_data = { 'template_id': str(template.id), - 'to': '07700 900 001', + 'to': '202-867-5309', 'personalisation': {'name': 'foo'}, 'created_by': str(service.created_by_id) } @@ -118,14 +118,14 @@ def test_send_one_off_notification_calls_persist_correctly_for_international_sms post_data = { 'template_id': str(template.id), - 'to': '+1 555 0100', + 'to': '+(44) 7700-900 855', 'personalisation': {'name': 'foo'}, 'created_by': str(service.created_by_id) } send_one_off_notification(service.id, post_data) - assert persist_mock.call_args[1]['recipient'] == '+1 555 0100' + assert persist_mock.call_args[1]['recipient'] == '+(44) 7700-900 855' def test_send_one_off_notification_calls_persist_correctly_for_email( @@ -223,7 +223,7 @@ def test_send_one_off_notification_honors_research_mode(notify_db_session, persi post_data = { 'template_id': str(template.id), - 'to': '07700 900 001', + 'to': '202-867-5309', 'created_by': str(service.created_by_id) } @@ -239,7 +239,7 @@ def test_send_one_off_notification_honors_priority(notify_db_session, persist_mo post_data = { 'template_id': str(template.id), - 'to': '07700 900 001', + 'to': '202-867-5309', 'created_by': str(service.created_by_id) } @@ -263,9 +263,9 @@ def test_send_one_off_notification_raises_if_invalid_recipient(notify_db_session @pytest.mark.parametrize('recipient', [ - '07700 900 001', # not in team or guest_list - '07700900123', # in guest_list - '+447700-900-123', # in guest_list in different format + '2028675300', # not in team or guest_list + '2028765309', # in guest_list + '+1-202-876-5309', # in guest_list in different format ]) def test_send_one_off_notification_raises_if_cant_send_to_recipient( notify_db_session, @@ -274,7 +274,7 @@ def test_send_one_off_notification_raises_if_cant_send_to_recipient( service = create_service(restricted=True) template = create_template(service=service) dao_add_and_commit_guest_list_contacts([ - ServiceGuestList.from_string(service.id, MOBILE_TYPE, '07700900123'), + ServiceGuestList.from_string(service.id, MOBILE_TYPE, '2028765309'), ]) post_data = { @@ -331,7 +331,7 @@ def test_send_one_off_notification_fails_if_created_by_other_service(sample_temp post_data = { 'template_id': str(sample_template.id), - 'to': '07700 900 001', + 'to': '202-867-5309', 'created_by': str(user_not_in_service.id) } @@ -414,12 +414,12 @@ def test_send_one_off_sms_notification_should_use_sms_sender_reply_to_text(sampl template = create_template(service=sample_service, template_type=SMS_TYPE) sms_sender = create_service_sms_sender( service=sample_service, - sms_sender='07123123123', + sms_sender='2028675309', is_default=False ) data = { - 'to': '07111111111', + 'to': '2028675000', 'template_id': str(template.id), 'created_by': str(sample_service.created_by_id), 'sender_id': str(sms_sender.id), @@ -433,7 +433,7 @@ def test_send_one_off_sms_notification_should_use_sms_sender_reply_to_text(sampl queue=None ) - assert notification.reply_to_text == "447123123123" + assert notification.reply_to_text == "+12028675309" def test_send_one_off_sms_notification_should_use_default_service_reply_to_text(sample_service, celery_mock): @@ -441,12 +441,12 @@ def test_send_one_off_sms_notification_should_use_default_service_reply_to_text( sample_service.service_sms_senders[0].is_default = False create_service_sms_sender( service=sample_service, - sms_sender='07123123456', + sms_sender='2028675309', is_default=True ) data = { - 'to': '07111111111', + 'to': '2028675000', 'template_id': str(template.id), 'created_by': str(sample_service.created_by_id), } @@ -459,7 +459,7 @@ def test_send_one_off_sms_notification_should_use_default_service_reply_to_text( queue=None ) - assert notification.reply_to_text == "447123123456" + assert notification.reply_to_text == "+12028675309" def test_send_one_off_notification_should_throw_exception_if_reply_to_id_doesnot_exist( @@ -481,7 +481,7 @@ def test_send_one_off_notification_should_throw_exception_if_sms_sender_id_doesn sample_template ): data = { - 'to': '07700 900 001', + 'to': '2028675000', 'template_id': str(sample_template.id), 'sender_id': str(uuid.uuid4()), 'created_by': str(sample_template.service.created_by_id) diff --git a/tests/app/service/test_rest.py b/tests/app/service/test_rest.py index 22c2709e7..6dd4b2ba9 100644 --- a/tests/app/service/test_rest.py +++ b/tests/app/service/test_rest.py @@ -204,7 +204,7 @@ def test_get_live_services_data(sample_user, admin_request): { 'consent_to_research': None, 'contact_email': 'notify@digital.cabinet-office.gov.uk', - 'contact_mobile': '+447700900986', + 'contact_mobile': '+12028675309', 'contact_name': 'Test User', 'email_totals': 1, 'email_volume_intent': None, @@ -222,7 +222,7 @@ def test_get_live_services_data(sample_user, admin_request): { 'consent_to_research': None, 'contact_email': 'notify@digital.cabinet-office.gov.uk', - 'contact_mobile': '+447700900986', + 'contact_mobile': '+12028675309', 'contact_name': 'Test User', 'email_totals': 0, 'email_volume_intent': None, @@ -2474,7 +2474,7 @@ def test_send_one_off_notification(sample_service, admin_request, mocker): service_id=sample_service.id, _data={ 'template_id': str(template.id), - 'to': '07700900001', + 'to': '2028675309', 'created_by': str(sample_service.created_by_id) }, _expected_status=201 diff --git a/tests/app/service/test_service_guest_list.py b/tests/app/service/test_service_guest_list.py index 344777d26..a7946b6ba 100644 --- a/tests/app/service/test_service_guest_list.py +++ b/tests/app/service/test_service_guest_list.py @@ -22,8 +22,8 @@ def test_get_guest_list_returns_data(client, sample_service_guest_list): def test_get_guest_list_separates_emails_and_phones(client, sample_service): dao_add_and_commit_guest_list_contacts([ ServiceGuestList.from_string(sample_service.id, EMAIL_TYPE, 'service@example.com'), - ServiceGuestList.from_string(sample_service.id, MOBILE_TYPE, '07123456789'), - ServiceGuestList.from_string(sample_service.id, MOBILE_TYPE, '+1800-555-555'), + ServiceGuestList.from_string(sample_service.id, MOBILE_TYPE, '2028675309'), + ServiceGuestList.from_string(sample_service.id, MOBILE_TYPE, '+1800-555-5555'), ]) response = client.get( @@ -32,7 +32,7 @@ def test_get_guest_list_separates_emails_and_phones(client, sample_service): assert response.status_code == 200 json_resp = json.loads(response.get_data(as_text=True)) assert json_resp['email_addresses'] == ['service@example.com'] - assert sorted(json_resp['phone_numbers']) == sorted(['+1800-555-555', '07123456789']) + assert sorted(json_resp['phone_numbers']) == sorted(['+18005555555', '+12028675309']) def test_get_guest_list_404s_with_unknown_service_id(client): @@ -57,7 +57,7 @@ def test_get_guest_list_returns_no_data(client, sample_service): def test_update_guest_list_replaces_old_guest_list(client, sample_service_guest_list): data = { 'email_addresses': ['foo@bar.com'], - 'phone_numbers': ['07123456789'] + 'phone_numbers': ['2028765309'] } response = client.put( @@ -69,7 +69,7 @@ def test_update_guest_list_replaces_old_guest_list(client, sample_service_guest_ assert response.status_code == 204 guest_list = ServiceGuestList.query.order_by(ServiceGuestList.recipient).all() assert len(guest_list) == 2 - assert guest_list[0].recipient == '07123456789' + assert guest_list[0].recipient == '+12028765309' assert guest_list[1].recipient == 'foo@bar.com' @@ -77,7 +77,7 @@ def test_update_guest_list_doesnt_remove_old_guest_list_if_error(client, sample_ data = { 'email_addresses': [''], - 'phone_numbers': ['07123456789'] + 'phone_numbers': ['2028675309'] } response = client.put( diff --git a/tests/app/test_commands.py b/tests/app/test_commands.py index b2b6abd14..fc6c85695 100644 --- a/tests/app/test_commands.py +++ b/tests/app/test_commands.py @@ -1,12 +1,13 @@ import pytest from app.commands import ( + _update_template, create_test_user, insert_inbound_numbers_from_file, populate_annual_billing_with_defaults, ) from app.dao.inbound_numbers_dao import dao_get_available_inbound_numbers -from app.models import AnnualBilling, User +from app.models import AnnualBilling, Template, User from tests.app.db import create_annual_billing, create_service @@ -19,7 +20,7 @@ def test_create_test_user_command(notify_db_session, notify_api): notify_api.test_cli_runner().invoke( create_test_user, [ '--email', 'somebody@fake.gov', - '--mobile_number', '555-555-5555', + '--mobile_number', '202-555-5555', '--password', 'correct horse battery staple', '--name', 'Fake Personson', # '--auth_type', 'sms_auth', # this is the default @@ -88,3 +89,20 @@ def test_populate_annual_billing_with_defaults_sets_free_allowance_to_zero_if_pr assert len(results) == 1 assert results[0].free_sms_fragment_limit == 0 + + +def test_update_template( + notify_db_session, email_2fa_code_template +): + + _update_template( + "299726d2-dba6-42b8-8209-30e1d66ea164", + "Example text message template!", + "sms", + ["Hi, I’m trying out U.S. Notify! Today is ((day of week)) and my favorite color is ((color))."], + "" + ) + + t = Template.query.all() + + assert t[0].name == "Example text message template!" diff --git a/tests/app/test_model.py b/tests/app/test_model.py index f8464a16e..8e42900d0 100644 --- a/tests/app/test_model.py +++ b/tests/app/test_model.py @@ -32,8 +32,8 @@ from tests.app.db import ( @pytest.mark.parametrize('mobile_number', [ - '07700 900678', - '+44 7700 900678' + '+447700900855', + '+12348675309' ]) def test_should_build_service_guest_list_from_mobile_number(mobile_number): service_guest_list = ServiceGuestList.from_string('service_id', MOBILE_TYPE, mobile_number) @@ -93,7 +93,7 @@ def test_status_conversion(initial_statuses, expected_statuses): @freeze_time("2016-01-01 11:09:00.000000") @pytest.mark.parametrize('template_type, recipient', [ - ('sms', '+447700900855'), + ('sms', '+12028675309'), ('email', 'foo@bar.com'), ]) def test_notification_for_csv_returns_correct_type(sample_service, template_type, recipient): @@ -154,21 +154,28 @@ def test_notification_personalisation_getter_returns_empty_dict_from_None(): assert noti.personalisation == {} -def test_notification_personalisation_getter_always_returns_empty_dict(): +def test_notification_personalisation_getter_always_returns_empty_dict(notify_app): noti = Notification() noti._personalisation = encryption.encrypt({}) assert noti.personalisation == {} +def test_notification_personalisation_getter_returns_empty_dict_for_encryption_errors(notify_app): + noti = Notification() + # old _personalisation values were created with encryption.sign, which will trigger a decryption error + noti._personalisation = encryption.sign({"value": "PII"}) + assert noti.personalisation == {} + + @pytest.mark.parametrize('input_value', [ None, {} ]) -def test_notification_personalisation_setter_always_sets_empty_dict(input_value): +def test_notification_personalisation_setter_always_sets_empty_dict(notify_app, input_value): noti = Notification() noti.personalisation = input_value - assert noti._personalisation == encryption.encrypt({}) + assert noti.personalisation == {} def test_notification_subject_is_none_for_sms(sample_service): diff --git a/tests/app/user/test_rest.py b/tests/app/user/test_rest.py index 1031685cf..f06a26275 100644 --- a/tests/app/user/test_rest.py +++ b/tests/app/user/test_rest.py @@ -107,7 +107,7 @@ def test_post_user(admin_request, notify_db_session): "name": "Test User", "email_address": "user@digital.cabinet-office.gov.uk", "password": "password", - "mobile_number": "+447700900986", + "mobile_number": "+12028675309", "logged_in_at": None, "state": "active", "failed_login_count": 0, @@ -130,7 +130,7 @@ def test_post_user_without_auth_type(admin_request, notify_db_session): "name": "Test User", "email_address": "user@digital.cabinet-office.gov.uk", "password": "password", - "mobile_number": "+447700900986", + "mobile_number": "+12028675309", "permissions": {}, } @@ -149,7 +149,7 @@ def test_post_user_missing_attribute_email(admin_request, notify_db_session): data = { "name": "Test User", "password": "password", - "mobile_number": "+447700900986", + "mobile_number": "+12028675309", "logged_in_at": None, "state": "active", "failed_login_count": 0, @@ -169,7 +169,7 @@ def test_create_user_missing_attribute_password(admin_request, notify_db_session data = { "name": "Test User", "email_address": "user@digital.cabinet-office.gov.uk", - "mobile_number": "+447700900986", + "mobile_number": "+12028675309", "logged_in_at": None, "state": "active", "failed_login_count": 0, @@ -224,7 +224,7 @@ def test_cannot_create_user_with_empty_strings(admin_request, notify_db_session) ) assert resp['message'] == { 'email_address': ['Not a valid email address'], - 'mobile_number': ['Invalid phone number: Not enough digits'], + 'mobile_number': ['Invalid phone number: The string supplied did not seem to be a phone number.'], 'name': ['Invalid name'] } @@ -883,7 +883,8 @@ def test_cannot_update_user_with_mobile_number_as_empty_string(admin_request, sa _data={'mobile_number': ''}, _expected_status=400 ) - assert resp['message']['mobile_number'] == ['Invalid phone number: Not enough digits'] + assert resp['message']['mobile_number'] == [ + 'Invalid phone number: The string supplied did not seem to be a phone number.'] def test_cannot_update_user_password_using_attributes_method(admin_request, sample_user): @@ -1100,10 +1101,9 @@ def test_search_for_users_by_email_handles_incorrect_data_format(notify_db_sessi @pytest.mark.parametrize('number, expected_reply_to', [ - ("1-403-123-4567", "notify_international_sender"), - ("27 123 4569 2312", "notify_international_sender"), - ("30 123 4567 7890", "Notify"), - ("+20 123 4567 7890", "Notify"), + ("403-123-4567", "Notify"), + ("+30 123 4567 7890", "Notify"), + ("+27 123 4569 2312", "notify_international_sender"), ]) def test_get_sms_reply_to_for_notify_service(team_member_mobile_edit_template, number, expected_reply_to): # need to import locally to avoid db session errors, diff --git a/tests/app/user/test_rest_verify.py b/tests/app/user/test_rest_verify.py index 8bf0cd9e4..65217f0b2 100644 --- a/tests/app/user/test_rest_verify.py +++ b/tests/app/user/test_rest_verify.py @@ -522,7 +522,7 @@ def test_user_verify_email_code_fails_if_code_already_used(admin_request, sample def test_send_user_2fa_code_sends_from_number_for_international_numbers( client, sample_user, mocker, sms_code_template ): - sample_user.mobile_number = "601117224412" + sample_user.mobile_number = "+601117224412" auth_header = create_admin_authorization_header() mocker.patch('app.user.rest.create_secret_code', return_value='11111') mocker.patch('app.user.rest.send_notification_to_queue') diff --git a/tests/app/v2/notifications/test_get_notifications.py b/tests/app/v2/notifications/test_get_notifications.py index 7c93f9aad..2129b558a 100644 --- a/tests/app/v2/notifications/test_get_notifications.py +++ b/tests/app/v2/notifications/test_get_notifications.py @@ -9,8 +9,8 @@ from tests.app.db import create_notification, create_template @pytest.mark.parametrize('billable_units, provider', [ - (1, 'mmg'), - (0, 'mmg'), + (1, 'sns'), + (0, 'sns'), (1, None) ]) def test_get_notification_by_id_returns_200( diff --git a/tests/app/v2/notifications/test_notification_schemas.py b/tests/app/v2/notifications/test_notification_schemas.py index c41e9c12b..33a5b2030 100644 --- a/tests/app/v2/notifications/test_notification_schemas.py +++ b/tests/app/v2/notifications/test_notification_schemas.py @@ -102,11 +102,11 @@ def test_get_notifications_request_invalid_statuses_and_template_types(): .format(invalid_template_type) in error_messages -valid_json = {"phone_number": "07515111111", +valid_json = {"phone_number": "2028675309", "template_id": str(uuid.uuid4()) } valid_json_with_optionals = { - "phone_number": "07515111111", + "phone_number": "2028675309", "template_id": str(uuid.uuid4()), "reference": "reference from caller", "personalisation": {"key": "value"} @@ -130,7 +130,7 @@ def test_post_sms_schema_is_valid(input): def test_post_sms_json_schema_bad_uuid(template_id): j = { "template_id": template_id, - "phone_number": "07515111111" + "phone_number": "2028675309" } with pytest.raises(ValidationError) as e: validate(j, post_sms_request_schema) @@ -158,7 +158,7 @@ def test_post_sms_json_schema_bad_uuid_and_missing_phone_number(): def test_post_sms_schema_with_personalisation_that_is_not_a_dict(): j = { - "phone_number": "07515111111", + "phone_number": "2028675309", "template_id": str(uuid.uuid4()), "reference": "reference from caller", "personalisation": "not_a_dict" @@ -174,9 +174,9 @@ def test_post_sms_schema_with_personalisation_that_is_not_a_dict(): @pytest.mark.parametrize('invalid_phone_number, err_msg', [ - ('08515111111', 'phone_number Not a UK mobile number'), - ('07515111*11', 'phone_number Must not contain letters or symbols'), - ('notaphoneumber', 'phone_number Must not contain letters or symbols'), + ('08515111111', 'phone_number Phone number is not possible'), + ('07515111*11', 'phone_number Not enough digits'), + ('notaphoneumber', 'phone_number The string supplied did not seem to be a phone number.'), (7700900001, 'phone_number 7700900001 is not of type string'), (None, 'phone_number None is not of type string'), ([], 'phone_number [] is not of type string'), @@ -194,13 +194,13 @@ def test_post_sms_request_schema_invalid_phone_number(invalid_phone_number, err_ def test_post_sms_request_schema_invalid_phone_number_and_missing_template(): - j = {"phone_number": '08515111111', + j = {"phone_number": '5558675309', } with pytest.raises(ValidationError) as e: validate(j, post_sms_request_schema) errors = json.loads(str(e.value)).get('errors') assert len(errors) == 2 - assert {"error": "ValidationError", "message": "phone_number Not a UK mobile number"} in errors + assert {"error": "ValidationError", "message": "phone_number Phone number range is not in use"} in errors assert {"error": "ValidationError", "message": "template_id is a required property"} in errors @@ -269,7 +269,7 @@ def test_post_schema_valid_scheduled_for(schema): if schema == post_email_request_schema: j.update({"email_address": "joe@gmail.com"}) else: - j.update({"phone_number": "07515111111"}) + j.update({"phone_number": "2028675309"}) assert validate(j, schema) == j @@ -286,7 +286,7 @@ def test_post_email_schema_invalid_scheduled_for(invalid_datetime, schema): if schema == post_email_request_schema: j.update({"email_address": "joe@gmail.com"}) else: - j.update({"phone_number": "07515111111"}) + j.update({"phone_number": "2028675309"}) with pytest.raises(ValidationError) as e: validate(j, schema) error = json.loads(str(e.value)) @@ -299,7 +299,7 @@ def test_post_email_schema_invalid_scheduled_for(invalid_datetime, schema): @freeze_time("2017-05-12 13:00:00") def test_scheduled_for_raises_validation_error_when_in_the_past(): - j = {"phone_number": "07515111111", + j = {"phone_number": "2028675309", "template_id": str(uuid.uuid4()), "scheduled_for": "2017-05-12 10:00"} with pytest.raises(ValidationError) as e: @@ -312,7 +312,7 @@ def test_scheduled_for_raises_validation_error_when_in_the_past(): @freeze_time("2017-05-12 13:00:00") def test_scheduled_for_raises_validation_error_when_more_than_24_hours_in_the_future(): - j = {"phone_number": "07515111111", + j = {"phone_number": "2028675309", "template_id": str(uuid.uuid4()), "scheduled_for": "2017-05-13 14:00"} with pytest.raises(ValidationError) as e: diff --git a/tests/app/v2/notifications/test_post_notifications.py b/tests/app/v2/notifications/test_post_notifications.py index 3fd8a4c00..83c8fc13e 100644 --- a/tests/app/v2/notifications/test_post_notifications.py +++ b/tests/app/v2/notifications/test_post_notifications.py @@ -102,7 +102,7 @@ def test_post_sms_notification_uses_inbound_number_as_sender(client, notify_db_s def test_post_sms_notification_uses_inbound_number_reply_to_as_sender(client, notify_db_session, mocker): - service = create_service_with_inbound_number(inbound_number='07123123123') + service = create_service_with_inbound_number(inbound_number='2028675309') template = create_template(service=service, content="Hello (( Name))\nYour thing is due soon") mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') @@ -124,8 +124,8 @@ def test_post_sms_notification_uses_inbound_number_reply_to_as_sender(client, no assert len(notifications) == 1 notification_id = notifications[0].id assert resp_json['id'] == str(notification_id) - assert resp_json['content']['from_number'] == '447123123123' - assert notifications[0].reply_to_text == '447123123123' + assert resp_json['content']['from_number'] == '+12028675309' + assert notifications[0].reply_to_text == '+12028675309' mocked.assert_called_once_with([str(notification_id)], queue='send-sms-tasks') @@ -159,7 +159,7 @@ def test_post_sms_notification_returns_201_with_sms_sender_id( def test_post_sms_notification_uses_sms_sender_id_reply_to( client, sample_template_with_placeholders, mocker ): - sms_sender = create_service_sms_sender(service=sample_template_with_placeholders.service, sms_sender='07123123123') + sms_sender = create_service_sms_sender(service=sample_template_with_placeholders.service, sms_sender='2028675309') mocked = mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') data = { 'phone_number': '+447700900855', @@ -176,10 +176,10 @@ def test_post_sms_notification_uses_sms_sender_id_reply_to( assert response.status_code == 201 resp_json = json.loads(response.get_data(as_text=True)) assert validate(resp_json, post_sms_response) == resp_json - assert resp_json['content']['from_number'] == '447123123123' + assert resp_json['content']['from_number'] == '+12028675309' notifications = Notification.query.all() assert len(notifications) == 1 - assert notifications[0].reply_to_text == '447123123123' + assert notifications[0].reply_to_text == '+12028675309' mocked.assert_called_once_with([resp_json['id']], queue='send-sms-tasks') @@ -189,7 +189,7 @@ def test_notification_reply_to_text_is_original_value_if_sender_is_changed_after sms_sender = create_service_sms_sender(service=sample_template.service, sms_sender='123456', is_default=False) mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') data = { - 'phone_number': '+447700900855', + 'phone_number': '+12028675309', 'template_id': str(sample_template.id), 'sms_sender_id': str(sms_sender.id) } @@ -220,7 +220,7 @@ def test_should_cache_template_lookups_in_memory(mocker, client, sample_template mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') data = { - 'phone_number': '+447700900855', + 'phone_number': '2028675309', 'template_id': str(sample_template.id), } @@ -455,9 +455,9 @@ def test_post_email_notification_returns_201(client, sample_email_template_with_ ('simulate-delivered@notifications.service.gov.uk', EMAIL_TYPE), ('simulate-delivered-2@notifications.service.gov.uk', EMAIL_TYPE), ('simulate-delivered-3@notifications.service.gov.uk', EMAIL_TYPE), - ('07700 900000', 'sms'), - ('07700 900111', 'sms'), - ('07700 900222', 'sms') + ('2028675000', 'sms'), + ('2028675111', 'sms'), + ('2028675222', 'sms') ]) def test_should_not_persist_or_send_notification_if_simulated_recipient( client, @@ -493,7 +493,7 @@ def test_should_not_persist_or_send_notification_if_simulated_recipient( @pytest.mark.parametrize("notification_type, key_send_to, send_to", - [("sms", "phone_number", "07700 900 855"), + [("sms", "phone_number", "2028675309"), ("email", "email_address", "sample@email.com")]) def test_send_notification_uses_priority_queue_when_template_is_marked_as_priority( client, @@ -532,7 +532,7 @@ def test_send_notification_uses_priority_queue_when_template_is_marked_as_priori @pytest.mark.parametrize( "notification_type, key_send_to, send_to", - [("sms", "phone_number", "07700 900 855"), ("email", "email_address", "sample@email.com")] + [("sms", "phone_number", "2028675309"), ("email", "email_address", "sample@email.com")] ) def test_returns_a_429_limit_exceeded_if_rate_limit_exceeded( client, @@ -581,7 +581,7 @@ def test_post_sms_notification_returns_400_if_not_allowed_to_send_int_sms( template = create_template(service=service) data = { - 'phone_number': '20-12-1234-1234', + 'phone_number': '+20-12-1234-1234', 'template_id': template.id } auth_header = create_service_authorization_header(service_id=service.id) @@ -627,7 +627,7 @@ def test_post_sms_notification_with_archived_reply_to_id_returns_400(client, sam @pytest.mark.parametrize('recipient,label,permission_type, notification_type,expected_error', [ - ('07700 900000', 'phone_number', 'email', 'sms', 'text messages'), + ('2028675309', 'phone_number', 'email', 'sms', 'text messages'), ('someone@test.com', 'email_address', 'sms', 'email', 'emails')]) def test_post_sms_notification_returns_400_if_not_allowed_to_send_notification( notify_db_session, client, recipient, label, permission_type, notification_type, expected_error @@ -691,7 +691,7 @@ def test_post_sms_notification_returns_201_if_allowed_to_send_int_sms( mocker.patch('app.celery.provider_tasks.deliver_sms.apply_async') data = { - 'phone_number': '20-12-1234-1234', + 'phone_number': '+20-12-1234-1234', 'template_id': sample_template.id } auth_header = create_service_authorization_header(service_id=sample_service.id) diff --git a/tests/conftest.py b/tests/conftest.py index dabd3eb33..6d65314e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,16 +12,20 @@ from app.dao.provider_details_dao import get_provider_details_by_identifier @pytest.fixture(scope='session') -def notify_api(): +def notify_app(): app = Flask('test') create_app(app) + return app + +@pytest.fixture(scope='session') +def notify_api(notify_app): # deattach server-error error handlers - error_handler_spec looks like: # {'blueprint_name': { # status_code: [error_handlers], # None: { ExceptionClass: error_handler } # }} - for error_handlers in app.error_handler_spec.values(): + for error_handlers in notify_app.error_handler_spec.values(): error_handlers.pop(500, None) if None in error_handlers: error_handlers[None] = { @@ -32,10 +36,10 @@ def notify_api(): if error_handlers[None] == []: error_handlers.pop(None) - ctx = app.app_context() + ctx = notify_app.app_context() ctx.push() - yield app + yield notify_app ctx.pop() @@ -98,11 +102,10 @@ def _notify_db(notify_api, worker_id): def sms_providers(_notify_db): """ In production we randomly choose which provider to use based on their priority. To guarantee tests run the same each - time, make sure we always choose mmg. You'll need to override them in your tests if you wish to do something + time, make sure we always choose sns. You'll need to override them in your tests if you wish to do something different. """ - get_provider_details_by_identifier('mmg').priority = 100 - get_provider_details_by_identifier('firetext').priority = 0 + get_provider_details_by_identifier('sns').priority = 100 @pytest.fixture(scope='function')