Merge branch 'main' into notify-api-446

This commit is contained in:
stvnrlly
2023-09-06 16:05:45 -04:00
30 changed files with 4496 additions and 2892 deletions
+2 -2
View File
@@ -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
+8 -8
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+25 -24
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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
+24
View File
@@ -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)
+35
View File
@@ -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()
+2
View File
@@ -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", "[]"))
+3
View File
@@ -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(
+2 -2
View File
@@ -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(
+22
View File
@@ -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
+2
View File
@@ -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()
+1
View File
@@ -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",
+2
View File
@@ -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)
+3
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
[virtualenvs]
prefer-active-python = true
+17 -11
View File
@@ -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"
+1
View File
@@ -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,
+2
View File
@@ -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
+12
View File
@@ -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",
+38
View File
@@ -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