mirror of
https://github.com/GSA/notifications-api.git
synced 2026-09-11 10:28:55 -04:00
Merge branch 'main' into notify-api-446
This commit is contained in:
@@ -13,6 +13,6 @@ runs:
|
|||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
python-version: "3.9"
|
||||||
- name: Install pipenv
|
- name: Install poetry
|
||||||
shell: bash
|
shell: bash
|
||||||
run: pip install --upgrade pipenv
|
run: pip install --upgrade poetry
|
||||||
|
|||||||
@@ -43,17 +43,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
|
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
|
||||||
- name: Run style checks
|
- name: Run style checks
|
||||||
run: pipenv run flake8 .
|
run: poetry run flake8 .
|
||||||
- name: Check imports alphabetized
|
- name: Check imports alphabetized
|
||||||
run: pipenv run isort --check-only ./app ./tests
|
run: poetry run isort --check-only ./app ./tests
|
||||||
- name: Check for dead code
|
- name: Check for dead code
|
||||||
run: make dead-code
|
run: make dead-code
|
||||||
- name: Run tests with coverage
|
- name: Run tests with coverage
|
||||||
run: pipenv run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10
|
run: poetry run coverage run --omit=*/notifications_utils/* -m pytest --maxfail=10
|
||||||
env:
|
env:
|
||||||
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
|
SQLALCHEMY_DATABASE_TEST_URI: postgresql://user:password@localhost:5432/test_notification_api
|
||||||
- name: Check coverage threshold
|
- name: Check coverage threshold
|
||||||
run: pipenv run coverage report --fail-under=50
|
run: poetry run coverage report --fail-under=50
|
||||||
|
|
||||||
validate-new-relic-config:
|
validate-new-relic-config:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -61,14 +61,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/setup-project
|
- uses: ./.github/actions/setup-project
|
||||||
- name: Install pipenv packages
|
- name: Install poetry packages
|
||||||
run: pipenv install --dev
|
run: poetry install
|
||||||
- name: Validate NewRelic config
|
- name: Validate NewRelic config
|
||||||
env:
|
env:
|
||||||
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
NEW_RELIC_LICENSE_KEY: ${{ secrets.NEW_RELIC_LICENSE_KEY }}
|
||||||
# Need to set a NEW_RELIC_ENVIRONMENT with monitor_mode: true
|
# Need to set a NEW_RELIC_ENVIRONMENT with monitor_mode: true
|
||||||
NEW_RELIC_ENVIRONMENT: staging
|
NEW_RELIC_ENVIRONMENT: staging
|
||||||
run: pipenv run newrelic-admin validate-config $NEW_RELIC_CONFIG_FILE
|
run: poetry run newrelic-admin validate-config $NEW_RELIC_CONFIG_FILE
|
||||||
|
|
||||||
pip-audit:
|
pip-audit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/setup-project
|
- uses: ./.github/actions/setup-project
|
||||||
- name: Create requirements.txt
|
- name: Create requirements.txt
|
||||||
run: pipenv requirements > requirements.txt
|
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||||
- uses: pypa/gh-action-pip-audit@v1.0.6
|
- uses: pypa/gh-action-pip-audit@v1.0.6
|
||||||
with:
|
with:
|
||||||
inputs: requirements.txt
|
inputs: requirements.txt
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/setup-project
|
- uses: ./.github/actions/setup-project
|
||||||
- name: Create requirements.txt
|
- name: Create requirements.txt
|
||||||
run: pipenv requirements > requirements.txt
|
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||||
- uses: pypa/gh-action-pip-audit@v1.0.6
|
- uses: pypa/gh-action-pip-audit@v1.0.6
|
||||||
with:
|
with:
|
||||||
inputs: requirements.txt
|
inputs: requirements.txt
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ jobs:
|
|||||||
- name: Install application dependencies
|
- name: Install application dependencies
|
||||||
run: make bootstrap
|
run: make bootstrap
|
||||||
|
|
||||||
- name: Create requirements.txt because Cloud Foundry does a weird pipenv thing
|
- name: Create requirements.txt
|
||||||
run: pipenv requirements > requirements.txt
|
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||||
|
|
||||||
- name: Deploy to cloud.gov
|
- name: Deploy to cloud.gov
|
||||||
uses: 18f/cg-deploy-action@main
|
uses: 18f/cg-deploy-action@main
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ jobs:
|
|||||||
- name: Install application dependencies
|
- name: Install application dependencies
|
||||||
run: make bootstrap
|
run: make bootstrap
|
||||||
|
|
||||||
- name: Create requirements.txt because Cloud Foundry does a weird pipenv thing
|
- name: Create requirements.txt
|
||||||
run: pipenv requirements > requirements.txt
|
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||||
|
|
||||||
- name: Deploy to cloud.gov
|
- name: Deploy to cloud.gov
|
||||||
uses: 18f/cg-deploy-action@main
|
uses: 18f/cg-deploy-action@main
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ jobs:
|
|||||||
- name: Install application dependencies
|
- name: Install application dependencies
|
||||||
run: make bootstrap
|
run: make bootstrap
|
||||||
|
|
||||||
- name: Create requirements.txt because Cloud Foundry does a weird pipenv thing
|
- name: Create requirements.txt
|
||||||
run: pipenv requirements > requirements.txt
|
run: poetry export --without-hashes --format=requirements.txt > requirements.txt
|
||||||
|
|
||||||
- name: Deploy to cloud.gov
|
- name: Deploy to cloud.gov
|
||||||
uses: 18f/cg-deploy-action@main
|
uses: 18f/cg-deploy-action@main
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ GIT_COMMIT ?= $(shell git rev-parse HEAD)
|
|||||||
.PHONY: bootstrap
|
.PHONY: bootstrap
|
||||||
bootstrap: ## Set up everything to run the app
|
bootstrap: ## Set up everything to run the app
|
||||||
make generate-version-file
|
make generate-version-file
|
||||||
pipenv install --dev
|
poetry install
|
||||||
|
poetry self add poetry-dotenv-plugin
|
||||||
createdb notification_api || true
|
createdb notification_api || true
|
||||||
(pipenv run flask db upgrade) || true
|
(poetry run flask db upgrade) || true
|
||||||
|
|
||||||
.PHONY: bootstrap-with-docker
|
.PHONY: bootstrap-with-docker
|
||||||
bootstrap-with-docker: ## Build the image to run the app in Docker
|
bootstrap-with-docker: ## Build the image to run the app in Docker
|
||||||
@@ -22,26 +23,26 @@ bootstrap-with-docker: ## Build the image to run the app in Docker
|
|||||||
|
|
||||||
.PHONY: run-procfile
|
.PHONY: run-procfile
|
||||||
run-procfile:
|
run-procfile:
|
||||||
pipenv run honcho start -f Procfile.dev
|
poetry run honcho start -f Procfile.dev
|
||||||
|
|
||||||
.PHONY: avg-complexity
|
.PHONY: avg-complexity
|
||||||
avg-complexity:
|
avg-complexity:
|
||||||
echo "*** Shows average complexity in radon of all code ***"
|
echo "*** Shows average complexity in radon of all code ***"
|
||||||
pipenv run radon cc ./app -a -na
|
poetry run radon cc ./app -a -na
|
||||||
|
|
||||||
.PHONY: too-complex
|
.PHONY: too-complex
|
||||||
too-complex:
|
too-complex:
|
||||||
echo "*** Shows code that got a rating of C, D or F in radon ***"
|
echo "*** Shows code that got a rating of C, D or F in radon ***"
|
||||||
pipenv run radon cc ./app -a -nc
|
poetry run radon cc ./app -a -nc
|
||||||
|
|
||||||
.PHONY: run-flask
|
.PHONY: run-flask
|
||||||
run-flask: ## Run flask
|
run-flask: ## Run flask
|
||||||
pipenv run newrelic-admin run-program flask run -p 6011 --host=0.0.0.0
|
poetry run newrelic-admin run-program flask run -p 6011 --host=0.0.0.0
|
||||||
|
|
||||||
.PHONY: run-celery
|
.PHONY: run-celery
|
||||||
run-celery: ## Run celery, TODO remove purge for staging/prod
|
run-celery: ## Run celery, TODO remove purge for staging/prod
|
||||||
pipenv run celery -A run_celery.notify_celery purge -f
|
poetry run celery -A run_celery.notify_celery purge -f
|
||||||
pipenv run newrelic-admin run-program celery \
|
poetry run newrelic-admin run-program celery \
|
||||||
-A run_celery.notify_celery worker \
|
-A run_celery.notify_celery worker \
|
||||||
--pidfile="/tmp/celery.pid" \
|
--pidfile="/tmp/celery.pid" \
|
||||||
--loglevel=INFO \
|
--loglevel=INFO \
|
||||||
@@ -50,17 +51,17 @@ run-celery: ## Run celery, TODO remove purge for staging/prod
|
|||||||
|
|
||||||
.PHONY: dead-code
|
.PHONY: dead-code
|
||||||
dead-code:
|
dead-code:
|
||||||
pipenv run vulture ./app --min-confidence=100
|
poetry run vulture ./app --min-confidence=100
|
||||||
|
|
||||||
.PHONY: run-celery-beat
|
.PHONY: run-celery-beat
|
||||||
run-celery-beat: ## Run celery beat
|
run-celery-beat: ## Run celery beat
|
||||||
pipenv run celery \
|
poetry run celery \
|
||||||
-A run_celery.notify_celery beat \
|
-A run_celery.notify_celery beat \
|
||||||
--loglevel=INFO
|
--loglevel=INFO
|
||||||
|
|
||||||
.PHONY: cloudgov-user-report
|
.PHONY: cloudgov-user-report
|
||||||
cloudgov-user-report:
|
cloudgov-user-report:
|
||||||
@pipenv run python -m terraform.ops.cloudgov_user_report
|
@poetry run python -m terraform.ops.cloudgov_user_report
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@@ -73,28 +74,28 @@ generate-version-file: ## Generates the app version file
|
|||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: export NEW_RELIC_ENVIRONMENT=test
|
test: export NEW_RELIC_ENVIRONMENT=test
|
||||||
test: ## Run tests and create coverage report
|
test: ## Run tests and create coverage report
|
||||||
pipenv run black .
|
poetry run black .
|
||||||
pipenv run flake8 .
|
poetry run flake8 .
|
||||||
pipenv run isort --check-only ./app ./tests
|
poetry run isort --check-only ./app ./tests
|
||||||
pipenv run coverage run -m pytest -vv --maxfail=10
|
poetry run coverage run -m pytest -vv --maxfail=10
|
||||||
pipenv run coverage report -m --fail-under=95
|
poetry run coverage report -m --fail-under=95
|
||||||
pipenv run coverage html -d .coverage_cache
|
poetry run coverage html -d .coverage_cache
|
||||||
|
|
||||||
.PHONY: freeze-requirements
|
.PHONY: freeze-requirements
|
||||||
freeze-requirements: ## Pin all requirements including sub dependencies into requirements.txt
|
freeze-requirements: ## Pin all requirements including sub dependencies into requirements.txt
|
||||||
pipenv lock
|
poetry lock
|
||||||
pipenv requirements
|
poetry requirements
|
||||||
|
|
||||||
.PHONY: audit
|
.PHONY: audit
|
||||||
audit:
|
audit:
|
||||||
pipenv requirements > requirements.txt
|
poetry requirements > requirements.txt
|
||||||
pipenv requirements --dev > requirements_for_test.txt
|
poetry requirements --dev > requirements_for_test.txt
|
||||||
pipenv run pip-audit -r requirements.txt
|
poetry run pip-audit -r requirements.txt
|
||||||
-pipenv run pip-audit -r requirements_for_test.txt
|
-poetry run pip-audit -r requirements_for_test.txt
|
||||||
|
|
||||||
.PHONY: static-scan
|
.PHONY: static-scan
|
||||||
static-scan:
|
static-scan:
|
||||||
pipenv run bandit -r app/
|
poetry run bandit -r app/
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
Generated
-2829
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@ On MacOS, using [Homebrew](https://brew.sh/) for package management is highly re
|
|||||||
|
|
||||||
1. Set up Postgres && Redis on your machine
|
1. Set up Postgres && Redis on your machine
|
||||||
|
|
||||||
1. Install [pipenv](https://pipenv.pypa.io/en/latest/)
|
1. Install [poetry](https://python-poetry.org/docs/#installation)
|
||||||
|
|
||||||
1. Run the project setup
|
1. Run the project setup
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ from app.models import (
|
|||||||
SMS_TYPE,
|
SMS_TYPE,
|
||||||
)
|
)
|
||||||
from app.notifications.process_notifications import persist_notification
|
from app.notifications.process_notifications import persist_notification
|
||||||
|
from app.notifications.validators import check_service_over_total_message_limit
|
||||||
from app.serialised_models import SerialisedService, SerialisedTemplate
|
from app.serialised_models import SerialisedService, SerialisedTemplate
|
||||||
from app.service.utils import service_allowed_to_send_to
|
from app.service.utils import service_allowed_to_send_to
|
||||||
from app.utils import DATETIME_FORMAT
|
from app.utils import DATETIME_FORMAT
|
||||||
|
from app.v2.errors import TotalRequestsError
|
||||||
|
|
||||||
|
|
||||||
@notify_celery.task(name="process-job")
|
@notify_celery.task(name="process-job")
|
||||||
@@ -64,6 +66,9 @@ def process_job(job_id, sender_id=None):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if __total_sending_limits_for_job_exceeded(service, job, job_id):
|
||||||
|
return
|
||||||
|
|
||||||
recipient_csv, template, sender_id = get_recipient_csv_and_template_and_sender_id(
|
recipient_csv, template, sender_id = get_recipient_csv_and_template_and_sender_id(
|
||||||
job
|
job
|
||||||
)
|
)
|
||||||
@@ -145,6 +150,25 @@ def process_row(row, template, job, service, sender_id=None):
|
|||||||
return notification_id
|
return notification_id
|
||||||
|
|
||||||
|
|
||||||
|
def __total_sending_limits_for_job_exceeded(service, job, job_id):
|
||||||
|
try:
|
||||||
|
total_sent = check_service_over_total_message_limit(KEY_TYPE_NORMAL, service)
|
||||||
|
if total_sent + job.notification_count > service.total_message_limit:
|
||||||
|
raise TotalRequestsError(service.total_message_limit)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except TotalRequestsError:
|
||||||
|
job.job_status = "sending limits exceeded"
|
||||||
|
job.processing_finished = datetime.utcnow()
|
||||||
|
dao_update_job(job)
|
||||||
|
current_app.logger.error(
|
||||||
|
"Job {} size {} error. Total sending limits {} exceeded".format(
|
||||||
|
job_id, job.notification_count, service.message_limit
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@notify_celery.task(bind=True, name="save-sms", max_retries=5, default_retry_delay=300)
|
@notify_celery.task(bind=True, name="save-sms", max_retries=5, default_retry_delay=300)
|
||||||
def save_sms(self, service_id, notification_id, encrypted_notification, sender_id=None):
|
def save_sms(self, service_id, notification_id, encrypted_notification, sender_id=None):
|
||||||
notification = encryption.decrypt(encrypted_notification)
|
notification = encryption.decrypt(encrypted_notification)
|
||||||
|
|||||||
@@ -772,12 +772,23 @@ def create_user_jwt(token):
|
|||||||
|
|
||||||
def _update_template(id, name, template_type, content, subject):
|
def _update_template(id, name, template_type, content, subject):
|
||||||
template = Template.query.filter_by(id=id).first()
|
template = Template.query.filter_by(id=id).first()
|
||||||
|
if not template:
|
||||||
|
template = Template(id=id)
|
||||||
|
template.service_id = "d6aa2c68-a2d9-4437-ab19-3ae8eb202553"
|
||||||
|
template.created_by_id = "6af522d0-2915-4e52-83a3-3690455a5fe6"
|
||||||
|
db.session.add(template)
|
||||||
template.name = name
|
template.name = name
|
||||||
template.template_type = template_type
|
template.template_type = template_type
|
||||||
template.content = "\n".join(content)
|
template.content = "\n".join(content)
|
||||||
template.subject = subject
|
template.subject = subject
|
||||||
|
|
||||||
history = TemplateHistory.query.filter_by(id=id).first()
|
history = TemplateHistory.query.filter_by(id=id).first()
|
||||||
|
if not history:
|
||||||
|
history = TemplateHistory(id=id)
|
||||||
|
history.service_id = "d6aa2c68-a2d9-4437-ab19-3ae8eb202553"
|
||||||
|
history.created_by_id = "6af522d0-2915-4e52-83a3-3690455a5fe6"
|
||||||
|
history.version = 1
|
||||||
|
db.session.add(history)
|
||||||
history.name = name
|
history.name = name
|
||||||
history.template_type = template_type
|
history.template_type = template_type
|
||||||
history.content = "\n".join(content)
|
history.content = "\n".join(content)
|
||||||
@@ -792,3 +803,27 @@ def update_templates():
|
|||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
for d in data:
|
for d in data:
|
||||||
_update_template(d["id"], d["name"], d["type"], d["content"], d["subject"])
|
_update_template(d["id"], d["name"], d["type"], d["content"], d["subject"])
|
||||||
|
|
||||||
|
|
||||||
|
@notify_command(name="create-new-service")
|
||||||
|
@click.option("-n", "--name", required=True, prompt=True)
|
||||||
|
@click.option("-l", "--message_limit", required=False, default=40000)
|
||||||
|
@click.option("-r", "--restricted", required=False, default=False)
|
||||||
|
@click.option("-e", "--email_from", required=True)
|
||||||
|
@click.option("-c", "--created_by_id", required=True)
|
||||||
|
def create_new_service(name, message_limit, restricted, email_from, created_by_id):
|
||||||
|
data = {
|
||||||
|
"name": name,
|
||||||
|
"message_limit": message_limit,
|
||||||
|
"restricted": restricted,
|
||||||
|
"email_from": email_from,
|
||||||
|
"created_by_id": created_by_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
service = Service(**data)
|
||||||
|
try:
|
||||||
|
db.session.add(service)
|
||||||
|
db.session.commit()
|
||||||
|
except IntegrityError:
|
||||||
|
print("duplicate service", service.name)
|
||||||
|
db.session.rollback()
|
||||||
|
|||||||
@@ -282,6 +282,8 @@ class Config(object):
|
|||||||
|
|
||||||
FREE_SMS_TIER_FRAGMENT_COUNT = 250000
|
FREE_SMS_TIER_FRAGMENT_COUNT = 250000
|
||||||
|
|
||||||
|
TOTAL_MESSAGE_LIMIT = 250000
|
||||||
|
|
||||||
DAILY_MESSAGE_LIMIT = notifications_utils.DAILY_MESSAGE_LIMIT
|
DAILY_MESSAGE_LIMIT = notifications_utils.DAILY_MESSAGE_LIMIT
|
||||||
|
|
||||||
HIGH_VOLUME_SERVICE = json.loads(getenv("HIGH_VOLUME_SERVICE", "[]"))
|
HIGH_VOLUME_SERVICE = json.loads(getenv("HIGH_VOLUME_SERVICE", "[]"))
|
||||||
|
|||||||
@@ -487,6 +487,9 @@ class Service(db.Model, Versioned):
|
|||||||
db.Boolean, index=False, unique=False, nullable=False, default=True
|
db.Boolean, index=False, unique=False, nullable=False, default=True
|
||||||
)
|
)
|
||||||
message_limit = db.Column(db.BigInteger, index=False, unique=False, nullable=False)
|
message_limit = db.Column(db.BigInteger, index=False, unique=False, nullable=False)
|
||||||
|
total_message_limit = db.Column(
|
||||||
|
db.BigInteger, index=False, unique=False, nullable=False
|
||||||
|
)
|
||||||
restricted = db.Column(db.Boolean, index=False, unique=False, nullable=False)
|
restricted = db.Column(db.Boolean, index=False, unique=False, nullable=False)
|
||||||
email_from = db.Column(db.Text, index=False, unique=True, nullable=False)
|
email_from = db.Column(db.Text, index=False, unique=True, nullable=False)
|
||||||
created_by_id = db.Column(
|
created_by_id = db.Column(
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ def persist_notification(
|
|||||||
reply_to_text=None,
|
reply_to_text=None,
|
||||||
billable_units=None,
|
billable_units=None,
|
||||||
document_download_count=None,
|
document_download_count=None,
|
||||||
updated_at=None
|
updated_at=None,
|
||||||
):
|
):
|
||||||
current_app.logger.info("Persisting notification")
|
current_app.logger.info("Persisting notification")
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ def persist_notification(
|
|||||||
current_app.logger.info("Redis total limit cache key does exist")
|
current_app.logger.info("Redis total limit cache key does exist")
|
||||||
redis_store.incr(total_key)
|
redis_store.incr(total_key)
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
"Redis total limit cache key has been incremented"
|
f"Redis total limit cache key has been incremented to {redis_store.get(total_key)}"
|
||||||
)
|
)
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
"{} {} created at {}".format(
|
"{} {} created at {}".format(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from notifications_utils import SMS_CHAR_COUNT_LIMIT
|
|||||||
from notifications_utils.clients.redis import (
|
from notifications_utils.clients.redis import (
|
||||||
daily_total_cache_key,
|
daily_total_cache_key,
|
||||||
rate_limit_cache_key,
|
rate_limit_cache_key,
|
||||||
|
total_limit_cache_key,
|
||||||
)
|
)
|
||||||
from notifications_utils.recipients import (
|
from notifications_utils.recipients import (
|
||||||
get_international_phone_info,
|
get_international_phone_info,
|
||||||
@@ -44,6 +45,27 @@ def check_service_over_api_rate_limit(service, api_key):
|
|||||||
raise RateLimitError(rate_limit, interval, api_key.key_type)
|
raise RateLimitError(rate_limit, interval, api_key.key_type)
|
||||||
|
|
||||||
|
|
||||||
|
def check_service_over_total_message_limit(key_type, service):
|
||||||
|
if key_type == KEY_TYPE_TEST or not current_app.config["REDIS_ENABLED"]:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
cache_key = total_limit_cache_key(service.id)
|
||||||
|
service_stats = redis_store.get(cache_key)
|
||||||
|
if service_stats is None:
|
||||||
|
# first message of the day, set the cache to 0 and the expiry to 24 hours
|
||||||
|
service_stats = 0
|
||||||
|
redis_store.set(cache_key, service_stats, ex=86400)
|
||||||
|
return service_stats
|
||||||
|
if int(service_stats) >= service.total_message_limit:
|
||||||
|
current_app.logger.warning(
|
||||||
|
"service {} has been rate limited for total use sent {} limit {}".format(
|
||||||
|
service.id, int(service_stats), service.total_message_limit
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise TotalRequestsError(service.total_message_limit)
|
||||||
|
return int(service_stats)
|
||||||
|
|
||||||
|
|
||||||
def check_application_over_retention_limit(key_type, service):
|
def check_application_over_retention_limit(key_type, service):
|
||||||
if key_type == KEY_TYPE_TEST or not current_app.config["REDIS_ENABLED"]:
|
if key_type == KEY_TYPE_TEST or not current_app.config["REDIS_ENABLED"]:
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ class DetailedServiceSchema(BaseSchema):
|
|||||||
"inbound_sms",
|
"inbound_sms",
|
||||||
"jobs",
|
"jobs",
|
||||||
"message_limit",
|
"message_limit",
|
||||||
|
"total_message_limit",
|
||||||
"permissions",
|
"permissions",
|
||||||
"rate_limit",
|
"rate_limit",
|
||||||
"reply_to_email_addresses",
|
"reply_to_email_addresses",
|
||||||
@@ -735,6 +736,7 @@ class ServiceHistorySchema(ma.Schema):
|
|||||||
updated_at = FlexibleDateTime()
|
updated_at = FlexibleDateTime()
|
||||||
active = fields.Boolean()
|
active = fields.Boolean()
|
||||||
message_limit = fields.Integer()
|
message_limit = fields.Integer()
|
||||||
|
total_message_limit = fields.Integer()
|
||||||
restricted = fields.Boolean()
|
restricted = fields.Boolean()
|
||||||
email_from = fields.String()
|
email_from = fields.String()
|
||||||
created_by_id = fields.UUID()
|
created_by_id = fields.UUID()
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class SerialisedService(SerialisedModel):
|
|||||||
"contact_link",
|
"contact_link",
|
||||||
"email_from",
|
"email_from",
|
||||||
"message_limit",
|
"message_limit",
|
||||||
|
"total_message_limit",
|
||||||
"permissions",
|
"permissions",
|
||||||
"rate_limit",
|
"rate_limit",
|
||||||
"restricted",
|
"restricted",
|
||||||
|
|||||||
@@ -216,6 +216,8 @@ def create_service():
|
|||||||
raise InvalidRequest(errors, status_code=400)
|
raise InvalidRequest(errors, status_code=400)
|
||||||
data.pop("service_domain", None)
|
data.pop("service_domain", None)
|
||||||
|
|
||||||
|
data["total_message_limit"] = current_app.config["TOTAL_MESSAGE_LIMIT"]
|
||||||
|
|
||||||
# validate json with marshmallow
|
# validate json with marshmallow
|
||||||
service_schema.load(data)
|
service_schema.load(data)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from app.notifications.process_notifications import (
|
|||||||
send_notification_to_queue,
|
send_notification_to_queue,
|
||||||
)
|
)
|
||||||
from app.notifications.validators import (
|
from app.notifications.validators import (
|
||||||
|
check_service_over_total_message_limit,
|
||||||
validate_and_format_recipient,
|
validate_and_format_recipient,
|
||||||
validate_template,
|
validate_template,
|
||||||
)
|
)
|
||||||
@@ -44,6 +45,8 @@ def send_one_off_notification(service_id, post_data):
|
|||||||
|
|
||||||
validate_template(template.id, personalisation, service, template.template_type)
|
validate_template(template.id, personalisation, service, template.template_type)
|
||||||
|
|
||||||
|
check_service_over_total_message_limit(KEY_TYPE_NORMAL, service)
|
||||||
|
|
||||||
validate_and_format_recipient(
|
validate_and_format_recipient(
|
||||||
send_to=post_data["to"],
|
send_to=post_data["to"],
|
||||||
key_type=KEY_TYPE_NORMAL,
|
key_type=KEY_TYPE_NORMAL,
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ cd /workspace
|
|||||||
git status
|
git status
|
||||||
|
|
||||||
make generate-version-file
|
make generate-version-file
|
||||||
pipenv install --dev
|
poetry install --dev
|
||||||
|
|
||||||
# Install virtualenv to support running the isolated make freeze-requirements from within the devcontainer
|
# Install virtualenv to support running the isolated make freeze-requirements from within the devcontainer
|
||||||
pip3 install virtualenv
|
pip3 install virtualenv
|
||||||
|
|
||||||
# Upgrade schema of the notification_api database
|
# Upgrade schema of the notification_api database
|
||||||
pipenv run flask db upgrade
|
poetry run flask db upgrade
|
||||||
|
|
||||||
# Run flask server
|
# Run flask server
|
||||||
# make run-flask
|
# make run-flask
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ cd /workspace
|
|||||||
git status
|
git status
|
||||||
|
|
||||||
make generate-version-file
|
make generate-version-file
|
||||||
pipenv install --dev
|
poetry install --dev
|
||||||
|
|
||||||
# Install virtualenv to support running the isolated make freeze-requirements from within the devcontainer
|
# Install virtualenv to support running the isolated make freeze-requirements from within the devcontainer
|
||||||
pip3 install virtualenv
|
pip3 install virtualenv
|
||||||
|
|||||||
+5
-5
@@ -247,7 +247,7 @@ Within GitHub Actions, several scans take place every day to ensure security and
|
|||||||
|
|
||||||
If you're checking out the system locally, you may want to create a user quickly.
|
If you're checking out the system locally, you may want to create a user quickly.
|
||||||
|
|
||||||
`pipenv run flask command create-test-user`
|
`poetry run flask command create-test-user`
|
||||||
|
|
||||||
This will run an interactive prompt to create a user, and then mark that user as active. *Use a real mobile number* if you want to log in, as the SMS auth code will be sent here.
|
This will run an interactive prompt to create a user, and then mark that user as active. *Use a real mobile number* if you want to log in, as the SMS auth code will be sent here.
|
||||||
|
|
||||||
@@ -327,8 +327,8 @@ Rules for use:
|
|||||||
$ terraform plan
|
$ terraform plan
|
||||||
$ terraform apply
|
$ terraform apply
|
||||||
```
|
```
|
||||||
1. start a pipenv shell as a shortcut to load `.env` file variables: `$ pipenv shell`
|
1. start a poetry shell as a shortcut to load `.env` file variables: `$ poetry shell`
|
||||||
1. Output requirements.txt file: `pipenv requirements > requirements.txt`
|
1. Output requirements.txt file: `poetry export --without-hashes --format=requirements.txt > requirements.txt`
|
||||||
1. Deploy the application:
|
1. Deploy the application:
|
||||||
```
|
```
|
||||||
cf push --vars-file deploy-config/sandbox.yml --var NEW_RELIC_LICENSE_KEY=$NEW_RELIC_LICENSE_KEY
|
cf push --vars-file deploy-config/sandbox.yml --var NEW_RELIC_LICENSE_KEY=$NEW_RELIC_LICENSE_KEY
|
||||||
@@ -405,7 +405,7 @@ command Using a command allows the operation to be tested, both with `pytest` an
|
|||||||
|
|
||||||
To see information about available commands, you can get a list with:
|
To see information about available commands, you can get a list with:
|
||||||
|
|
||||||
`pipenv run flask command`
|
`poetry run flask command`
|
||||||
|
|
||||||
Appending `--help` to any command will give you more information about parameters.
|
Appending `--help` to any command will give you more information about parameters.
|
||||||
|
|
||||||
@@ -797,7 +797,7 @@ US_Notify Administrators are responsible for ensuring that remediations for vuln
|
|||||||
|
|
||||||
Notify.gov DNS records are maintained within [the 18f/dns repository](https://github.com/18F/dns/blob/main/terraform/notify.gov.tf). To create new DNS records for notify.gov or any subdomains:
|
Notify.gov DNS records are maintained within [the 18f/dns repository](https://github.com/18F/dns/blob/main/terraform/notify.gov.tf). To create new DNS records for notify.gov or any subdomains:
|
||||||
|
|
||||||
1. Update the `notify.gov.tf` terraform to update or create the new records within Route53 and push the branch to the 18f/dns repository.
|
1. Update the `notify.gov.tf` terraform to update oƒr create the new records within Route53 and push the branch to the 18f/dns repository.
|
||||||
1. Open a PR.
|
1. Open a PR.
|
||||||
1. Verify that the plan output within circleci creates the records that you expect.
|
1. Verify that the plan output within circleci creates the records that you expect.
|
||||||
1. Request a PR review from the 18F/tts-tech-portfolio team
|
1. Request a PR review from the 18F/tts-tech-portfolio team
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
Revision ID: 0400_add_total_message_limit
|
||||||
|
Revises: 0399_remove_research_mode
|
||||||
|
Create Date: 2023-04-24 11:35:22.873930
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0400_add_total_message_limit"
|
||||||
|
down_revision = "0399_remove_research_mode"
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column("services", sa.Column("total_message_limit", sa.Integer))
|
||||||
|
op.add_column("services_history", sa.Column("total_message_limit", sa.Integer))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column("services", "total_message_limit")
|
||||||
|
op.drop_column("services_history", "total_message_limit")
|
||||||
Generated
+4254
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
[virtualenvs]
|
||||||
|
prefer-active-python = true
|
||||||
+17
-11
@@ -1,9 +1,12 @@
|
|||||||
[[source]]
|
[tool.poetry]
|
||||||
url = "https://pypi.org/simple"
|
name = "notifications-api"
|
||||||
verify_ssl = true
|
version = "0.1.0"
|
||||||
name = "pypi"
|
description = "Notify.gov backend"
|
||||||
|
authors = ["Your Name <you@example.com>"]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
[packages]
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.9"
|
||||||
alembic = "==1.11.2"
|
alembic = "==1.11.2"
|
||||||
amqp = "==5.1.1"
|
amqp = "==5.1.1"
|
||||||
arrow = "==1.2.3"
|
arrow = "==1.2.3"
|
||||||
@@ -45,12 +48,14 @@ flask-sqlalchemy = "==3.0.5"
|
|||||||
gunicorn = {version = "==21.2.0", extras = ["eventlet"]}
|
gunicorn = {version = "==21.2.0", extras = ["eventlet"]}
|
||||||
iso8601 = "==2.0.0"
|
iso8601 = "==2.0.0"
|
||||||
itsdangerous = "==2.1.2"
|
itsdangerous = "==2.1.2"
|
||||||
jsonschema = {version = "==4.19.0", extras = ["format"]}
|
jsonschema = {version = "==4.17.0", extras = ["format"]}
|
||||||
lxml = "==4.9.3"
|
lxml = "==4.9.3"
|
||||||
marshmallow = "==3.20.1"
|
marshmallow = "==3.20.1"
|
||||||
marshmallow-sqlalchemy = "==0.29.0"
|
marshmallow-sqlalchemy = "==0.29.0"
|
||||||
notifications-python-client = "==6.3.0"
|
notifications-python-client = "==6.3.0"
|
||||||
oscrypto = "==1.3.0"
|
oscrypto = "==1.3.0"
|
||||||
|
poetry = "==1.6.1"
|
||||||
|
poetry-dotenv-plugin = "==0.2.0"
|
||||||
psycopg2-binary = "==2.9.3"
|
psycopg2-binary = "==2.9.3"
|
||||||
pyjwt = "==2.8.0"
|
pyjwt = "==2.8.0"
|
||||||
python-dotenv = "==1.0.0"
|
python-dotenv = "==1.0.0"
|
||||||
@@ -58,12 +63,12 @@ radon = "==6.0.1"
|
|||||||
sqlalchemy = "==1.4.40"
|
sqlalchemy = "==1.4.40"
|
||||||
werkzeug = "~=2.3"
|
werkzeug = "~=2.3"
|
||||||
vulture = "==2.8"
|
vulture = "==2.8"
|
||||||
|
|
||||||
packaging = "==23.1"
|
packaging = "==23.1"
|
||||||
notifications-utils = {editable = true, ref = "main", git = "https://github.com/GSA/notifications-utils.git"}
|
notifications-utils = {git = "https://github.com/GSA/notifications-utils.git", develop = true, branch = "main"}
|
||||||
newrelic = "*"
|
newrelic = "*"
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
exceptiongroup = "==1.1.2"
|
exceptiongroup = "==1.1.2"
|
||||||
flake8 = "==4.0.1"
|
flake8 = "==4.0.1"
|
||||||
flake8-bugbear = "==23.3.12"
|
flake8-bugbear = "==23.3.12"
|
||||||
@@ -82,5 +87,6 @@ bandit = "*"
|
|||||||
honcho = "*"
|
honcho = "*"
|
||||||
cloudfoundry-client = "*"
|
cloudfoundry-client = "*"
|
||||||
|
|
||||||
[requires]
|
[build-system]
|
||||||
python_version = "3.9"
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
@@ -211,6 +211,7 @@ def sample_service(sample_user):
|
|||||||
data = {
|
data = {
|
||||||
"name": service_name,
|
"name": service_name,
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"email_from": email_from,
|
"email_from": email_from,
|
||||||
"created_by": sample_user,
|
"created_by": sample_user,
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ def create_service(
|
|||||||
email_from=None,
|
email_from=None,
|
||||||
prefix_sms=True,
|
prefix_sms=True,
|
||||||
message_limit=1000,
|
message_limit=1000,
|
||||||
|
total_message_limit=250000,
|
||||||
organization_type="federal",
|
organization_type="federal",
|
||||||
check_if_service_exists=False,
|
check_if_service_exists=False,
|
||||||
go_live_user=None,
|
go_live_user=None,
|
||||||
@@ -123,6 +124,7 @@ def create_service(
|
|||||||
service = Service(
|
service = Service(
|
||||||
name=service_name,
|
name=service_name,
|
||||||
message_limit=message_limit,
|
message_limit=message_limit,
|
||||||
|
total_message_limit=total_message_limit,
|
||||||
restricted=restricted,
|
restricted=restricted,
|
||||||
email_from=email_from
|
email_from=email_from
|
||||||
if email_from
|
if email_from
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ def test_get_service_by_id(admin_request, sample_service):
|
|||||||
"id",
|
"id",
|
||||||
"inbound_api",
|
"inbound_api",
|
||||||
"message_limit",
|
"message_limit",
|
||||||
|
"total_message_limit",
|
||||||
"name",
|
"name",
|
||||||
"notes",
|
"notes",
|
||||||
"organization",
|
"organization",
|
||||||
@@ -372,6 +373,7 @@ def test_create_service(
|
|||||||
"name": "created service",
|
"name": "created service",
|
||||||
"user_id": str(sample_user.id),
|
"user_id": str(sample_user.id),
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"email_from": "created.service",
|
"email_from": "created.service",
|
||||||
@@ -441,6 +443,7 @@ def test_create_service_with_domain_sets_organization(
|
|||||||
"name": "created service",
|
"name": "created service",
|
||||||
"user_id": str(sample_user.id),
|
"user_id": str(sample_user.id),
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"email_from": "created.service",
|
"email_from": "created.service",
|
||||||
@@ -465,6 +468,7 @@ def test_create_service_should_create_annual_billing_for_service(
|
|||||||
"name": "created service",
|
"name": "created service",
|
||||||
"user_id": str(sample_user.id),
|
"user_id": str(sample_user.id),
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"email_from": "created.service",
|
"email_from": "created.service",
|
||||||
@@ -488,6 +492,7 @@ def test_create_service_should_raise_exception_and_not_create_service_if_annual_
|
|||||||
"name": "created service",
|
"name": "created service",
|
||||||
"user_id": str(sample_user.id),
|
"user_id": str(sample_user.id),
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"email_from": "created.service",
|
"email_from": "created.service",
|
||||||
@@ -518,6 +523,7 @@ def test_create_service_inherits_branding_from_organization(
|
|||||||
"name": "created service",
|
"name": "created service",
|
||||||
"user_id": str(sample_user.id),
|
"user_id": str(sample_user.id),
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"email_from": "created.service",
|
"email_from": "created.service",
|
||||||
@@ -536,6 +542,7 @@ def test_should_not_create_service_with_missing_user_id_field(notify_api, fake_u
|
|||||||
"email_from": "service",
|
"email_from": "service",
|
||||||
"name": "created service",
|
"name": "created service",
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"created_by": str(fake_uuid),
|
"created_by": str(fake_uuid),
|
||||||
@@ -556,6 +563,7 @@ def test_should_error_if_created_by_missing(notify_api, sample_user):
|
|||||||
"email_from": "service",
|
"email_from": "service",
|
||||||
"name": "created service",
|
"name": "created service",
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"user_id": str(sample_user.id),
|
"user_id": str(sample_user.id),
|
||||||
@@ -581,6 +589,7 @@ def test_should_not_create_service_with_missing_if_user_id_is_not_in_database(
|
|||||||
"user_id": fake_uuid,
|
"user_id": fake_uuid,
|
||||||
"name": "created service",
|
"name": "created service",
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"created_by": str(fake_uuid),
|
"created_by": str(fake_uuid),
|
||||||
@@ -623,6 +632,7 @@ def test_should_not_create_service_with_duplicate_name(
|
|||||||
"name": sample_service.name,
|
"name": sample_service.name,
|
||||||
"user_id": str(sample_service.users[0].id),
|
"user_id": str(sample_service.users[0].id),
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"email_from": "sample.service2",
|
"email_from": "sample.service2",
|
||||||
@@ -650,6 +660,7 @@ def test_create_service_should_throw_duplicate_key_constraint_for_existing_email
|
|||||||
"name": service_name,
|
"name": service_name,
|
||||||
"user_id": str(first_service.users[0].id),
|
"user_id": str(first_service.users[0].id),
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"email_from": "first.service",
|
"email_from": "first.service",
|
||||||
@@ -1137,6 +1148,7 @@ def test_default_permissions_are_added_for_user_service(
|
|||||||
"name": "created service",
|
"name": "created service",
|
||||||
"user_id": str(sample_user.id),
|
"user_id": str(sample_user.id),
|
||||||
"message_limit": 1000,
|
"message_limit": 1000,
|
||||||
|
"total_message_limit": 250000,
|
||||||
"restricted": False,
|
"restricted": False,
|
||||||
"active": False,
|
"active": False,
|
||||||
"email_from": "created.service",
|
"email_from": "created.service",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import pytest
|
|||||||
|
|
||||||
from app.commands import (
|
from app.commands import (
|
||||||
_update_template,
|
_update_template,
|
||||||
|
create_new_service,
|
||||||
create_test_user,
|
create_test_user,
|
||||||
fix_billable_units,
|
fix_billable_units,
|
||||||
insert_inbound_numbers_from_file,
|
insert_inbound_numbers_from_file,
|
||||||
@@ -24,6 +25,7 @@ from app.models import (
|
|||||||
Job,
|
Job,
|
||||||
Notification,
|
Notification,
|
||||||
Organization,
|
Organization,
|
||||||
|
Service,
|
||||||
Template,
|
Template,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
@@ -324,3 +326,39 @@ def test_update_template(notify_db_session, email_2fa_code_template):
|
|||||||
t = Template.query.all()
|
t = Template.query.all()
|
||||||
|
|
||||||
assert t[0].name == "Example text message template!"
|
assert t[0].name == "Example text message template!"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_service_command(notify_db_session, notify_api):
|
||||||
|
notify_api.test_cli_runner().invoke(
|
||||||
|
create_test_user,
|
||||||
|
[
|
||||||
|
"--email",
|
||||||
|
"somebody@fake.gov",
|
||||||
|
"--mobile_number",
|
||||||
|
"202-555-5555",
|
||||||
|
"--password",
|
||||||
|
"correct horse battery staple",
|
||||||
|
"--name",
|
||||||
|
"Fake Personson",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
user = User.query.first()
|
||||||
|
|
||||||
|
service_count = Service.query.count()
|
||||||
|
|
||||||
|
# run the command
|
||||||
|
result = notify_api.test_cli_runner().invoke(
|
||||||
|
create_new_service,
|
||||||
|
["-e", "somebody@fake.gov", "-n", "Fake Service", "-c", user.id],
|
||||||
|
)
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# there should be one more service
|
||||||
|
assert Service.query.count() == service_count + 1
|
||||||
|
|
||||||
|
# that service should be the one we added
|
||||||
|
service = Service.query.filter_by(name="Fake Service").first()
|
||||||
|
assert service.email_from == "somebody@fake.gov"
|
||||||
|
assert service.restricted is False
|
||||||
|
assert service.message_limit == 40000
|
||||||
|
|||||||
Reference in New Issue
Block a user