-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat/scaffold UI test user on demand (#2286)
* initial commit * feat: create and destroy test users on demand * chore(config): add some hardcoded UUIDs for cypress related items * feat(cypress api): update create_test_user route: - add safeguards to ensure this cant run in prod - hash the password on the fly instead of storing another secret * feat(cypress api): update cleanup_stale_users route: - add safeguards to ensure this cant run in prod - remove/update related tables * chore: formatting * chore(format): formatting files I haven't touched :( * chore: formatting * chore: formatting * chore: formatting * chore: update docstrings; enhance exception handling * fix(cypress api): use service id from config * chore: add more cypress values to config * feat(cypress api): dont pass password around since its already a secret in this repo; create an admin user at the same time as a regular user * feat(cypress data): migration to create cypress service, permissions, users and templates * feat(create_test_user): update delete logic to be more complete; paramaterize email address; ensure passed in param is alphanumeric * chore: formatting * chore: remove unreachable code * task: add config values for testing * feat(auth): add separate auth mechanism for ui testing * chore: add tests * chore: formatting * chore: fix tests * chore: remove unused code * chore: fix migration due to incoming migrations * chore: re-number migrations due to merge * task: pass new secret `CYPRESS_USER_PW_SECRET` into CI * chore: add some debugging to the workflow * task: debug workflow * fix(workflow): put the env statement in the right place * chore: fix workflow * chore: add __init__.py in `tests/app/cypress` folder to make tests work * Plz [review] ! * chore: fix incorrect descriptions * fix: correct typo in function name * fix: use cds domain instead of gov.uk * chore: fix failing test
- Loading branch information
1 parent
6fbb101
commit 9f30e3a
Showing
14 changed files
with
537 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
""" | ||
This module will be used by the cypress tests to create users on the fly whenever a test suite is run, and clean | ||
them up periodically to keep the data footprint small. | ||
""" | ||
|
||
import hashlib | ||
import re | ||
import uuid | ||
from datetime import datetime, timedelta | ||
|
||
from flask import Blueprint, current_app, jsonify | ||
|
||
from app import db | ||
from app.dao.services_dao import dao_add_user_to_service | ||
from app.dao.users_dao import save_model_user | ||
from app.errors import register_errors | ||
from app.models import ( | ||
AnnualBilling, | ||
LoginEvent, | ||
Permission, | ||
Service, | ||
ServicePermission, | ||
ServiceUser, | ||
Template, | ||
TemplateHistory, | ||
TemplateRedacted, | ||
User, | ||
VerifyCode, | ||
) | ||
|
||
cypress_blueprint = Blueprint("cypress", __name__) | ||
register_errors(cypress_blueprint) | ||
|
||
EMAIL_PREFIX = "notify-ui-tests+ag_" | ||
|
||
|
||
@cypress_blueprint.route("/create_user/<email_name>", methods=["POST"]) | ||
def create_test_user(email_name): | ||
""" | ||
Create a test user for Notify UI testing. | ||
Args: | ||
email_name (str): The name to be used in the email address of the test user. | ||
Returns: | ||
dict: A dictionary containing the serialized user information. | ||
""" | ||
if current_app.config["NOTIFY_ENVIRONMENT"] == "production": | ||
return jsonify(message="Forbidden"), 403 | ||
|
||
# Sanitize email_name to allow only alphanumeric characters | ||
if not re.match(r"^[a-z0-9]+$", email_name): | ||
return jsonify(message="Invalid email name"), 400 | ||
|
||
try: | ||
# Create the users | ||
user_regular = { | ||
"id": uuid.uuid4(), | ||
"name": "Notify UI testing account", | ||
"email_address": f"{EMAIL_PREFIX}{email_name}@cds-snc.ca", | ||
"password": hashlib.sha256( | ||
(current_app.config["CYPRESS_USER_PW_SECRET"] + current_app.config["DANGEROUS_SALT"]).encode("utf-8") | ||
).hexdigest(), | ||
"mobile_number": "9025555555", | ||
"state": "active", | ||
"blocked": False, | ||
} | ||
|
||
user = User(**user_regular) | ||
save_model_user(user) | ||
|
||
# Create the users | ||
user_admin = { | ||
"id": uuid.uuid4(), | ||
"name": "Notify UI testing account", | ||
"email_address": f"{EMAIL_PREFIX}{email_name}[email protected]", | ||
"password": hashlib.sha256( | ||
(current_app.config["CYPRESS_USER_PW_SECRET"] + current_app.config["DANGEROUS_SALT"]).encode("utf-8") | ||
).hexdigest(), | ||
"mobile_number": "9025555555", | ||
"state": "active", | ||
"blocked": False, | ||
"platform_admin": True, | ||
} | ||
|
||
user2 = User(**user_admin) | ||
save_model_user(user2) | ||
|
||
# add user to cypress service w/ full permissions | ||
service = Service.query.filter_by(id=current_app.config["CYPRESS_SERVICE_ID"]).first() | ||
permissions_reg = [] | ||
for p in [ | ||
"manage_users", | ||
"manage_templates", | ||
"manage_settings", | ||
"send_texts", | ||
"send_emails", | ||
"send_letters", | ||
"manage_api_keys", | ||
"view_activity", | ||
]: | ||
permissions_reg.append(Permission(permission=p)) | ||
|
||
dao_add_user_to_service(service, user, permissions=permissions_reg) | ||
|
||
permissions_admin = [] | ||
for p in [ | ||
"manage_users", | ||
"manage_templates", | ||
"manage_settings", | ||
"send_texts", | ||
"send_emails", | ||
"send_letters", | ||
"manage_api_keys", | ||
"view_activity", | ||
]: | ||
permissions_admin.append(Permission(permission=p)) | ||
dao_add_user_to_service(service, user2, permissions=permissions_admin) | ||
|
||
current_app.logger.info(f"Created test user {user.email_address} and {user2.email_address}") | ||
except Exception: | ||
return jsonify(message="Error creating user"), 400 | ||
|
||
users = {"regular": user.serialize(), "admin": user2.serialize()} | ||
|
||
return jsonify(users), 201 | ||
|
||
|
||
def _destroy_test_user(email_name): | ||
user = User.query.filter_by(email_address=f"{EMAIL_PREFIX}{email_name}@cds-snc.ca").first() | ||
|
||
if not user: | ||
current_app.logger.error(f"Error destroying test user {user.email_address}: no user found") | ||
return | ||
|
||
try: | ||
# update the cypress service's created_by to be the main cypress user | ||
# this value gets changed when updating branding (and possibly other updates to service) | ||
# and is a bug | ||
cypress_service = Service.query.filter_by(id=current_app.config["CYPRESS_SERVICE_ID"]).first() | ||
cypress_service.created_by_id = current_app.config["CYPRESS_TEST_USER_ID"] | ||
|
||
# cycle through all the services created by this user, remove associated entities | ||
services = Service.query.filter_by(created_by=user).filter(Service.id != current_app.config["CYPRESS_SERVICE_ID"]) | ||
for service in services.all(): | ||
TemplateHistory.query.filter_by(service_id=service.id).delete() | ||
|
||
Template.query.filter_by(service_id=service.id).delete() | ||
AnnualBilling.query.filter_by(service_id=service.id).delete() | ||
ServicePermission.query.filter_by(service_id=service.id).delete() | ||
Permission.query.filter_by(service_id=service.id).delete() | ||
|
||
services.delete() | ||
|
||
# remove all entities related to the user itself | ||
TemplateRedacted.query.filter_by(updated_by=user).delete() | ||
TemplateHistory.query.filter_by(created_by=user).delete() | ||
Template.query.filter_by(created_by=user).delete() | ||
Permission.query.filter_by(user=user).delete() | ||
LoginEvent.query.filter_by(user=user).delete() | ||
ServiceUser.query.filter_by(user_id=user.id).delete() | ||
VerifyCode.query.filter_by(user=user).delete() | ||
User.query.filter_by(email_address=f"{EMAIL_PREFIX}{email_name}@cds-snc.ca").delete() | ||
|
||
db.session.commit() | ||
|
||
except Exception as e: | ||
current_app.logger.error(f"Error destroying test user {user.email_address}: {str(e)}") | ||
db.session.rollback() | ||
|
||
|
||
@cypress_blueprint.route("/cleanup", methods=["GET"]) | ||
def cleanup_stale_users(): | ||
""" | ||
Method for cleaning up stale users. This method will only be used internally by the Cypress tests. | ||
This method is responsible for removing stale testing users from the database. | ||
Stale users are identified as users whose email addresses match the pattern "%notify-ui-tests+ag_%@cds-snc.ca%" and whose creation time is older than three hours ago. | ||
If this is accessed from production, it will return a 403 Forbidden response. | ||
Returns: | ||
A JSON response with a success message if the cleanup is successful, or an error message if an exception occurs during the cleanup process. | ||
""" | ||
if current_app.config["NOTIFY_ENVIRONMENT"] == "production": | ||
return jsonify(message="Forbidden"), 403 | ||
|
||
try: | ||
three_hours_ago = datetime.utcnow() - timedelta(hours=3) | ||
users = User.query.filter( | ||
User.email_address.like(f"%{EMAIL_PREFIX}%@cds-snc.ca%"), User.created_at < three_hours_ago | ||
).all() | ||
|
||
# loop through users and call destroy_user on each one | ||
for user in users: | ||
user_email = user.email_address.split("+ag_")[1].split("@")[0] | ||
_destroy_test_user(user_email) | ||
|
||
db.session.commit() | ||
except Exception: | ||
current_app.logger.error("[cleanup_stale_users]: error cleaning up test users") | ||
return jsonify(message="Error cleaning up"), 500 | ||
|
||
current_app.logger.info("[cleanup_stale_users]: Cleaned up stale test users") | ||
return jsonify(message="Clean up complete"), 201 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.