diff --git a/app/config.py b/app/config.py index 4ba6fbd89e..e552367dcc 100644 --- a/app/config.py +++ b/app/config.py @@ -743,6 +743,7 @@ class Test(Development): TEMPLATE_PREVIEW_API_HOST = "http://localhost:9999" FAILED_LOGIN_LIMIT = 0 GC_ORGANISATIONS_BUCKET_NAME = "test-gc-organisations" + CYPRESS_USER_EMAIL_PREFIX = "notify-ui-tests+ag_" class Production(Config): diff --git a/app/cypress/decorators.py b/app/cypress/decorators.py new file mode 100644 index 0000000000..f5a0828516 --- /dev/null +++ b/app/cypress/decorators.py @@ -0,0 +1,26 @@ +import os +from functools import wraps + +from flask import current_app, jsonify + +from app.models import User + +EMAIL_PREFIX = os.getenv("CYPRESS_USER_EMAIL_PREFIX", "notify-ui-tests+ag_") + + +def fetch_cypress_user_by_id(func): + """A simple decorator to fetch a user by id and pass it to the decorated function. + Useful to reduce boilerplate in the Cypress REST routes that delete by user id. + """ + + @wraps(func) + def wrapper(user_id, *args, **kwargs): + user = User.query.filter_by(id=user_id).first() + + if not user: + current_app.logger.error(f"Error: No user found with id {user_id}") + return jsonify({"error": f"User id {user_id} not found"}), 404 + + return func(user_id, user, *args, **kwargs) # Pass user instead of email_name + + return wrapper diff --git a/app/cypress/rest.py b/app/cypress/rest.py index 956c684b31..417882ab83 100644 --- a/app/cypress/rest.py +++ b/app/cypress/rest.py @@ -11,17 +11,21 @@ from flask import Blueprint, current_app, jsonify from app import db +from app.cypress.decorators import fetch_cypress_user_by_id from app.dao.services_dao import dao_add_user_to_service +from app.dao.template_categories_dao import dao_delete_template_category_by_id from app.dao.users_dao import save_model_user from app.errors import register_errors from app.models import ( AnnualBilling, + EmailBranding, LoginEvent, Permission, Service, ServicePermission, ServiceUser, Template, + TemplateCategory, TemplateHistory, TemplateRedacted, User, @@ -141,6 +145,7 @@ def _destroy_test_user(email_name): 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() @@ -156,6 +161,8 @@ def _destroy_test_user(email_name): TemplateRedacted.query.filter_by(updated_by=user).delete() TemplateHistory.query.filter_by(created_by=user).delete() Template.query.filter_by(created_by=user).delete() + TemplateCategory.query.filter_by(created_by=user).delete() + EmailBranding.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() @@ -169,6 +176,50 @@ def _destroy_test_user(email_name): db.session.rollback() +@cypress_blueprint.route("/template-categories/cleanup/", methods=["POST"]) +@fetch_cypress_user_by_id +def delete_template_categories_by_user_id(user_id, user): + """Deletes all template categories created by user_id. + + + Args: + user_id (str): The id of the user to delete template categories for. + user (User): The DB user object to delete template categories for, fetched by the fetch_cypress_user_by_id decorator. + + Returns: + A JSON response with a 201 if all template categories were successfully deleted. If a template fails deletion the ID + is stored and returned with a 207 response. + """ + query = TemplateCategory.query.filter_by(created_by_id=user_id) + results = query.all() + remaining = [] + + current_app.logger.info(f"[Cypress API]: Deleting {len(results)} template categories created by user {user_id}.") + + for template_category in results: + try: + dao_delete_template_category_by_id(template_category.id, cascade=True) + except Exception as e: + current_app.logger.info(f"[Cypress API]: Error deleting template category {template_category.id}: {str(e)}") + remaining.append(template_category.id) + + if remaining: + message = ( + jsonify( + message=f"Template category clean up complete {len(results) - len(remaining)} of {len(results)} deleted.", + failed_category_ids=remaining, + ), + 207, + ) + else: + message = ( + jsonify(message=f"Template category clean up complete {len(results) - len(remaining)} of {len(results)} deleted."), + 201, + ) + + return message + + @cypress_blueprint.route("/cleanup", methods=["GET"]) def cleanup_stale_users(): """ diff --git a/tests/app/conftest.py b/tests/app/conftest.py index e5e7675488..f8b512b6c2 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -415,6 +415,10 @@ def create_template_category( hidden=False, created_by_id=None, ): + if not created_by_id: + user = create_user() + created_by_id = user.id + data = { "name_en": name_en, "name_fr": name_fr, @@ -423,12 +427,9 @@ def create_template_category( "sms_process_type": sms_process_type, "email_process_type": email_process_type, "hidden": hidden, + "created_by_id": created_by_id, } - if not created_by_id: - user = create_user() - data.update({"created_by_id": str(user.id)}) - template_category = TemplateCategory(**data) dao_create_template_category(template_category) diff --git a/tests/app/cypress/test_rest.py b/tests/app/cypress/test_rest.py index 079d1de05d..dbc513b753 100644 --- a/tests/app/cypress/test_rest.py +++ b/tests/app/cypress/test_rest.py @@ -1,11 +1,14 @@ import json +import os from datetime import datetime, timedelta +from unittest.mock import patch from app.models import User from tests import create_cypress_authorization_header +from tests.app.conftest import create_sample_template, create_template_category from tests.conftest import set_config_values -EMAIL_PREFIX = "notify-ui-tests+ag_" +EMAIL_PREFIX = os.getenv("CYPRESS_USER_EMAIL_PREFIX", "notify-ui-tests+ag_") def test_create_test_user(client, sample_service_cypress): @@ -97,3 +100,43 @@ def test_cleanup_stale_users(client, sample_service_cypress, cypress_user, notif user = User.query.filter_by(email_address=f"{EMAIL_PREFIX}emailsuffix_admin@cds-snc.ca").first() assert user is None + + +def test_delete_template_categories_by_user_id_success(client, cypress_user, notify_db, notify_db_session): + cascade = "true" + path = f"/cypress/template-categories/cleanup/{cypress_user.id}?cascade={cascade}" + auth_header = create_cypress_authorization_header() + + category = create_template_category(notify_db, notify_db_session, created_by_id=cypress_user.id) + + with patch("app.cypress.rest.dao_delete_template_category_by_id") as mock_delete: + mock_delete.return_value = None # Simulate successful deletion + response = client.post(path, headers=[auth_header], content_type="application/json") + + assert response.status_code == 201 + resp_json = json.loads(response.get_data(as_text=True)) + assert resp_json["message"] == "Template category clean up complete 1 of 1 deleted." + + # Verify the mock was called for each template category + assert mock_delete.call_count == 1 + mock_delete.assert_any_call(category.id, cascade=True) + + +def test_delete_template_categories_by_user_id_exception(client, cypress_user, notify_db, notify_db_session): + path = f"/cypress/template-categories/cleanup/{cypress_user.id}" + auth_header = create_cypress_authorization_header() + + # Mock template categories created by the user + categories = [ + create_template_category(notify_db, notify_db_session, name_en="1", name_fr="1", created_by_id=cypress_user.id), + create_template_category(notify_db, notify_db_session, created_by_id=cypress_user.id), + ] + create_sample_template(notify_db, notify_db_session, template_category=categories[0]) + + with patch("app.cypress.rest.dao_delete_template_category_by_id", side_effect=Exception("bad things happened")): + response = client.post(path, headers=[auth_header], content_type="application/json") + + assert response.status_code == 207 + resp_json = json.loads(response.get_data(as_text=True)) + assert resp_json["message"] == "Template category clean up complete 0 of 2 deleted." + assert len(resp_json["failed_category_ids"]) == 2